├── .darklua-wally.json ├── .darklua.json ├── .gitattributes ├── .github ├── pull_request_template.md └── workflows │ ├── release.yml │ └── test.yml ├── .gitignore ├── .luaurc ├── .moonwave ├── custom.css └── static │ └── components │ ├── background │ ├── dark.png │ └── light.png │ ├── button │ ├── dark.png │ └── light.png │ ├── checkbox │ ├── dark.png │ └── light.png │ ├── colorpicker │ ├── dark.png │ └── light.png │ ├── datepicker │ ├── dark.png │ └── light.png │ ├── dropdown │ ├── dark.png │ └── light.png │ ├── dropshadowframe │ ├── dark.png │ └── light.png │ ├── label │ ├── dark.png │ └── light.png │ ├── loadingdots │ ├── dark.gif │ └── light.gif │ ├── mainbutton │ ├── dark.png │ └── light.png │ ├── numbersequencepicker │ ├── dark.png │ └── light.png │ ├── numericinput │ ├── dark.png │ └── light.png │ ├── progressbar │ ├── dark.png │ └── light.png │ ├── radiobutton │ ├── dark.png │ └── light.png │ ├── scrollframe │ ├── dark.png │ └── light.png │ ├── slider │ ├── dark.png │ └── light.png │ ├── splitter │ ├── dark.png │ └── light.png │ ├── tabcontainer │ ├── dark.png │ └── light.png │ └── textinput │ ├── dark.png │ └── light.png ├── .npmignore ├── .styluaignore ├── .vscode ├── extensions.json └── settings.json ├── CHANGELOG.md ├── LICENSE ├── README.md ├── docs ├── getting-started.md └── intro.md ├── foreman.toml ├── model.project.json ├── moonwave.toml ├── package-lock.json ├── package.json ├── scripts ├── analyze.sh ├── build-assets.sh ├── build-roblox-model.sh ├── build-wally-package.sh ├── install-deps.sh ├── npm-to-wally.js └── serve.sh ├── selene.toml ├── selene_defs.yml ├── serve.project.json ├── src ├── CommonProps.luau ├── Components │ ├── Background.luau │ ├── Button.luau │ ├── Checkbox.luau │ ├── ColorPicker.luau │ ├── DatePicker.luau │ ├── DropShadowFrame.luau │ ├── Dropdown │ │ ├── ClearButton.luau │ │ ├── DropdownItem.luau │ │ ├── Types.luau │ │ └── init.luau │ ├── Foundation │ │ ├── BaseButton.luau │ │ ├── BaseIcon.luau │ │ ├── BaseLabelledToggle.luau │ │ └── BaseTextInput.luau │ ├── Label.luau │ ├── LoadingDots.luau │ ├── MainButton.luau │ ├── NumberSequencePicker │ │ ├── AxisLabel.luau │ │ ├── Constants.luau │ │ ├── DashedLine.luau │ │ ├── FreeLine.luau │ │ ├── LabelledNumericInput.luau │ │ ├── SequenceNode.luau │ │ └── init.luau │ ├── NumericInput.luau │ ├── PluginProvider.luau │ ├── ProgressBar.luau │ ├── RadioButton.luau │ ├── ScrollFrame │ │ ├── Constants.luau │ │ ├── ScrollBar.luau │ │ ├── ScrollBarArrow.luau │ │ ├── Types.luau │ │ └── init.luau │ ├── Slider.luau │ ├── Splitter.luau │ ├── TabContainer.luau │ └── TextInput.luau ├── Constants.luau ├── Contexts │ ├── PluginContext.luau │ └── ThemeContext.luau ├── Hooks │ ├── useFreshCallback.luau │ ├── useMouseDrag.luau │ ├── useMouseIcon.luau │ ├── usePlugin.luau │ └── useTheme.luau ├── Stories │ ├── Background.story.luau │ ├── Button.story.luau │ ├── Checkbox.story.luau │ ├── ColorPicker.story.luau │ ├── DatePicker.story.luau │ ├── DropShadowFrame.story.luau │ ├── Dropdown.story.luau │ ├── Helpers │ │ ├── createStory.luau │ │ ├── getStoryPlugin.luau │ │ └── studiocomponents.storybook.luau │ ├── Label.story.luau │ ├── LoadingDots.story.luau │ ├── MainButton.story.luau │ ├── NumberSequencePicker.story.luau │ ├── NumericInput.story.luau │ ├── ProgressBar.story.luau │ ├── RadioButton.story.luau │ ├── ScrollFrame.story.luau │ ├── Slider.story.luau │ ├── Splitter.story.luau │ ├── TabContainer.story.luau │ └── TextInput.story.luau ├── getTextSize.luau └── init.luau └── stylua.toml /.darklua-wally.json: -------------------------------------------------------------------------------- 1 | { 2 | "process": [ 3 | { 4 | "rule": "convert_require", 5 | "current": { 6 | "name": "path", 7 | "sources": { 8 | "@pkg": "." 9 | } 10 | }, 11 | "target": { 12 | "name": "roblox", 13 | "rojo_sourcemap": "./sourcemap.json", 14 | "indexing_style": "find_first_child" 15 | } 16 | } 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /.darklua.json: -------------------------------------------------------------------------------- 1 | { 2 | "rules": [ 3 | { 4 | "rule": "convert_require", 5 | "current": { 6 | "name": "path", 7 | "sources": { 8 | "@pkg": "node_modules/.luau-aliases" 9 | } 10 | }, 11 | "target": { 12 | "name": "roblox", 13 | "rojo_sourcemap": "./darklua-sourcemap.json", 14 | "indexing_style": "find_first_child" 15 | } 16 | } 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text eol=lf 2 | 3 | *.gif binary 4 | *.ico binary 5 | *.jpg binary 6 | *.png binary 7 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | Closes #[issue number] 2 | 3 | 4 | 5 | - [ ] add entry to the changelog 6 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | "on": 4 | workflow_dispatch: 5 | inputs: 6 | release_tag: 7 | description: The version to release starting with `v` 8 | required: true 9 | type: string 10 | release_ref: 11 | description: The branch, tag or SHA to checkout (default to latest) 12 | default: "" 13 | type: string 14 | 15 | permissions: 16 | contents: write 17 | 18 | jobs: 19 | publish-package: 20 | name: Publish package 21 | runs-on: ubuntu-latest 22 | steps: 23 | - uses: actions/checkout@v4 24 | 25 | - name: Enable corepack 26 | run: corepack enable 27 | 28 | - uses: actions/setup-node@v3 29 | with: 30 | node-version: latest 31 | registry-url: https://registry.npmjs.org 32 | cache: npm 33 | cache-dependency-path: package-lock.json 34 | 35 | - name: Install packages 36 | run: npm ci 37 | 38 | - name: Publish to npm 39 | run: npm publish --access public 40 | env: 41 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} 42 | 43 | publish-wally-package: 44 | needs: publish-package 45 | name: Publish wally package 46 | runs-on: ubuntu-latest 47 | steps: 48 | - uses: actions/checkout@v4 49 | 50 | - name: Enable corepack 51 | run: corepack enable 52 | 53 | - uses: Roblox/setup-foreman@v1 54 | with: 55 | token: ${{ secrets.GITHUB_TOKEN }} 56 | 57 | - uses: actions/setup-node@v3 58 | with: 59 | node-version: latest 60 | registry-url: https://registry.npmjs.org 61 | cache: npm 62 | cache-dependency-path: package-lock.json 63 | 64 | - name: Install packages 65 | run: npm ci 66 | 67 | - name: Build assets 68 | run: npm run build-assets 69 | 70 | - name: Login to wally 71 | run: wally login --project-path build/wally --token ${{ secrets.WALLY_ACCESS_TOKEN }} 72 | 73 | - name: Publish to wally 74 | run: wally publish --project-path build/wally 75 | 76 | create-release: 77 | needs: publish-package 78 | name: Create release 79 | runs-on: ubuntu-latest 80 | outputs: 81 | upload_url: ${{ steps.create_release.outputs.upload_url }} 82 | steps: 83 | - uses: actions/checkout@v4 84 | 85 | - name: Create tag 86 | run: | 87 | git fetch --tags --no-recurse-submodules 88 | if [ ! $(git tag -l ${{ inputs.release_tag }}) ]; then 89 | git tag ${{ inputs.release_tag }} 90 | git push origin ${{ inputs.release_tag }} 91 | fi 92 | 93 | - name: Create release 94 | id: create_release 95 | uses: softprops/action-gh-release@v1 96 | env: 97 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 98 | with: 99 | tag_name: ${{ inputs.release_tag }} 100 | name: ${{ inputs.release_tag }} 101 | draft: false 102 | 103 | build-assets: 104 | needs: create-release 105 | name: Add assets 106 | runs-on: ubuntu-latest 107 | strategy: 108 | fail-fast: false 109 | matrix: 110 | include: 111 | - artifact-name: studiocomponents.rbxm 112 | path: build/studiocomponents.rbxm 113 | asset-type: application/octet-stream 114 | steps: 115 | - uses: actions/checkout@v4 116 | 117 | - uses: Roblox/setup-foreman@v1 118 | with: 119 | token: ${{ secrets.GITHUB_TOKEN }} 120 | 121 | - uses: actions/setup-node@v3 122 | with: 123 | node-version: latest 124 | registry-url: https://registry.npmjs.org 125 | cache: npm 126 | cache-dependency-path: package-lock.json 127 | 128 | - name: Install packages 129 | run: npm ci 130 | 131 | - name: Build assets 132 | run: npm run build-assets 133 | 134 | - name: Upload asset 135 | uses: actions/upload-artifact@v3 136 | with: 137 | name: ${{ matrix.artifact-name }} 138 | path: ${{ matrix.path }} 139 | 140 | - name: Add asset to Release 141 | uses: actions/upload-release-asset@v1 142 | env: 143 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 144 | with: 145 | upload_url: ${{ needs.create-release.outputs.upload_url }} 146 | asset_path: ${{ matrix.path }} 147 | asset_name: ${{ matrix.artifact-name }} 148 | asset_content_type: ${{ matrix.asset-type }} 149 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Tests 2 | 3 | "on": 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | branches: 9 | - main 10 | 11 | jobs: 12 | test: 13 | name: Run tests 14 | runs-on: ubuntu-latest 15 | steps: 16 | - uses: actions/checkout@v4 17 | 18 | - uses: Roblox/setup-foreman@v1 19 | with: 20 | token: ${{ secrets.GITHUB_TOKEN }} 21 | 22 | - uses: actions/setup-node@v3 23 | with: 24 | node-version: latest 25 | registry-url: https://registry.npmjs.org 26 | cache: npm 27 | cache-dependency-path: package-lock.json 28 | 29 | - name: Install packages 30 | run: npm ci 31 | 32 | - name: Run linter 33 | run: npm run lint:selene 34 | # skip luau-lsp as it cannot ignore errors in node_modules 35 | 36 | - name: Verify code style 37 | run: npm run style-check 38 | 39 | - name: Build assets 40 | run: npm run build-assets 41 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /site 2 | /assets 3 | 4 | /*.rbxl 5 | /*.rbxlx 6 | /*.rbxl.lock 7 | /*.rbxlx.lock 8 | /*.rbxm 9 | /*.rbxmx 10 | 11 | /build 12 | /serve 13 | /temp 14 | /NOTES.txt 15 | 16 | /node_modules 17 | 18 | .yarn 19 | 20 | /globalTypes.d.lua 21 | 22 | **/sourcemap.json 23 | **/darklua-sourcemap.json 24 | -------------------------------------------------------------------------------- /.luaurc: -------------------------------------------------------------------------------- 1 | { 2 | "languageMode": "strict", 3 | "lintErrors": true, 4 | "lint": { 5 | "*": true 6 | }, 7 | "aliases": { 8 | "pkg": "./node_modules/.luau-aliases" 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /.moonwave/custom.css: -------------------------------------------------------------------------------- 1 | td:nth-child(1) { 2 | background-color: rgb(46, 46, 46); 3 | } 4 | 5 | td:nth-child(2) { 6 | background-color: rgb(255, 255, 255) 7 | } 8 | 9 | td { 10 | width: auto; 11 | } 12 | 13 | td.min { 14 | width: 1%; 15 | white-space: nowrap; 16 | } -------------------------------------------------------------------------------- /.moonwave/static/components/background/dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sircfenner/StudioComponents/ceb9d451ec5f53adec6e539d4ea4a6a986882fd6/.moonwave/static/components/background/dark.png -------------------------------------------------------------------------------- /.moonwave/static/components/background/light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sircfenner/StudioComponents/ceb9d451ec5f53adec6e539d4ea4a6a986882fd6/.moonwave/static/components/background/light.png -------------------------------------------------------------------------------- /.moonwave/static/components/button/dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sircfenner/StudioComponents/ceb9d451ec5f53adec6e539d4ea4a6a986882fd6/.moonwave/static/components/button/dark.png -------------------------------------------------------------------------------- /.moonwave/static/components/button/light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sircfenner/StudioComponents/ceb9d451ec5f53adec6e539d4ea4a6a986882fd6/.moonwave/static/components/button/light.png -------------------------------------------------------------------------------- /.moonwave/static/components/checkbox/dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sircfenner/StudioComponents/ceb9d451ec5f53adec6e539d4ea4a6a986882fd6/.moonwave/static/components/checkbox/dark.png -------------------------------------------------------------------------------- /.moonwave/static/components/checkbox/light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sircfenner/StudioComponents/ceb9d451ec5f53adec6e539d4ea4a6a986882fd6/.moonwave/static/components/checkbox/light.png -------------------------------------------------------------------------------- /.moonwave/static/components/colorpicker/dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sircfenner/StudioComponents/ceb9d451ec5f53adec6e539d4ea4a6a986882fd6/.moonwave/static/components/colorpicker/dark.png -------------------------------------------------------------------------------- /.moonwave/static/components/colorpicker/light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sircfenner/StudioComponents/ceb9d451ec5f53adec6e539d4ea4a6a986882fd6/.moonwave/static/components/colorpicker/light.png -------------------------------------------------------------------------------- /.moonwave/static/components/datepicker/dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sircfenner/StudioComponents/ceb9d451ec5f53adec6e539d4ea4a6a986882fd6/.moonwave/static/components/datepicker/dark.png -------------------------------------------------------------------------------- /.moonwave/static/components/datepicker/light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sircfenner/StudioComponents/ceb9d451ec5f53adec6e539d4ea4a6a986882fd6/.moonwave/static/components/datepicker/light.png -------------------------------------------------------------------------------- /.moonwave/static/components/dropdown/dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sircfenner/StudioComponents/ceb9d451ec5f53adec6e539d4ea4a6a986882fd6/.moonwave/static/components/dropdown/dark.png -------------------------------------------------------------------------------- /.moonwave/static/components/dropdown/light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sircfenner/StudioComponents/ceb9d451ec5f53adec6e539d4ea4a6a986882fd6/.moonwave/static/components/dropdown/light.png -------------------------------------------------------------------------------- /.moonwave/static/components/dropshadowframe/dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sircfenner/StudioComponents/ceb9d451ec5f53adec6e539d4ea4a6a986882fd6/.moonwave/static/components/dropshadowframe/dark.png -------------------------------------------------------------------------------- /.moonwave/static/components/dropshadowframe/light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sircfenner/StudioComponents/ceb9d451ec5f53adec6e539d4ea4a6a986882fd6/.moonwave/static/components/dropshadowframe/light.png -------------------------------------------------------------------------------- /.moonwave/static/components/label/dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sircfenner/StudioComponents/ceb9d451ec5f53adec6e539d4ea4a6a986882fd6/.moonwave/static/components/label/dark.png -------------------------------------------------------------------------------- /.moonwave/static/components/label/light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sircfenner/StudioComponents/ceb9d451ec5f53adec6e539d4ea4a6a986882fd6/.moonwave/static/components/label/light.png -------------------------------------------------------------------------------- /.moonwave/static/components/loadingdots/dark.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sircfenner/StudioComponents/ceb9d451ec5f53adec6e539d4ea4a6a986882fd6/.moonwave/static/components/loadingdots/dark.gif -------------------------------------------------------------------------------- /.moonwave/static/components/loadingdots/light.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sircfenner/StudioComponents/ceb9d451ec5f53adec6e539d4ea4a6a986882fd6/.moonwave/static/components/loadingdots/light.gif -------------------------------------------------------------------------------- /.moonwave/static/components/mainbutton/dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sircfenner/StudioComponents/ceb9d451ec5f53adec6e539d4ea4a6a986882fd6/.moonwave/static/components/mainbutton/dark.png -------------------------------------------------------------------------------- /.moonwave/static/components/mainbutton/light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sircfenner/StudioComponents/ceb9d451ec5f53adec6e539d4ea4a6a986882fd6/.moonwave/static/components/mainbutton/light.png -------------------------------------------------------------------------------- /.moonwave/static/components/numbersequencepicker/dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sircfenner/StudioComponents/ceb9d451ec5f53adec6e539d4ea4a6a986882fd6/.moonwave/static/components/numbersequencepicker/dark.png -------------------------------------------------------------------------------- /.moonwave/static/components/numbersequencepicker/light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sircfenner/StudioComponents/ceb9d451ec5f53adec6e539d4ea4a6a986882fd6/.moonwave/static/components/numbersequencepicker/light.png -------------------------------------------------------------------------------- /.moonwave/static/components/numericinput/dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sircfenner/StudioComponents/ceb9d451ec5f53adec6e539d4ea4a6a986882fd6/.moonwave/static/components/numericinput/dark.png -------------------------------------------------------------------------------- /.moonwave/static/components/numericinput/light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sircfenner/StudioComponents/ceb9d451ec5f53adec6e539d4ea4a6a986882fd6/.moonwave/static/components/numericinput/light.png -------------------------------------------------------------------------------- /.moonwave/static/components/progressbar/dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sircfenner/StudioComponents/ceb9d451ec5f53adec6e539d4ea4a6a986882fd6/.moonwave/static/components/progressbar/dark.png -------------------------------------------------------------------------------- /.moonwave/static/components/progressbar/light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sircfenner/StudioComponents/ceb9d451ec5f53adec6e539d4ea4a6a986882fd6/.moonwave/static/components/progressbar/light.png -------------------------------------------------------------------------------- /.moonwave/static/components/radiobutton/dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sircfenner/StudioComponents/ceb9d451ec5f53adec6e539d4ea4a6a986882fd6/.moonwave/static/components/radiobutton/dark.png -------------------------------------------------------------------------------- /.moonwave/static/components/radiobutton/light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sircfenner/StudioComponents/ceb9d451ec5f53adec6e539d4ea4a6a986882fd6/.moonwave/static/components/radiobutton/light.png -------------------------------------------------------------------------------- /.moonwave/static/components/scrollframe/dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sircfenner/StudioComponents/ceb9d451ec5f53adec6e539d4ea4a6a986882fd6/.moonwave/static/components/scrollframe/dark.png -------------------------------------------------------------------------------- /.moonwave/static/components/scrollframe/light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sircfenner/StudioComponents/ceb9d451ec5f53adec6e539d4ea4a6a986882fd6/.moonwave/static/components/scrollframe/light.png -------------------------------------------------------------------------------- /.moonwave/static/components/slider/dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sircfenner/StudioComponents/ceb9d451ec5f53adec6e539d4ea4a6a986882fd6/.moonwave/static/components/slider/dark.png -------------------------------------------------------------------------------- /.moonwave/static/components/slider/light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sircfenner/StudioComponents/ceb9d451ec5f53adec6e539d4ea4a6a986882fd6/.moonwave/static/components/slider/light.png -------------------------------------------------------------------------------- /.moonwave/static/components/splitter/dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sircfenner/StudioComponents/ceb9d451ec5f53adec6e539d4ea4a6a986882fd6/.moonwave/static/components/splitter/dark.png -------------------------------------------------------------------------------- /.moonwave/static/components/splitter/light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sircfenner/StudioComponents/ceb9d451ec5f53adec6e539d4ea4a6a986882fd6/.moonwave/static/components/splitter/light.png -------------------------------------------------------------------------------- /.moonwave/static/components/tabcontainer/dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sircfenner/StudioComponents/ceb9d451ec5f53adec6e539d4ea4a6a986882fd6/.moonwave/static/components/tabcontainer/dark.png -------------------------------------------------------------------------------- /.moonwave/static/components/tabcontainer/light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sircfenner/StudioComponents/ceb9d451ec5f53adec6e539d4ea4a6a986882fd6/.moonwave/static/components/tabcontainer/light.png -------------------------------------------------------------------------------- /.moonwave/static/components/textinput/dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sircfenner/StudioComponents/ceb9d451ec5f53adec6e539d4ea4a6a986882fd6/.moonwave/static/components/textinput/dark.png -------------------------------------------------------------------------------- /.moonwave/static/components/textinput/light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sircfenner/StudioComponents/ceb9d451ec5f53adec6e539d4ea4a6a986882fd6/.moonwave/static/components/textinput/light.png -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | /.* 2 | /scripts 3 | /assets 4 | /docs 5 | /site 6 | 7 | /build 8 | /serve 9 | /temp 10 | 11 | /*.json 12 | /*.json5 13 | /*.yml 14 | /*.toml 15 | /*.md 16 | /*.txt 17 | /*.tgz 18 | 19 | *.d.lua 20 | *.d.luau 21 | 22 | **/*.rbxl 23 | **/*.rbxlx 24 | **/*.rbxl.lock 25 | **/*.rbxlx.lock 26 | **/*.rbxm 27 | **/*.rbxmx 28 | -------------------------------------------------------------------------------- /.styluaignore: -------------------------------------------------------------------------------- 1 | /node_modules 2 | /temp 3 | /build 4 | /serve 5 | 6 | **/*.d.lua 7 | **/*.d.luau 8 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "johnnymorganz.luau-lsp", 4 | "johnnymorganz.stylua", 5 | "kampfkarren.selene-vscode" 6 | ] 7 | } -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "luau-lsp.completion.imports.requireStyle": "alwaysRelative", 3 | "luau-lsp.platform.type": "roblox", 4 | "luau-lsp.sourcemap.rojoProjectFile": "model.project.json", 5 | } -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## Unreleased 4 | - Added an optional `DisplayTitle` prop to `TabContainer` children tabs to allow displaying custom text on tabs 5 | 6 | ## 1.2.0 7 | 8 | - Added usePlugin hook 9 | - Added RectSize, RectOffset, and ResampleMode to icon props available in Button, MainButton, and Dropdown 10 | - Fixed NumberSequencePicker error when adding 21st keypoint ([#48](https://github.com/sircfenner/StudioComponents/issues/48)) 11 | - Bumped package and tool versions 12 | 13 | ## 1.1.0 14 | 15 | - Fixed image links in documentation 16 | - Added OnCompleted prop to Slider 17 | - Added component: DatePicker 18 | 19 | ## 1.0.0 20 | 21 | Migrated from [Roact](https://github.com/Roblox/roact) to [react-lua](https://github.com/jsdotlua/react-lua) 22 | and rewrote the library from the ground up. 23 | 24 | There are many API differences; consult the docs on this. Removal of some components was either due 25 | to no longer being in scope for this project or requiring an API redesign which didn't make it 26 | into v1.0.0. 27 | 28 | ### Added 29 | 30 | - Full type annotations 31 | - Components: DropShadowFrame, LoadingDots, NumberSequencePicker, NumericInput, ProgressBar 32 | - Hooks: useMouseIcon 33 | 34 | ### Removed 35 | 36 | - Components: BaseButton, Tooltip, VerticalCollapsibleSection, VerticalExpandingList, Widget, withTheme 37 | - Contexts: ThemeContext 38 | - Hooks: usePlugin 39 | 40 | ## 0.1.0 - 0.1.4 41 | 42 | Initial release through to the final Roact version. Added various components and changed APIs. 43 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2025 sircfenner 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # StudioComponents 2 | 3 | ## [Read the documentation here!](https://sircfenner.github.io/StudioComponents/) 4 | 5 | A collection of React implementations of Roblox Studio components such as Checkboxes, Buttons, and Dropdowns. This is intended for building plugins for Roblox Studio. 6 | 7 |

8 | 9 | 10 |

11 |

An example Dropdown

12 | 13 | This project is built for [react-lua](https://github.com/jsdotlua/react-lua), Roblox's translation 14 | of upstream ReactJS 17.x into Luau. 15 | 16 | ## Installation 17 | 18 | ### Wally 19 | 20 | Add `studiocomponents` to your `wally.toml`: 21 | 22 | ```toml 23 | studiocomponents = "sircfenner/studiocomponents@1.0.0" 24 | ``` 25 | 26 | ### NPM & yarn 27 | 28 | Add `studiocomponents` to your dependencies: 29 | 30 | ```bash 31 | npm install @sircfenner/studiocomponents 32 | ``` 33 | 34 | ```bash 35 | yarn add @sircfenner/studiocomponents 36 | ``` 37 | 38 | Run `npmluau`. 39 | 40 | ## License 41 | 42 | This project is available under the MIT license. See [LICENSE](LICENSE) for details. 43 | -------------------------------------------------------------------------------- /docs/getting-started.md: -------------------------------------------------------------------------------- 1 | --- 2 | sidebar_position: 2 3 | --- 4 | 5 | # Getting Started 6 | 7 | This project is built for react-lua, which can be installed either via NPM/yarn, wally, or a release. See the [repository](https://github.com/jsdotlua/react-lua) for more information. 8 | 9 | StudioComponents exposes a table of components, hooks, and a reference to the Constants file. Minimal example of using a component from StudioComponents: 10 | 11 | ```lua 12 | local React = require(Packages.React) 13 | local StudioComponents = require(Packages.StudioComponents) 14 | 15 | local function MyComponent() 16 | return React.createElement(StudioComponents.Label, { 17 | Text = "Hello, from StudioComponents!" 18 | }) 19 | end 20 | ``` 21 | 22 | ## Installation 23 | 24 | ### Wally 25 | 26 | Add `studiocomponents` to your `wally.toml`: 27 | 28 | ```toml 29 | studiocomponents = "sircfenner/studiocomponents@1.0.0" 30 | ``` 31 | 32 | ### NPM & yarn 33 | 34 | Add `studiocomponents` to your dependencies: 35 | 36 | ```bash 37 | npm install @sircfenner/studiocomponents 38 | ``` 39 | 40 | ```bash 41 | yarn add @sircfenner/studiocomponents 42 | ``` 43 | 44 | Run `npmluau`. 45 | -------------------------------------------------------------------------------- /docs/intro.md: -------------------------------------------------------------------------------- 1 | --- 2 | sidebar_position: 1 3 | --- 4 | 5 | # About 6 | 7 | This is a collection of React components for building Roblox Studio plugins. These include common user interface components found in Studio and are made to closely match the look and functionality of their built-in counterparts, including synchronizing with the user's selected theme. 8 | 9 | These components are built for [react-lua](https://github.com/jsdotlua/react-lua), Roblox's translation of ReactJS v17 into Luau. A prior version of this project (before v1.0.0) used [Roact](https://github.com/Roblox/roact) and had multiple API differences. For more information, see the [Changelog](../changelog). 10 | 11 | :::note 12 | These components are only suitable for use in plugins. This is because they rely on plugin- or Studio-only APIs. 13 | ::: 14 | 15 | ## Why recreate the Studio interface? 16 | 17 | Closely replicating the built-in user interface has two main advantages: 18 | 19 | 1. Roblox Studio users recognise these components and know how to use them. 20 | 2. Less adjustment required when switching between third-party and built-in interfaces. 21 | 22 | The design of some built-in user interface components has changed in the lifetime of this 23 | project. In some cases, these changes had negative implications for accessiblity or consistency so 24 | their previous versions are used here instead. 25 | 26 | ## Plugins created with StudioComponents 27 | 28 | With wider adoption, using these components to build a plugin will also align it with other third-party plugins in appearance, familiarity, and usability. 29 | 30 | Some plugins created with StudioComponents include: 31 | 32 | - [Archimedes 3](https://devforum.roblox.com/t/introducing-archimedes-3-a-building-plugin/1610366), a popular building plugin used to create smooth arcs 33 | - [Collision Groups Editor](https://github.com/sircfenner/CollisionGroupsEditor), an alternative to the built-in editor for Collision Groups 34 | - [Layers](https://github.com/call23re/Layers), a tool for working with logical sections of 3D models 35 | - [Benchmarker](https://devforum.roblox.com/t/benchmarker-plugin-compare-function-speeds-with-graphs-percentiles-and-more/829912), a performance benchmarking tool for Luau code 36 | - [LampLight](https://devforum.roblox.com/t/lamplight-global-illumination-for-roblox-new-v12/1837877), a tool for baking Global Illumination bounce lighting into scenes 37 | - [MeshVox](https://devforum.roblox.com/t/meshvox-v10-a-powerful-3d-smooth-terrain-importstamping-tool/2576245), a smooth terrain importing and stamping tool 38 | 39 | :::info 40 | Some of these plugins were built with the earlier Roact version (version 0.x, before react-lua was adopted) or the [Fusion port](https://github.com/mvyasu/PluginEssentials) of it. 41 | ::: 42 | 43 | ## Migrating from Roact StudioComponents 44 | 45 | Existing users of the Roact version looking to migrate their project to React and the current version of StudioComponents should: 46 | 47 | 1. Follow the react-lua [guide for migrating from Roact](https://jsdotlua.github.io/react-lua/migrating-from-legacy/minimum-requirements/) 48 | 2. Follow this project's [installation guide](./getting-started) 49 | 3. Address any [API differences](../changelog) between legacy StudioComponents and this version 50 | -------------------------------------------------------------------------------- /foreman.toml: -------------------------------------------------------------------------------- 1 | [tools] 2 | darklua = { github = "seaofvoices/darklua", version = "=0.15.0"} 3 | luau-lsp = { github = "JohnnyMorganz/luau-lsp", version = "=1.38.0"} 4 | rojo = { github = "rojo-rbx/rojo", version = "=7.4.4"} 5 | selene = { github = "Kampfkarren/selene", version = "=0.27.1"} 6 | stylua = { github = "JohnnyMorganz/StyLua", version = "=2.0.2"} 7 | wally = { github = "UpliftGames/wally", version = "=0.3.2" } 8 | -------------------------------------------------------------------------------- /model.project.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "studiocomponents", 3 | "tree": { 4 | "$path": "src", 5 | "node_modules": { 6 | "$path": "node_modules" 7 | } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /moonwave.toml: -------------------------------------------------------------------------------- 1 | title = "StudioComponents" 2 | gitRepoUrl = "https://github.com/sircfenner/StudioComponents" 3 | organizationName = "sircfenner" 4 | projectName = "StudioComponents" 5 | gitSourceBranch = "main" 6 | 7 | [docusaurus] 8 | tagline = "React components for building Roblox Studio plugins" 9 | 10 | [footer] 11 | copyright = "Copyright © 2025 sircfenner. Built with Moonwave and Docusaurus." 12 | 13 | [home] 14 | enabled = true 15 | includeReadme = false 16 | 17 | [[classOrder]] 18 | section = "General" 19 | collapsed = false 20 | classes = ["Constants", "CommonProps"] 21 | 22 | [[classOrder]] 23 | section = "Components" 24 | collapsed = false 25 | classes = ["Background", "Button", "Checkbox", "ColorPicker", "DatePicker", "Dropdown", "DropShadowFrame", "Label", "LoadingDots", "MainButton", "NumberSequencePicker", "NumericInput", "PluginProvider", "ProgressBar", "RadioButton", "ScrollFrame", "Slider", "Splitter", "TabContainer", "TextInput"] 26 | 27 | [[classOrder]] 28 | section = "Hooks" 29 | collapsed = false 30 | classes = ["useMouseIcon", "useTheme", "usePlugin"] -------------------------------------------------------------------------------- /package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@sircfenner/studiocomponents", 3 | "version": "1.2.0", 4 | "lockfileVersion": 3, 5 | "requires": true, 6 | "packages": { 7 | "": { 8 | "name": "@sircfenner/studiocomponents", 9 | "version": "1.2.0", 10 | "license": "MIT", 11 | "dependencies": { 12 | "@jsdotlua/react": "^17.2.1", 13 | "@jsdotlua/react-roblox": "^17.2.1" 14 | }, 15 | "devDependencies": { 16 | "npmluau": "^0.1.1" 17 | } 18 | }, 19 | "node_modules/@jsdotlua/boolean": { 20 | "version": "1.2.6", 21 | "license": "MIT", 22 | "dependencies": { 23 | "@jsdotlua/number": "^1.2.6" 24 | } 25 | }, 26 | "node_modules/@jsdotlua/collections": { 27 | "version": "1.2.6", 28 | "license": "MIT", 29 | "dependencies": { 30 | "@jsdotlua/es7-types": "^1.2.6", 31 | "@jsdotlua/instance-of": "^1.2.6" 32 | } 33 | }, 34 | "node_modules/@jsdotlua/console": { 35 | "version": "1.2.6", 36 | "license": "MIT", 37 | "dependencies": { 38 | "@jsdotlua/collections": "^1.2.6" 39 | } 40 | }, 41 | "node_modules/@jsdotlua/es7-types": { 42 | "version": "1.2.6", 43 | "license": "MIT" 44 | }, 45 | "node_modules/@jsdotlua/instance-of": { 46 | "version": "1.2.6", 47 | "license": "MIT" 48 | }, 49 | "node_modules/@jsdotlua/luau-polyfill": { 50 | "version": "1.2.6", 51 | "license": "MIT", 52 | "dependencies": { 53 | "@jsdotlua/boolean": "^1.2.6", 54 | "@jsdotlua/collections": "^1.2.6", 55 | "@jsdotlua/console": "^1.2.6", 56 | "@jsdotlua/es7-types": "^1.2.6", 57 | "@jsdotlua/instance-of": "^1.2.6", 58 | "@jsdotlua/math": "^1.2.6", 59 | "@jsdotlua/number": "^1.2.6", 60 | "@jsdotlua/string": "^1.2.6", 61 | "@jsdotlua/timers": "^1.2.6", 62 | "symbol-luau": "^1.0.0" 63 | } 64 | }, 65 | "node_modules/@jsdotlua/math": { 66 | "version": "1.2.6", 67 | "license": "MIT" 68 | }, 69 | "node_modules/@jsdotlua/number": { 70 | "version": "1.2.6", 71 | "license": "MIT" 72 | }, 73 | "node_modules/@jsdotlua/promise": { 74 | "version": "3.5.0", 75 | "resolved": "https://registry.npmjs.org/@jsdotlua/promise/-/promise-3.5.0.tgz", 76 | "integrity": "sha512-uMwL18+wAhzJ65O9VYEsS2+ns5J/ABcY3oTASznlKdPU+syE1LK1hTy/LqEouHiDNZ1zAvmNUA3kY7oZ4/3gOw==" 77 | }, 78 | "node_modules/@jsdotlua/react": { 79 | "version": "17.2.1", 80 | "resolved": "https://registry.npmjs.org/@jsdotlua/react/-/react-17.2.1.tgz", 81 | "integrity": "sha512-hZ+z4DOKZlHr5UGgomJgD8kCiqHzR+fZyNcIw0RS9G+ADUapnyW+hCSnrxTHmPA9PFhJKvnn7a8boDyGIxd6/g==", 82 | "dependencies": { 83 | "@jsdotlua/luau-polyfill": "^1.2.6", 84 | "@jsdotlua/shared": "^17.2.1" 85 | } 86 | }, 87 | "node_modules/@jsdotlua/react-reconciler": { 88 | "version": "17.2.1", 89 | "resolved": "https://registry.npmjs.org/@jsdotlua/react-reconciler/-/react-reconciler-17.2.1.tgz", 90 | "integrity": "sha512-wtSXXM5Dl7YVXc8C8N50nF+P2iEM6uqWiQDpUevYgyz4G9bJomI12naWfDs5LGhtlX/Pnigxa3GiyFjnTHsCtQ==", 91 | "dependencies": { 92 | "@jsdotlua/luau-polyfill": "^1.2.6", 93 | "@jsdotlua/promise": "^3.5.0", 94 | "@jsdotlua/react": "^17.2.1", 95 | "@jsdotlua/scheduler": "^17.2.1", 96 | "@jsdotlua/shared": "^17.2.1" 97 | } 98 | }, 99 | "node_modules/@jsdotlua/react-roblox": { 100 | "version": "17.2.1", 101 | "resolved": "https://registry.npmjs.org/@jsdotlua/react-roblox/-/react-roblox-17.2.1.tgz", 102 | "integrity": "sha512-C2Q6UvVvyUwlNQ2grAXx/Ou5z2zlFHB8ROUJBDZ+pLAYK43tlYa+EhLGhn7J2CLP6YtlPaXA3qaMgzz9oDyYPg==", 103 | "dependencies": { 104 | "@jsdotlua/luau-polyfill": "^1.2.6", 105 | "@jsdotlua/react": "^17.2.1", 106 | "@jsdotlua/react-reconciler": "^17.2.1", 107 | "@jsdotlua/scheduler": "^17.2.1", 108 | "@jsdotlua/shared": "^17.2.1" 109 | } 110 | }, 111 | "node_modules/@jsdotlua/scheduler": { 112 | "version": "17.2.1", 113 | "resolved": "https://registry.npmjs.org/@jsdotlua/scheduler/-/scheduler-17.2.1.tgz", 114 | "integrity": "sha512-hTnoLYG899h3uVNPakYT6l7zCG8es+qgzxEYniaZesSA9UG+2VbKUvobXvbaargeKmr6Unm3lAryEvT+vZ5gKQ==", 115 | "dependencies": { 116 | "@jsdotlua/luau-polyfill": "^1.2.6", 117 | "@jsdotlua/shared": "^17.2.1" 118 | } 119 | }, 120 | "node_modules/@jsdotlua/shared": { 121 | "version": "17.2.1", 122 | "resolved": "https://registry.npmjs.org/@jsdotlua/shared/-/shared-17.2.1.tgz", 123 | "integrity": "sha512-Ot9X5Es+5ihpDWJi58K+KS09NEvequK4bJ0i8QkniYw8dDRFyBwRUPK5afy2YZyEe6i20ow7fDip4jb0HdYjcg==", 124 | "dependencies": { 125 | "@jsdotlua/luau-polyfill": "^1.2.6" 126 | } 127 | }, 128 | "node_modules/@jsdotlua/string": { 129 | "version": "1.2.6", 130 | "license": "MIT", 131 | "dependencies": { 132 | "@jsdotlua/es7-types": "^1.2.6", 133 | "@jsdotlua/number": "^1.2.6" 134 | } 135 | }, 136 | "node_modules/@jsdotlua/timers": { 137 | "version": "1.2.6", 138 | "license": "MIT", 139 | "dependencies": { 140 | "@jsdotlua/collections": "^1.2.6" 141 | } 142 | }, 143 | "node_modules/commander": { 144 | "version": "11.1.0", 145 | "dev": true, 146 | "license": "MIT", 147 | "engines": { 148 | "node": ">=16" 149 | } 150 | }, 151 | "node_modules/npmluau": { 152 | "version": "0.1.1", 153 | "dev": true, 154 | "license": "MIT", 155 | "dependencies": { 156 | "commander": "^11.0.0", 157 | "walkdir": "^0.4.1" 158 | }, 159 | "bin": { 160 | "npmluau": "main.js" 161 | } 162 | }, 163 | "node_modules/symbol-luau": { 164 | "version": "1.0.1", 165 | "license": "MIT" 166 | }, 167 | "node_modules/walkdir": { 168 | "version": "0.4.1", 169 | "dev": true, 170 | "license": "MIT", 171 | "engines": { 172 | "node": ">=6.0.0" 173 | } 174 | } 175 | } 176 | } 177 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@sircfenner/studiocomponents", 3 | "version": "1.2.0", 4 | "description": "React components for building Roblox Studio plugins", 5 | "license": "MIT", 6 | "author": "sircfenner ", 7 | "homepage": "https://github.com/sircfenner/studiocomponents#readme", 8 | "repository": { 9 | "type": "git", 10 | "url": "git+https://github.com/sircfenner/studiocomponents.git" 11 | }, 12 | "main": "src/init.luau", 13 | "scripts": { 14 | "build-assets": "sh ./scripts/build-assets.sh", 15 | "serve": "sh ./scripts/serve.sh", 16 | "clean": "rm -rf node_modules build serve temp darklua-sourcemap.json", 17 | "format": "stylua .", 18 | "lint": "sh ./scripts/analyze.sh && selene src", 19 | "lint:luau": "sh ./scripts/analyze.sh", 20 | "lint:selene": "selene src", 21 | "prepare": "npmluau", 22 | "style-check": "stylua . --check", 23 | "verify-pack": "npm pack --dry-run" 24 | }, 25 | "dependencies": { 26 | "@jsdotlua/react": "^17.2.1", 27 | "@jsdotlua/react-roblox": "^17.2.1" 28 | }, 29 | "devDependencies": { 30 | "npmluau": "^0.1.1" 31 | }, 32 | "keywords": [ 33 | "luau" 34 | ] 35 | } 36 | -------------------------------------------------------------------------------- /scripts/analyze.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -e 4 | 5 | TYPES_FILE=globalTypes.d.lua 6 | 7 | if [ ! -f "$TYPES_FILE" ]; then 8 | curl https://raw.githubusercontent.com/JohnnyMorganz/luau-lsp/main/scripts/globalTypes.d.lua > $TYPES_FILE 9 | fi 10 | 11 | luau-lsp analyze --base-luaurc=.luaurc --definitions=$TYPES_FILE src 12 | -------------------------------------------------------------------------------- /scripts/build-assets.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -e 4 | 5 | scripts/build-roblox-model.sh .darklua.json build/studiocomponents.rbxm 6 | scripts/build-wally-package.sh 7 | -------------------------------------------------------------------------------- /scripts/build-roblox-model.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -e 4 | 5 | DARKLUA_CONFIG=$1 6 | BUILD_OUTPUT=$2 7 | SOURCEMAP=darklua-sourcemap.json 8 | TEMP_DIR=temp 9 | 10 | scripts/install-deps.sh 11 | 12 | rm -rf $TEMP_DIR 13 | mkdir -p $TEMP_DIR 14 | 15 | cp -r src/ $TEMP_DIR/ 16 | cp -rL node_modules/ $TEMP_DIR/ 17 | 18 | cp "$DARKLUA_CONFIG" "$TEMP_DIR/$DARKLUA_CONFIG" 19 | rojo sourcemap model.project.json -o $TEMP_DIR/$SOURCEMAP 20 | 21 | cd $TEMP_DIR 22 | 23 | darklua process --config "$DARKLUA_CONFIG" src src 24 | darklua process --config "$DARKLUA_CONFIG" node_modules node_modules 25 | 26 | cd .. 27 | 28 | cp model.project.json $TEMP_DIR/ 29 | 30 | rm -f "$BUILD_OUTPUT" 31 | mkdir -p $(dirname "$BUILD_OUTPUT") 32 | 33 | rojo build $TEMP_DIR/model.project.json -o "$BUILD_OUTPUT" 34 | 35 | rm -rf $TEMP_DIR -------------------------------------------------------------------------------- /scripts/build-wally-package.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -e 4 | 5 | TEMP_DIR=temp 6 | WALLY_PACKAGE=build/wally 7 | 8 | scripts/install-deps.sh 9 | 10 | rm -rf $TEMP_DIR 11 | mkdir -p $TEMP_DIR 12 | 13 | cp -r src $TEMP_DIR/src 14 | rm -rf $WALLY_PACKAGE 15 | 16 | mkdir -p $WALLY_PACKAGE 17 | cp LICENSE $WALLY_PACKAGE/LICENSE 18 | 19 | node ./scripts/npm-to-wally.js package.json $WALLY_PACKAGE/wally.toml $WALLY_PACKAGE/default.project.json $TEMP_DIR/wally-package.project.json 20 | 21 | cp .darklua-wally.json $TEMP_DIR 22 | cp -r node_modules/.luau-aliases/* $TEMP_DIR 23 | 24 | rojo sourcemap $TEMP_DIR/wally-package.project.json --output $TEMP_DIR/sourcemap.json 25 | 26 | darklua process --config $TEMP_DIR/.darklua-wally.json $TEMP_DIR/src $WALLY_PACKAGE/src 27 | 28 | rm -rf $TEMP_DIR 29 | 30 | wally package --project-path $WALLY_PACKAGE --list -------------------------------------------------------------------------------- /scripts/install-deps.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -e 4 | 5 | if [ ! -d node_modules ]; then 6 | npm install 7 | fi 8 | 9 | if [ ! -d node_modules/.luau-aliases ]; then 10 | npm run prepare 11 | fi -------------------------------------------------------------------------------- /scripts/npm-to-wally.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | /* 4 | adapted from: https://github.com/jsdotlua/dom-testing-library-lua/blob/main/scripts/npm-to-wally.js 5 | 6 | changes: 7 | - remove workspaces logic not required by this repository 8 | - mirror description field from package.json to wally.toml 9 | 10 | */ 11 | 12 | const { Command } = require("commander"); 13 | 14 | const fs = require("fs").promises; 15 | const path = require("path"); 16 | const process = require("process"); 17 | 18 | const extractPackageNameWhenScoped = (packageName) => 19 | packageName.startsWith("@") 20 | ? packageName.substring(packageName.indexOf("/") + 1) 21 | : packageName; 22 | 23 | const readPackageConfig = async (packagePath) => { 24 | const packageContent = await fs.readFile(packagePath).catch((err) => { 25 | console.error( 26 | `unable to read package.json at '${packagePath}': ${err}` 27 | ); 28 | return null; 29 | }); 30 | 31 | if (packageContent !== null) { 32 | try { 33 | const packageData = JSON.parse(packageContent); 34 | return packageData; 35 | } catch (error) { 36 | console.error( 37 | `unable to parse package.json at '${packagePath}': ${err}` 38 | ); 39 | } 40 | } 41 | 42 | return null; 43 | }; 44 | 45 | const main = async ( 46 | packageJsonPath, 47 | wallyOutputPath, 48 | wallyRojoConfigPath, 49 | rojoConfigPath 50 | ) => { 51 | const packageData = await readPackageConfig(packageJsonPath); 52 | 53 | const { 54 | name: scopedName, 55 | version, 56 | license, 57 | dependencies = [], 58 | description, 59 | } = packageData; 60 | 61 | const tomlLines = ["[package]", `name = "${scopedName.substring(1)}"`]; 62 | 63 | if (description) { 64 | tomlLines.push(`description = "${description}"`); 65 | } 66 | 67 | tomlLines.push( 68 | `version = "${version}"`, 69 | 'registry = "https://github.com/UpliftGames/wally-index"', 70 | 'realm = "shared"', 71 | `license = "${license}"`, 72 | "", 73 | "[dependencies]" 74 | ); 75 | 76 | const rojoConfig = { 77 | name: "WallyPackage", 78 | tree: { 79 | $className: "Folder", 80 | Package: { 81 | $path: "src", 82 | }, 83 | }, 84 | }; 85 | 86 | for (const [dependencyName, specifiedVersion] of Object.entries( 87 | dependencies 88 | )) { 89 | const name = extractPackageNameWhenScoped(dependencyName); 90 | rojoConfig.tree[name] = { 91 | $path: `${dependencyName}.luau`, 92 | }; 93 | 94 | const wallyPackageName = name.indexOf("-") !== -1 ? `"${name}"` : name; 95 | if (specifiedVersion == "workspace:^") { 96 | error("workspace version not supported"); 97 | } else { 98 | tomlLines.push( 99 | `${wallyPackageName} = "jsdotlua/${name}@${specifiedVersion}"` 100 | ); 101 | } 102 | } 103 | 104 | tomlLines.push(""); 105 | 106 | const wallyRojoConfig = { 107 | name: scopedName.substring(scopedName.indexOf("/") + 1), 108 | tree: { 109 | $path: "src", 110 | }, 111 | }; 112 | 113 | await Promise.all([ 114 | fs.writeFile(wallyOutputPath, tomlLines.join("\n")).catch((err) => { 115 | console.error( 116 | `unable to write wally config at '${wallyOutputPath}': ${err}` 117 | ); 118 | }), 119 | fs 120 | .writeFile(rojoConfigPath, JSON.stringify(rojoConfig, null, 2)) 121 | .catch((err) => { 122 | console.error( 123 | `unable to write rojo config at '${rojoConfigPath}': ${err}` 124 | ); 125 | }), 126 | fs 127 | .writeFile( 128 | wallyRojoConfigPath, 129 | JSON.stringify(wallyRojoConfig, null, 2) 130 | ) 131 | .catch((err) => { 132 | console.error( 133 | `unable to write rojo config for wally at '${wallyRojoConfigPath}': ${err}` 134 | ); 135 | }), 136 | ]); 137 | }; 138 | 139 | const createCLI = () => { 140 | const program = new Command(); 141 | 142 | program 143 | .name("npm-to-wally") 144 | .description("a utility to convert npm packages to wally packages") 145 | .argument("") 146 | .argument("") 147 | .argument("") 148 | .argument("") 149 | .action(async (packageJson, wallyToml, wallyRojoConfig, rojoConfig) => { 150 | const cwd = process.cwd(); 151 | await main( 152 | path.join(cwd, packageJson), 153 | path.join(cwd, wallyToml), 154 | path.join(cwd, wallyRojoConfig), 155 | path.join(cwd, rojoConfig) 156 | ); 157 | }); 158 | 159 | return (args) => { 160 | program.parse(args); 161 | }; 162 | }; 163 | 164 | const run = createCLI(); 165 | 166 | run(process.argv); 167 | -------------------------------------------------------------------------------- /scripts/serve.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -e 4 | 5 | DARKLUA_CONFIG=.darklua.json 6 | SOURCEMAP=darklua-sourcemap.json 7 | SERVE_DIR=serve 8 | 9 | scripts/install-deps.sh 10 | 11 | rm -f $SOURCEMAP 12 | rm -rf $SERVE_DIR 13 | mkdir -p $SERVE_DIR 14 | 15 | cp model.project.json $SERVE_DIR/model.project.json 16 | cp serve.project.json $SERVE_DIR/serve.project.json 17 | cp -r src $SERVE_DIR/src 18 | cp -rL node_modules $SERVE_DIR/node_modules 19 | 20 | rojo sourcemap model.project.json -o $SOURCEMAP 21 | #darklua process --config $DARKLUA_CONFIG src $SERVE_DIR/src 22 | #darklua process --config $DARKLUA_CONFIG node_modules $SERVE_DIR/node_modules 23 | 24 | rojo sourcemap --watch model.project.json -o $SOURCEMAP & 25 | darklua process -w --config $DARKLUA_CONFIG src $SERVE_DIR/src & 26 | darklua process -w --config $DARKLUA_CONFIG node_modules $SERVE_DIR/node_modules & 27 | 28 | rojo serve $SERVE_DIR/serve.project.json 29 | -------------------------------------------------------------------------------- /selene.toml: -------------------------------------------------------------------------------- 1 | std = "selene_defs" 2 | 3 | -------------------------------------------------------------------------------- /selene_defs.yml: -------------------------------------------------------------------------------- 1 | base: roblox 2 | name: selene_defs 3 | globals: 4 | # override Roblox require style with string requires 5 | require: 6 | args: 7 | - type: string 8 | -------------------------------------------------------------------------------- /serve.project.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "studiocomponents-dev", 3 | "tree": { 4 | "$className": "DataModel", 5 | "ServerStorage": { 6 | "studiocomponents": { 7 | "$path": "model.project.json" 8 | } 9 | } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/CommonProps.luau: -------------------------------------------------------------------------------- 1 | --[=[ 2 | @class CommonProps 3 | @private 4 | 5 | The props listed here are accepted by every component except where explicitly noted. 6 | These props are accepted in addition to the props specified by components on their API pages. 7 | 8 | :::info 9 | This file is not exported and serves only to host an internal type and documentation. 10 | ::: 11 | ]=] 12 | 13 | --[=[ 14 | @within CommonProps 15 | @interface CommonProps 16 | 17 | @field Disabled boolean? 18 | @field AnchorPoint Vector2? 19 | @field Position UDim2? 20 | @field Size UDim2? 21 | @field LayoutOrder number? 22 | @field ZIndex number? 23 | ]=] 24 | 25 | export type T = { 26 | Disabled: boolean?, 27 | AnchorPoint: Vector2?, 28 | Position: UDim2?, 29 | Size: UDim2?, 30 | LayoutOrder: number?, 31 | ZIndex: number?, 32 | } 33 | 34 | return {} 35 | -------------------------------------------------------------------------------- /src/Components/Background.luau: -------------------------------------------------------------------------------- 1 | --[=[ 2 | @class Background 3 | 4 | A borderless frame matching the default background color of Studio widgets. 5 | 6 | | Dark | Light | 7 | | - | - | 8 | | ![Dark](/StudioComponents/components/background/dark.png) | ![Light](/StudioComponents/components/background/light.png) | 9 | 10 | Any children passed will be parented to the frame, which makes it suitable for use as, 11 | for example, the root component in a plugin Widget. For example: 12 | 13 | ```lua 14 | local function MyComponent() 15 | return React.createElement(StudioComponents.Background, {}, { 16 | MyChild = React.createElement(...), 17 | }) 18 | end 19 | ``` 20 | ]=] 21 | 22 | local React = require("@pkg/@jsdotlua/react") 23 | 24 | local CommonProps = require("../CommonProps") 25 | local useTheme = require("../Hooks/useTheme") 26 | 27 | --[=[ 28 | @within Background 29 | @interface Props 30 | @tag Component Props 31 | 32 | @field ... CommonProps 33 | @field children React.ReactNode 34 | ]=] 35 | 36 | type BackgroundProps = CommonProps.T & { 37 | children: React.ReactNode?, 38 | } 39 | 40 | local function Background(props: BackgroundProps) 41 | local theme = useTheme() 42 | return React.createElement("Frame", { 43 | BackgroundColor3 = theme:GetColor(Enum.StudioStyleGuideColor.MainBackground), 44 | BorderSizePixel = 0, 45 | AnchorPoint = props.AnchorPoint, 46 | Position = props.Position, 47 | Size = props.Size or UDim2.fromScale(1, 1), 48 | LayoutOrder = props.LayoutOrder, 49 | ZIndex = props.ZIndex, 50 | }, props.children) 51 | end 52 | 53 | return Background 54 | -------------------------------------------------------------------------------- /src/Components/Button.luau: -------------------------------------------------------------------------------- 1 | --[=[ 2 | @class Button 3 | A basic button that supports text, an icon, or both. This should be used as a standalone button 4 | or as a secondary button alongside a [MainButton] for the primary action in a group of options. 5 | 6 | | Dark | Light | 7 | | - | - | 8 | | ![Dark](/StudioComponents/components/button/dark.png) | ![Light](/StudioComponents/components/button/light.png) | 9 | 10 | The `OnActivated` prop should be a callback which is run when the button is clicked. 11 | For example: 12 | 13 | ```lua 14 | local function MyComponent() 15 | return React.createElement(StudioComponents.Button, { 16 | Text = "Click Me", 17 | OnActivated = function() 18 | print("Button clicked!") 19 | end 20 | }) 21 | end 22 | ``` 23 | 24 | The default size of buttons can be found in [Constants.DefaultButtonHeight]. To override this, 25 | there are two main options, which may be combined: 26 | 1. Pass a `Size` prop. 27 | 2. Pass an `AutomaticSize` prop. 28 | 29 | AutomaticSize is a simpler version of Roblox's built-in AutomaticSize system. Passing a value of 30 | `Enum.AutomaticSize.X` will override the button's width to fit the text and/or icon. Passing a 31 | value of `Enum.AutomaticSize.Y` will do the same but with the button's height. Passing 32 | `Enum.AutomaticSize.XY` will override both axes. 33 | ]=] 34 | 35 | local React = require("@pkg/@jsdotlua/react") 36 | 37 | local BaseButton = require("./Foundation/BaseButton") 38 | 39 | --[=[ 40 | @within Button 41 | @interface IconProps 42 | 43 | @field Image string 44 | @field Size Vector2 45 | @field Transparency number? 46 | @field Color Color3? 47 | @field UseThemeColor boolean? 48 | @field Alignment HorizontalAlignment? 49 | @field ResampleMode Enum.ResamplerMode? 50 | @field RectOffset Vector2? 51 | @field RectSize Vector2? 52 | 53 | The `Alignment` prop is used to configure which side of any text the icon 54 | appears on. Left-alignment is the default and center-alignment is not supported. 55 | 56 | When specifying icon color, at most one of `Color` and `UseThemeColor` should be specified. 57 | ]=] 58 | 59 | --[=[ 60 | @within Button 61 | @interface Props 62 | @tag Component Props 63 | 64 | @field ... CommonProps 65 | @field AutomaticSize AutomaticSize? 66 | @field OnActivated (() -> ())? 67 | @field Text string? 68 | @field Icon IconProps? 69 | ]=] 70 | 71 | local function Button(props: BaseButton.BaseButtonConsumerProps) 72 | local merged = table.clone(props) :: BaseButton.BaseButtonProps 73 | merged.BackgroundColorStyle = Enum.StudioStyleGuideColor.Button 74 | merged.BorderColorStyle = Enum.StudioStyleGuideColor.ButtonBorder 75 | merged.TextColorStyle = Enum.StudioStyleGuideColor.ButtonText 76 | 77 | return React.createElement(BaseButton, merged) 78 | end 79 | 80 | return Button 81 | -------------------------------------------------------------------------------- /src/Components/Checkbox.luau: -------------------------------------------------------------------------------- 1 | --[=[ 2 | @class Checkbox 3 | 4 | A box which can be checked or unchecked, usually used to toggle an option. Passing a value to 5 | the `Label` prop is the recommended way to indicate the purpose of a checkbox. 6 | 7 | | Dark | Light | 8 | | - | - | 9 | | ![Dark](/StudioComponents/components/checkbox/dark.png) | ![Light](/StudioComponents/components/checkbox/light.png) | 10 | 11 | As this is a controlled component, you should pass a value to the `Value` prop representing 12 | whether the box is checked, and a callback value to the `OnChanged` prop which gets run when 13 | the user interacts with the checkbox. For example: 14 | 15 | ```lua 16 | local function MyComponent() 17 | local selected, setSelected = React.useState(false) 18 | return React.createElement(StudioComponents.Checkbox, { 19 | Value = selected, 20 | OnChanged = setSelected, 21 | }) 22 | end 23 | ``` 24 | 25 | The default height of a checkbox, including its label, can be found in [Constants.DefaultToggleHeight]. 26 | The size of the whole checkbox can be overridden by passing a value to the `Size` prop. 27 | 28 | By default, the box and label are left-aligned within the parent frame. This can be overriden by 29 | passing an [Enum.HorizontalAlignment] value to the `ContentAlignment` prop. 30 | 31 | By default, the box is placed to the left of the label. This can be overriden by passing either 32 | `Enum.HorizontalAlignment.Left` or `Enum.HorizontalAlignment.Right` to the 33 | `ButtonAlignment` prop. 34 | 35 | Checkboxes can also represent 'indeterminate' values, which indicates that it is neither 36 | checked nor unchecked. This can be achieved by passing `nil` to the `Value` prop. 37 | This might be used when a checkbox represents the combined state of two different options, one of 38 | which has a value of `true` and the other `false`. 39 | 40 | :::info 41 | The built-in Studio checkboxes were changed during this project's lifetime to be smaller and 42 | have a lower contrast ratio, especially in Dark theme. This component retains the old design 43 | as it is more accessible. 44 | ::: 45 | ]=] 46 | 47 | local React = require("@pkg/@jsdotlua/react") 48 | 49 | local BaseLabelledToggle = require("./Foundation/BaseLabelledToggle") 50 | local useTheme = require("../Hooks/useTheme") 51 | 52 | local INDICATOR_IMAGE = "rbxassetid://14890059620" 53 | 54 | --[=[ 55 | @within Checkbox 56 | @interface Props 57 | @tag Component Props 58 | 59 | @field ... CommonProps 60 | @field Value boolean? 61 | @field OnChanged (() -> ())? 62 | @field Label string? 63 | @field ContentAlignment HorizontalAlignment? 64 | @field ButtonAlignment HorizontalAlignment? 65 | ]=] 66 | 67 | type CheckboxProps = BaseLabelledToggle.BaseLabelledToggleConsumerProps & { 68 | Value: boolean?, 69 | } 70 | 71 | local function Checkbox(props: CheckboxProps) 72 | local theme = useTheme() 73 | local mergedProps = table.clone(props) :: BaseLabelledToggle.BaseLabelledToggleProps 74 | 75 | function mergedProps.RenderButton(subProps: { Hovered: boolean }) 76 | local mainModifier = Enum.StudioStyleGuideModifier.Default 77 | if props.Disabled then 78 | mainModifier = Enum.StudioStyleGuideModifier.Disabled 79 | elseif subProps.Hovered then 80 | mainModifier = Enum.StudioStyleGuideModifier.Hover 81 | end 82 | 83 | local backModifier = Enum.StudioStyleGuideModifier.Default 84 | if props.Disabled then 85 | backModifier = Enum.StudioStyleGuideModifier.Disabled 86 | elseif props.Value == true then 87 | backModifier = Enum.StudioStyleGuideModifier.Selected 88 | elseif subProps.Hovered then 89 | backModifier = Enum.StudioStyleGuideModifier.Hover 90 | end 91 | 92 | local rectOffset = Vector2.new(0, 0) 93 | if props.Value == nil then -- indeterminate 94 | local background = theme:GetColor(Enum.StudioStyleGuideColor.MainBackground) 95 | local _, _, val = background:ToHSV() 96 | rectOffset = if val < 0.5 then Vector2.new(14, 0) else Vector2.new(28, 0) 97 | end 98 | 99 | local indicatorColor = theme:GetColor(Enum.StudioStyleGuideColor.CheckedFieldIndicator, mainModifier) 100 | local indicatorTransparency = 0 101 | if props.Value == nil then 102 | indicatorColor = Color3.fromRGB(255, 255, 255) 103 | if props.Disabled then 104 | indicatorTransparency = 0.5 105 | end 106 | end 107 | 108 | return React.createElement("Frame", { 109 | BackgroundColor3 = theme:GetColor(Enum.StudioStyleGuideColor.CheckedFieldBackground, backModifier), 110 | BorderColor3 = theme:GetColor(Enum.StudioStyleGuideColor.CheckedFieldBorder, mainModifier), 111 | BorderMode = Enum.BorderMode.Inset, 112 | Size = UDim2.fromScale(1, 1), 113 | }, { 114 | Indicator = props.Value ~= false and React.createElement("ImageLabel", { 115 | BackgroundTransparency = 1, 116 | Size = UDim2.fromOffset(14, 14), 117 | Image = INDICATOR_IMAGE, 118 | ImageColor3 = indicatorColor, 119 | ImageRectOffset = rectOffset, 120 | ImageRectSize = Vector2.new(14, 14), 121 | ImageTransparency = indicatorTransparency, 122 | }), 123 | }) 124 | end 125 | 126 | return React.createElement(BaseLabelledToggle, mergedProps) 127 | end 128 | 129 | return Checkbox 130 | -------------------------------------------------------------------------------- /src/Components/DropShadowFrame.luau: -------------------------------------------------------------------------------- 1 | --[=[ 2 | @class DropShadowFrame 3 | 4 | A container frame equivalent in appearance to a [Background] with a 5 | drop shadow in the lower right sides and corner. 6 | This matches the appearance of some built-in Roblox Studio elements such as tooltips. 7 | It is useful for providing contrast against a background. 8 | 9 | | Dark | Light | 10 | | - | - | 11 | | ![Dark](/StudioComponents/components/dropshadowframe/dark.png) | ![Light](/StudioComponents/components/dropshadowframe/light.png) | 12 | 13 | Any children passed will be parented to the container frame. For example: 14 | 15 | ```lua 16 | local function MyComponent() 17 | return React.createElement(StudioComponents.DropShadowFrame, {}, { 18 | MyLabel = React.createElement(StudioComponents.Label, ...), 19 | MyCheckbox = React.createElement(StudioComponents.Checkbox, ...), 20 | }) 21 | end 22 | ``` 23 | ]=] 24 | 25 | local React = require("@pkg/@jsdotlua/react") 26 | 27 | local CommonProps = require("../CommonProps") 28 | local useTheme = require("../Hooks/useTheme") 29 | 30 | local shadowData = { 31 | { 32 | Position = UDim2.fromOffset(4, 4), 33 | Size = UDim2.new(1, 1, 1, 1), 34 | Radius = 5, 35 | Transparency = 0.96, 36 | }, 37 | { 38 | Position = UDim2.fromOffset(1, 1), 39 | Size = UDim2.new(1, -2, 1, -2), 40 | Radius = 4, 41 | Transparency = 0.88, 42 | }, 43 | { 44 | Position = UDim2.fromOffset(1, 1), 45 | Size = UDim2.new(1, -2, 1, -2), 46 | Radius = 3, 47 | Transparency = 0.80, 48 | }, 49 | { 50 | Position = UDim2.fromOffset(1, 1), 51 | Size = UDim2.new(1, -2, 1, -2), 52 | Radius = 2, 53 | Transparency = 0.77, 54 | }, 55 | } 56 | 57 | --[=[ 58 | @within DropShadowFrame 59 | @interface Props 60 | @tag Component Props 61 | 62 | @field ... CommonProps 63 | @field children React.ReactNode 64 | ]=] 65 | 66 | type DropShadowFrameProps = CommonProps.T & { 67 | children: React.ReactNode, 68 | } 69 | 70 | local function DropShadowFrame(props: DropShadowFrameProps) 71 | local theme = useTheme() 72 | 73 | local shadow 74 | for i = #shadowData, 1, -1 do 75 | local data = shadowData[i] 76 | shadow = React.createElement("Frame", { 77 | Position = data.Position, 78 | Size = data.Size, 79 | BackgroundColor3 = theme:GetColor(Enum.StudioStyleGuideColor.DropShadow), 80 | BackgroundTransparency = data.Transparency, 81 | BorderSizePixel = 0, 82 | ZIndex = 0, 83 | }, { 84 | Corner = React.createElement("UICorner", { 85 | CornerRadius = UDim.new(0, data.Radius), 86 | }), 87 | }, shadow) 88 | end 89 | 90 | return React.createElement("Frame", { 91 | AnchorPoint = props.AnchorPoint, 92 | Position = props.Position, 93 | Size = props.Size or UDim2.fromScale(1, 1), 94 | LayoutOrder = props.LayoutOrder, 95 | ZIndex = props.ZIndex, 96 | BackgroundTransparency = 1, 97 | }, { 98 | Shadow = not props.Disabled and shadow, 99 | Content = React.createElement("Frame", { 100 | Size = UDim2.fromScale(1, 1), 101 | BackgroundColor3 = theme:GetColor(Enum.StudioStyleGuideColor.MainBackground), 102 | BorderColor3 = theme:GetColor(Enum.StudioStyleGuideColor.Border), 103 | ZIndex = 1, 104 | }, props.children), 105 | }) 106 | end 107 | 108 | return DropShadowFrame 109 | -------------------------------------------------------------------------------- /src/Components/Dropdown/ClearButton.luau: -------------------------------------------------------------------------------- 1 | local React = require("@pkg/@jsdotlua/react") 2 | 3 | local useTheme = require("../../Hooks/useTheme") 4 | 5 | type ClearButtonProps = { 6 | Size: UDim2, 7 | Position: UDim2, 8 | AnchorPoint: Vector2, 9 | OnActivated: () -> (), 10 | } 11 | 12 | local function ClearButton(props: ClearButtonProps) 13 | local theme = useTheme() 14 | local hovered, setHovered = React.useState(false) 15 | 16 | return React.createElement("TextButton", { 17 | AnchorPoint = props.AnchorPoint, 18 | Position = props.Position, 19 | Size = props.Size, 20 | BackgroundTransparency = 1, 21 | ZIndex = 2, 22 | Text = "", 23 | [React.Event.InputBegan] = function(_, input: InputObject) 24 | if input.UserInputType == Enum.UserInputType.MouseMovement then 25 | setHovered(true) 26 | end 27 | end, 28 | [React.Event.InputEnded] = function(_, input: InputObject) 29 | if input.UserInputType == Enum.UserInputType.MouseMovement then 30 | setHovered(false) 31 | end 32 | end, 33 | [React.Event.Activated] = function() 34 | props.OnActivated() 35 | end, 36 | }, { 37 | Icon = React.createElement("ImageLabel", { 38 | AnchorPoint = Vector2.new(0.5, 0.5), 39 | Position = UDim2.fromScale(0.5, 0.5), 40 | Size = UDim2.fromOffset(10, 10), 41 | Image = "rbxassetid://16969027907", 42 | ImageColor3 = theme:GetColor(Enum.StudioStyleGuideColor.SubText), 43 | ImageTransparency = if hovered then 0 else 0.6, 44 | BackgroundTransparency = 1, 45 | }), 46 | }) 47 | end 48 | 49 | return ClearButton 50 | -------------------------------------------------------------------------------- /src/Components/Dropdown/DropdownItem.luau: -------------------------------------------------------------------------------- 1 | local React = require("@pkg/@jsdotlua/react") 2 | 3 | local Constants = require("../../Constants") 4 | local useTheme = require("../../Hooks/useTheme") 5 | 6 | local BaseIcon = require("../Foundation/BaseIcon") 7 | 8 | local DropdownTypes = require("./Types") 9 | 10 | type DropdownItemProps = { 11 | Id: string, 12 | Text: string, 13 | Icon: DropdownTypes.DropdownItemIcon?, 14 | LayoutOrder: number, 15 | Height: number, 16 | TextInset: number, 17 | Selected: boolean, 18 | OnSelected: (item: string) -> (), 19 | } 20 | 21 | local function DropdownItem(props: DropdownItemProps) 22 | local theme = useTheme() 23 | local hovered, setHovered = React.useState(false) 24 | 25 | local modifier = Enum.StudioStyleGuideModifier.Default 26 | if props.Selected then 27 | modifier = Enum.StudioStyleGuideModifier.Selected 28 | elseif hovered then 29 | modifier = Enum.StudioStyleGuideModifier.Hover 30 | end 31 | 32 | local iconNode: React.Node? 33 | if props.Icon then 34 | local iconColor = Color3.fromRGB(255, 255, 255) 35 | if props.Icon.UseThemeColor then 36 | iconColor = theme:GetColor(Enum.StudioStyleGuideColor.MainText) 37 | elseif props.Icon.Color then 38 | iconColor = props.Icon.Color 39 | end 40 | 41 | local iconProps = (table.clone(props.Icon) :: any) :: BaseIcon.BaseIconProps 42 | iconProps.Color = iconColor 43 | iconProps.AnchorPoint = Vector2.new(0, 0.5) 44 | iconProps.Position = UDim2.fromScale(0, 0.5) 45 | iconProps.Size = UDim2.fromOffset(props.Icon.Size.X, props.Icon.Size.Y) 46 | iconProps.Disabled = nil 47 | iconProps.LayoutOrder = nil 48 | 49 | iconNode = React.createElement(BaseIcon, iconProps) 50 | end 51 | 52 | return React.createElement("Frame", { 53 | LayoutOrder = props.LayoutOrder, 54 | Size = UDim2.new(1, 0, 0, props.Height), 55 | BackgroundColor3 = theme:GetColor(Enum.StudioStyleGuideColor.Item, modifier), 56 | BorderSizePixel = 0, 57 | }, { 58 | Button = React.createElement("TextButton", { 59 | Position = UDim2.fromOffset(0, 1), 60 | Size = UDim2.new(1, 0, 1, -1), 61 | BackgroundTransparency = 1, 62 | AutoButtonColor = false, 63 | Text = "", 64 | [React.Event.InputBegan] = function(_, input) 65 | if input.UserInputType == Enum.UserInputType.MouseMovement then 66 | setHovered(true) 67 | end 68 | end, 69 | [React.Event.InputEnded] = function(_, input) 70 | if input.UserInputType == Enum.UserInputType.MouseMovement then 71 | setHovered(false) 72 | end 73 | end, 74 | [React.Event.Activated] = function() 75 | props.OnSelected(props.Id) 76 | end, 77 | }, { 78 | Padding = React.createElement("UIPadding", { 79 | PaddingLeft = UDim.new(0, props.TextInset), 80 | PaddingBottom = UDim.new(0, 2), 81 | }), 82 | Icon = iconNode, 83 | Label = React.createElement("TextLabel", { 84 | BackgroundTransparency = 1, 85 | AnchorPoint = Vector2.new(1, 0), 86 | Position = UDim2.fromScale(1, 0), 87 | Size = UDim2.new(1, if props.Icon then -props.Icon.Size.X - 4 else 0, 1, 0), 88 | Font = Constants.DefaultFont, 89 | TextSize = Constants.DefaultTextSize, 90 | TextColor3 = theme:GetColor(Enum.StudioStyleGuideColor.MainText, modifier), 91 | TextXAlignment = Enum.TextXAlignment.Left, 92 | TextTruncate = Enum.TextTruncate.AtEnd, 93 | Text = props.Text, 94 | }), 95 | }), 96 | }) 97 | end 98 | 99 | return DropdownItem 100 | -------------------------------------------------------------------------------- /src/Components/Dropdown/Types.luau: -------------------------------------------------------------------------------- 1 | --[=[ 2 | @within Dropdown 3 | @type DropdownItem string | DropdownItemDetail 4 | ]=] 5 | 6 | export type DropdownItem = string | DropdownItemDetail 7 | 8 | --[=[ 9 | @within Dropdown 10 | @interface DropdownItemDetail 11 | 12 | @field Id string 13 | @field Text string 14 | @field Icon DropdownItemIcon? 15 | ]=] 16 | 17 | export type DropdownItemDetail = { 18 | Id: string, 19 | Text: string, 20 | Icon: DropdownItemIcon?, 21 | } 22 | 23 | --[=[ 24 | @within Dropdown 25 | @interface DropdownItemIcon 26 | 27 | @field Image string 28 | @field Size Vector2 29 | @field Transparency number? 30 | @field Color Color3? 31 | @field UseThemeColor boolean? 32 | @field ResampleMode Enum.ResamplerMode? 33 | @field RectOffset Vector2? 34 | @field RectSize Vector2? 35 | ]=] 36 | 37 | export type DropdownItemIcon = { 38 | Image: string, 39 | Size: Vector2, 40 | Transparency: number?, 41 | Color: Color3?, 42 | ResampleMode: Enum.ResamplerMode?, 43 | RectOffset: Vector2?, 44 | RectSize: Vector2?, 45 | UseThemeColor: boolean?, 46 | } 47 | 48 | return {} 49 | -------------------------------------------------------------------------------- /src/Components/Foundation/BaseButton.luau: -------------------------------------------------------------------------------- 1 | local React = require("@pkg/@jsdotlua/react") 2 | 3 | local CommonProps = require("../../CommonProps") 4 | local Constants = require("../../Constants") 5 | 6 | local getTextSize = require("../../getTextSize") 7 | local useTheme = require("../../Hooks/useTheme") 8 | 9 | local BaseIcon = require("./BaseIcon") 10 | 11 | local PADDING_X = 8 12 | local PADDING_Y = 4 13 | local DEFAULT_HEIGHT = Constants.DefaultButtonHeight 14 | 15 | export type BaseButtonIconProps = { 16 | Image: string, 17 | Size: Vector2, 18 | Transparency: number?, 19 | Color: Color3?, 20 | ResampleMode: Enum.ResamplerMode?, 21 | RectOffset: Vector2?, 22 | RectSize: Vector2?, 23 | UseThemeColor: boolean?, 24 | Alignment: Enum.HorizontalAlignment?, 25 | } 26 | 27 | export type BaseButtonConsumerProps = CommonProps.T & { 28 | AutomaticSize: Enum.AutomaticSize?, 29 | OnActivated: (() -> ())?, 30 | Selected: boolean?, 31 | Text: string?, 32 | TextTransparency: number?, 33 | Icon: BaseButtonIconProps?, 34 | } 35 | 36 | export type BaseButtonProps = BaseButtonConsumerProps & { 37 | BackgroundColorStyle: Enum.StudioStyleGuideColor?, 38 | BorderColorStyle: Enum.StudioStyleGuideColor?, 39 | TextColorStyle: Enum.StudioStyleGuideColor?, 40 | } 41 | 42 | local function BaseButton(props: BaseButtonProps) 43 | local theme = useTheme() 44 | 45 | local textSize = if props.Text then getTextSize(props.Text) else Vector2.zero 46 | local iconSize = if props.Icon then props.Icon.Size else Vector2.zero 47 | 48 | local contentWidth = textSize.X + iconSize.X 49 | if props.Text and props.Icon then 50 | contentWidth += PADDING_X 51 | end 52 | 53 | local contentHeight = textSize.Y 54 | if props.Icon then 55 | contentHeight = math.max(contentHeight, iconSize.Y) 56 | end 57 | 58 | local hovered, setHovered = React.useState(false) 59 | local pressed, setPressed = React.useState(false) 60 | local modifier = Enum.StudioStyleGuideModifier.Default 61 | if props.Disabled then 62 | modifier = Enum.StudioStyleGuideModifier.Disabled 63 | elseif props.Selected then 64 | modifier = Enum.StudioStyleGuideModifier.Selected 65 | elseif pressed and hovered then 66 | modifier = Enum.StudioStyleGuideModifier.Pressed 67 | elseif hovered then 68 | modifier = Enum.StudioStyleGuideModifier.Hover 69 | end 70 | 71 | local backColorStyle = props.BackgroundColorStyle or Enum.StudioStyleGuideColor.Button 72 | local borderColorStyle = props.BorderColorStyle or Enum.StudioStyleGuideColor.ButtonBorder 73 | local textColorStyle = props.TextColorStyle or Enum.StudioStyleGuideColor.ButtonText 74 | 75 | local backColor = theme:GetColor(backColorStyle, modifier) 76 | local borderColor3 = theme:GetColor(borderColorStyle, modifier) 77 | local textColor = theme:GetColor(textColorStyle, modifier) 78 | 79 | local size = props.Size or UDim2.new(1, 0, 0, DEFAULT_HEIGHT) 80 | local autoSize = props.AutomaticSize 81 | if autoSize == Enum.AutomaticSize.X or autoSize == Enum.AutomaticSize.XY then 82 | size = UDim2.new(UDim.new(0, contentWidth + PADDING_X * 2), size.Height) 83 | end 84 | if autoSize == Enum.AutomaticSize.Y or autoSize == Enum.AutomaticSize.XY then 85 | size = UDim2.new(size.Width, UDim.new(0, math.max(DEFAULT_HEIGHT, contentHeight + PADDING_Y * 2))) 86 | end 87 | 88 | local iconNode: React.Node? 89 | if props.Icon then 90 | local iconProps = (table.clone(props.Icon) :: any) :: BaseIcon.BaseIconProps 91 | iconProps.Disabled = props.Disabled 92 | iconProps.Color = iconProps.Color or if props.Icon.UseThemeColor then textColor else nil 93 | iconProps.LayoutOrder = if props.Icon.Alignment == Enum.HorizontalAlignment.Right then 3 else 1 94 | iconProps.Size = UDim2.fromOffset(props.Icon.Size.X, props.Icon.Size.Y) 95 | 96 | iconNode = React.createElement(BaseIcon, iconProps) 97 | end 98 | 99 | return React.createElement("TextButton", { 100 | AutoButtonColor = false, 101 | AnchorPoint = props.AnchorPoint, 102 | Position = props.Position, 103 | Size = size, 104 | LayoutOrder = props.LayoutOrder, 105 | ZIndex = props.ZIndex, 106 | Text = "", 107 | BackgroundColor3 = backColor, 108 | BorderColor3 = borderColor3, 109 | [React.Event.InputBegan] = function(_, input) 110 | if input.UserInputType == Enum.UserInputType.MouseMovement then 111 | setHovered(true) 112 | elseif input.UserInputType == Enum.UserInputType.MouseButton1 then 113 | setPressed(true) 114 | end 115 | end, 116 | [React.Event.InputEnded] = function(_, input) 117 | if input.UserInputType == Enum.UserInputType.MouseMovement then 118 | setHovered(false) 119 | elseif input.UserInputType == Enum.UserInputType.MouseButton1 then 120 | setPressed(false) 121 | end 122 | end, 123 | [React.Event.Activated] = function() 124 | if not props.Disabled and props.OnActivated then 125 | props.OnActivated() 126 | end 127 | end, 128 | }, { 129 | Layout = React.createElement("UIListLayout", { 130 | Padding = UDim.new(0, PADDING_X), 131 | SortOrder = Enum.SortOrder.LayoutOrder, 132 | FillDirection = Enum.FillDirection.Horizontal, 133 | HorizontalAlignment = Enum.HorizontalAlignment.Center, 134 | VerticalAlignment = Enum.VerticalAlignment.Center, 135 | }), 136 | Icon = iconNode, 137 | Label = props.Text and React.createElement("TextLabel", { 138 | TextColor3 = textColor, 139 | Font = Constants.DefaultFont, 140 | TextSize = Constants.DefaultTextSize, 141 | Text = props.Text, 142 | TextTransparency = props.TextTransparency or 0, 143 | Size = UDim2.new(0, textSize.X, 1, 0), 144 | BackgroundTransparency = 1, 145 | LayoutOrder = 2, 146 | }), 147 | }) 148 | end 149 | 150 | return BaseButton 151 | -------------------------------------------------------------------------------- /src/Components/Foundation/BaseIcon.luau: -------------------------------------------------------------------------------- 1 | local React = require("@pkg/@jsdotlua/react") 2 | 3 | local CommonProps = require("../../CommonProps") 4 | 5 | export type BaseIconConsumerProps = CommonProps.T & { 6 | Image: string, 7 | Transparency: number?, 8 | Color: Color3?, 9 | ResampleMode: Enum.ResamplerMode?, 10 | RectOffset: Vector2?, 11 | RectSize: Vector2?, 12 | } 13 | 14 | export type BaseIconProps = BaseIconConsumerProps 15 | 16 | local function BaseIcon(props: BaseIconProps) 17 | return React.createElement("ImageLabel", { 18 | Size = props.Size, 19 | Position = props.Position, 20 | AnchorPoint = props.AnchorPoint, 21 | LayoutOrder = props.LayoutOrder, 22 | ZIndex = props.ZIndex, 23 | BackgroundTransparency = 1, 24 | Image = props.Image, 25 | ImageColor3 = props.Color, 26 | ImageTransparency = 1 - (1 - (props.Transparency or 0)) * (1 - if props.Disabled then 0.2 else 0), 27 | ImageRectOffset = props.RectOffset, 28 | ImageRectSize = props.RectSize, 29 | ResampleMode = props.ResampleMode, 30 | }) 31 | end 32 | 33 | return BaseIcon 34 | -------------------------------------------------------------------------------- /src/Components/Foundation/BaseLabelledToggle.luau: -------------------------------------------------------------------------------- 1 | local React = require("@pkg/@jsdotlua/react") 2 | 3 | local CommonProps = require("../../CommonProps") 4 | local Constants = require("../../Constants") 5 | 6 | local getTextSize = require("../../getTextSize") 7 | local useTheme = require("../../Hooks/useTheme") 8 | 9 | local DEFAULT_HEIGHT = Constants.DefaultToggleHeight 10 | local BOX_SIZE = 16 11 | local INNER_PADDING = 6 12 | 13 | export type BaseLabelledToggleConsumerProps = CommonProps.T & { 14 | ContentAlignment: Enum.HorizontalAlignment?, 15 | ButtonAlignment: Enum.HorizontalAlignment?, 16 | OnChanged: (() -> ())?, 17 | Label: string?, 18 | } 19 | 20 | export type BaseLabelledToggleProps = BaseLabelledToggleConsumerProps & { 21 | RenderButton: React.FC<{ Hovered: boolean }>?, 22 | } 23 | 24 | local function BaseLabelledToggle(props: BaseLabelledToggleProps) 25 | local theme = useTheme() 26 | local hovered, setHovered = React.useState(false) 27 | 28 | local mainModifier = Enum.StudioStyleGuideModifier.Default 29 | if props.Disabled then 30 | mainModifier = Enum.StudioStyleGuideModifier.Disabled 31 | elseif hovered then 32 | mainModifier = Enum.StudioStyleGuideModifier.Hover 33 | end 34 | 35 | local contentAlignment = props.ContentAlignment or Enum.HorizontalAlignment.Left 36 | local buttonAlignment = props.ButtonAlignment or Enum.HorizontalAlignment.Left 37 | 38 | local textWidth = if props.Label then getTextSize(props.Label).X else 0 39 | local textAlignment = Enum.TextXAlignment.Left 40 | local buttonOrder = 1 41 | local labelOrder = 2 42 | if buttonAlignment == Enum.HorizontalAlignment.Right then 43 | buttonOrder = 2 44 | labelOrder = 1 45 | textAlignment = Enum.TextXAlignment.Right 46 | end 47 | 48 | local content = nil 49 | if props.RenderButton then 50 | content = React.createElement(props.RenderButton, { 51 | Hovered = hovered, 52 | }) 53 | end 54 | 55 | return React.createElement("TextButton", { 56 | Size = props.Size or UDim2.new(1, 0, 0, DEFAULT_HEIGHT), 57 | Position = props.Position, 58 | AnchorPoint = props.AnchorPoint, 59 | LayoutOrder = props.LayoutOrder, 60 | ZIndex = props.ZIndex, 61 | BackgroundTransparency = 1, 62 | Text = "", 63 | [React.Event.InputBegan] = function(_, input) 64 | if input.UserInputType == Enum.UserInputType.MouseMovement then 65 | setHovered(true) 66 | end 67 | end, 68 | [React.Event.InputEnded] = function(_, input) 69 | if input.UserInputType == Enum.UserInputType.MouseMovement then 70 | setHovered(false) 71 | end 72 | end, 73 | [React.Event.Activated] = function() 74 | if not props.Disabled and props.OnChanged then 75 | props.OnChanged() 76 | end 77 | end, 78 | }, { 79 | Layout = React.createElement("UIListLayout", { 80 | HorizontalAlignment = contentAlignment, 81 | VerticalAlignment = Enum.VerticalAlignment.Center, 82 | FillDirection = Enum.FillDirection.Horizontal, 83 | SortOrder = Enum.SortOrder.LayoutOrder, 84 | Padding = UDim.new(0, INNER_PADDING), 85 | }), 86 | Button = React.createElement("Frame", { 87 | BackgroundTransparency = 1, 88 | Size = UDim2.fromOffset(BOX_SIZE, BOX_SIZE), 89 | LayoutOrder = buttonOrder, 90 | }, { 91 | Content = content, 92 | }), 93 | Label = props.Label and React.createElement("TextLabel", { 94 | BackgroundTransparency = 1, 95 | Size = UDim2.new(1, -BOX_SIZE - INNER_PADDING, 1, 0), 96 | TextXAlignment = textAlignment, 97 | TextTruncate = Enum.TextTruncate.AtEnd, 98 | Text = props.Label, 99 | Font = Constants.DefaultFont, 100 | TextSize = Constants.DefaultTextSize, 101 | TextColor3 = theme:GetColor(Enum.StudioStyleGuideColor.MainText, mainModifier), 102 | LayoutOrder = labelOrder, 103 | }, { 104 | Constraint = React.createElement("UISizeConstraint", { 105 | MaxSize = Vector2.new(textWidth, math.huge), 106 | }), 107 | Pad = React.createElement("UIPadding", { 108 | PaddingBottom = UDim.new(0, 1), 109 | }), 110 | }), 111 | }) 112 | end 113 | 114 | return BaseLabelledToggle 115 | -------------------------------------------------------------------------------- /src/Components/Foundation/BaseTextInput.luau: -------------------------------------------------------------------------------- 1 | local React = require("@pkg/@jsdotlua/react") 2 | 3 | local CommonProps = require("../../CommonProps") 4 | local Constants = require("../../Constants") 5 | 6 | local getTextSize = require("../../getTextSize") 7 | local useTheme = require("../../Hooks/useTheme") 8 | 9 | local PLACEHOLDER_TEXT_COLOR = Color3.fromRGB(102, 102, 102) 10 | local EDGE_PADDING_PX = 5 11 | local DEFAULT_HEIGHT = Constants.DefaultInputHeight 12 | 13 | local TEXT_SIZE = Constants.DefaultTextSize 14 | local FONT = Constants.DefaultFont 15 | 16 | local function joinDictionaries(dict0, dict1) 17 | local joined = table.clone(dict0) 18 | for k, v in dict1 do 19 | joined[k] = v 20 | end 21 | return joined 22 | end 23 | 24 | export type BaseTextInputConsumerProps = CommonProps.T & { 25 | PlaceholderText: string?, 26 | ClearTextOnFocus: boolean?, 27 | OnFocused: (() -> ())?, 28 | OnFocusLost: ((text: string, enterPressed: boolean, input: InputObject) -> ())?, 29 | children: React.ReactNode, 30 | } 31 | 32 | export type BaseTextInputProps = BaseTextInputConsumerProps & { 33 | Text: string, 34 | OnChanged: (newText: string) -> (), 35 | RightPaddingExtra: number?, 36 | } 37 | 38 | local function BaseTextInput(props: BaseTextInputProps) 39 | local theme = useTheme() 40 | local hovered, setHovered = React.useState(false) 41 | local focused, setFocused = React.useState(false) 42 | local disabled = props.Disabled == true 43 | 44 | local predictNextCursor = React.useRef(-1) :: { current: number } 45 | local lastCursor = React.useRef(-1) :: { current: number } 46 | 47 | local mainModifier = Enum.StudioStyleGuideModifier.Default 48 | local borderModifier = Enum.StudioStyleGuideModifier.Default 49 | if disabled then 50 | mainModifier = Enum.StudioStyleGuideModifier.Disabled 51 | borderModifier = Enum.StudioStyleGuideModifier.Disabled 52 | elseif focused then 53 | borderModifier = Enum.StudioStyleGuideModifier.Selected 54 | elseif hovered then 55 | borderModifier = Enum.StudioStyleGuideModifier.Hover 56 | end 57 | 58 | local cursor, setCursor = React.useState(-1) 59 | local containerSize, setContainerSize = React.useState(Vector2.zero) 60 | local innerOffset = React.useRef(0) :: { current: number } 61 | 62 | local fullTextWidth = getTextSize(props.Text).X 63 | local textFieldSize = UDim2.fromScale(1, 1) 64 | 65 | if not disabled then 66 | local min = EDGE_PADDING_PX 67 | local max = containerSize.X - EDGE_PADDING_PX 68 | local textUpToCursor = string.sub(props.Text, 1, cursor - 1) 69 | local offset = getTextSize(textUpToCursor).X + EDGE_PADDING_PX 70 | local innerArea = max - min 71 | local fullOffset = offset + innerOffset.current 72 | if fullTextWidth <= innerArea or not focused then 73 | innerOffset.current = 0 74 | else 75 | if fullOffset < min then 76 | innerOffset.current += min - fullOffset 77 | elseif fullOffset > max then 78 | innerOffset.current -= fullOffset - max 79 | end 80 | innerOffset.current = math.max(innerOffset.current, innerArea - fullTextWidth) 81 | end 82 | else 83 | innerOffset.current = 0 84 | end 85 | 86 | if focused then 87 | local textFieldWidth = math.max(containerSize.X, fullTextWidth + EDGE_PADDING_PX * 2) 88 | textFieldSize = UDim2.new(0, textFieldWidth, 1, 0) 89 | end 90 | 91 | local textFieldProps = { 92 | Size = textFieldSize, 93 | Position = UDim2.fromOffset(innerOffset.current, 0), 94 | BackgroundTransparency = 1, 95 | Font = FONT, 96 | Text = props.Text, 97 | TextSize = TEXT_SIZE, 98 | TextColor3 = theme:GetColor(Enum.StudioStyleGuideColor.MainText, mainModifier), 99 | TextXAlignment = Enum.TextXAlignment.Left, 100 | TextTruncate = if focused then Enum.TextTruncate.None else Enum.TextTruncate.AtEnd, 101 | [React.Event.InputBegan] = function(_, input: InputObject) 102 | if input.UserInputType == Enum.UserInputType.MouseMovement then 103 | setHovered(true) 104 | end 105 | end, 106 | [React.Event.InputEnded] = function(_, input: InputObject) 107 | if input.UserInputType == Enum.UserInputType.MouseMovement then 108 | setHovered(false) 109 | end 110 | end, 111 | children = { 112 | Padding = React.createElement("UIPadding", { 113 | PaddingLeft = UDim.new(0, EDGE_PADDING_PX), 114 | PaddingRight = UDim.new(0, EDGE_PADDING_PX), 115 | }), 116 | }, 117 | } 118 | 119 | local textField 120 | if disabled then 121 | textField = React.createElement("TextLabel", textFieldProps) 122 | else 123 | textField = React.createElement( 124 | "TextBox", 125 | joinDictionaries(textFieldProps, { 126 | PlaceholderText = props.PlaceholderText, 127 | PlaceholderColor3 = PLACEHOLDER_TEXT_COLOR, 128 | ClearTextOnFocus = props.ClearTextOnFocus, 129 | MultiLine = false, 130 | [React.Change.CursorPosition] = function(rbx: TextBox) 131 | -- cursor position changed fires before text changed, so we defer it until after; 132 | -- this enables us to use the pre-text-changed cursor position to revert to 133 | task.defer(function() 134 | lastCursor.current = rbx.CursorPosition 135 | end) 136 | setCursor(rbx.CursorPosition) 137 | end, 138 | [React.Change.Text] = function(rbx: TextBox) 139 | local newText = rbx.Text 140 | if newText ~= props.Text then 141 | predictNextCursor.current = rbx.CursorPosition 142 | rbx.Text = props.Text 143 | rbx.CursorPosition = math.max(1, lastCursor.current) 144 | props.OnChanged((string.gsub(newText, "[\n\r]", ""))) 145 | elseif focused then 146 | rbx.CursorPosition = math.max(1, predictNextCursor.current) 147 | end 148 | end, 149 | [React.Event.Focused] = function() 150 | setFocused(true) 151 | if props.OnFocused then 152 | props.OnFocused() 153 | end 154 | end, 155 | [React.Event.FocusLost] = function(rbx: TextBox, enterPressed: boolean, input: InputObject) 156 | setFocused(false) 157 | if props.OnFocusLost then 158 | props.OnFocusLost(rbx.Text, enterPressed, input) 159 | end 160 | end :: () -> (), 161 | }) 162 | ) 163 | end 164 | 165 | local rightPaddingExtra = props.RightPaddingExtra or 0 166 | 167 | return React.createElement("Frame", { 168 | AnchorPoint = props.AnchorPoint, 169 | Position = props.Position, 170 | Size = props.Size or UDim2.new(1, 0, 0, DEFAULT_HEIGHT), 171 | LayoutOrder = props.LayoutOrder, 172 | ZIndex = props.ZIndex, 173 | BackgroundColor3 = theme:GetColor(Enum.StudioStyleGuideColor.InputFieldBackground, mainModifier), 174 | BorderColor3 = theme:GetColor(Enum.StudioStyleGuideColor.InputFieldBorder, borderModifier), 175 | BorderMode = Enum.BorderMode.Inset, 176 | [React.Change.AbsoluteSize] = function(rbx: Frame) 177 | setContainerSize(rbx.AbsoluteSize - Vector2.new(rightPaddingExtra, 0)) 178 | end, 179 | }, { 180 | Clipping = React.createElement("Frame", { 181 | Size = UDim2.new(1, -rightPaddingExtra, 1, 0), 182 | BackgroundTransparency = 1, 183 | ClipsDescendants = true, 184 | ZIndex = 0, 185 | }, { 186 | TextField = textField, 187 | }), 188 | }, props.children) 189 | end 190 | 191 | return BaseTextInput 192 | -------------------------------------------------------------------------------- /src/Components/Label.luau: -------------------------------------------------------------------------------- 1 | --[=[ 2 | @class Label 3 | 4 | A basic text label with default styling to match built-in labels as closely as possible. 5 | 6 | | Dark | Light | 7 | | - | - | 8 | | ![Dark](/StudioComponents/components/label/dark.png) | ![Light](/StudioComponents/components/label/light.png) | 9 | 10 | By default, text color matches the current theme's MainText color, which is the color 11 | used in the Explorer and Properties widgets as well as most other places. It can be overriden 12 | in two ways: 13 | 1. Passing a [StudioStyleGuideColor](https://create.roblox.com/docs/reference/engine/enums/StudioStyleGuideColor) 14 | to the `TextColorStyle` prop. This is the preferred way to recolor text 15 | because it will use the correct version of the color for the user's current selected theme. 16 | 2. Passing a [Color3] value to the `TextColor3` prop. This is useful when a color is not represented 17 | by any StudioStyleGuideColor or should remain constant regardless of theme. 18 | 19 | Example of creating an error message label: 20 | 21 | ```lua 22 | local function MyComponent() 23 | return React.createElement(StudioComponents.Label, { 24 | Text = "Please enter at least 5 characters!", 25 | TextColorStyle = Enum.StudioStyleGuideColor.ErrorText, 26 | }) 27 | end 28 | ``` 29 | 30 | Plugins like [Theme Color Shower](https://create.roblox.com/store/asset/3115567199/Theme-Color-Shower) 31 | are useful for finding a StudioStyleGuideColor to use. 32 | 33 | This component will parent any children passed to it to the underlying TextLabel instance. 34 | This is useful for things like adding extra padding around the text using a nested UIPadding, 35 | or adding a UIStroke / UIGradient. 36 | 37 | Labels use [Constants.DefaultFont] for Font and [Constants.DefaultTextSize] for TextSize. This 38 | cannot currently be overriden via props. 39 | ]=] 40 | 41 | local React = require("@pkg/@jsdotlua/react") 42 | 43 | local CommonProps = require("../CommonProps") 44 | local Constants = require("../Constants") 45 | local useTheme = require("../Hooks/useTheme") 46 | 47 | --[=[ 48 | @within Label 49 | @interface Props 50 | @tag Component Props 51 | 52 | @field ... CommonProps 53 | @field Text string 54 | @field TextWrapped boolean? 55 | @field TextXAlignment Enum.TextXAlignment? 56 | @field TextYAlignment Enum.TextYAlignment? 57 | @field TextTruncate Enum.TextTruncate? 58 | @field TextTransparency number? 59 | @field TextColor3 Color3? 60 | @field RichText boolean? 61 | @field MaxVisibleGraphemes number? 62 | @field TextColorStyle Enum.StudioStyleGuideColor? 63 | @field children React.ReactNode 64 | ]=] 65 | 66 | type LabelProps = CommonProps.T & { 67 | Text: string, 68 | TextWrapped: boolean?, 69 | TextXAlignment: Enum.TextXAlignment?, 70 | TextYAlignment: Enum.TextYAlignment?, 71 | TextTruncate: Enum.TextTruncate?, 72 | TextTransparency: number?, 73 | TextColor3: Color3?, 74 | RichText: boolean?, 75 | MaxVisibleGraphemes: number?, 76 | TextColorStyle: Enum.StudioStyleGuideColor?, 77 | children: React.ReactNode, 78 | } 79 | 80 | local function Label(props: LabelProps) 81 | local theme = useTheme() 82 | local modifier = Enum.StudioStyleGuideModifier.Default 83 | if props.Disabled then 84 | modifier = Enum.StudioStyleGuideModifier.Disabled 85 | end 86 | 87 | local style = props.TextColorStyle or Enum.StudioStyleGuideColor.MainText 88 | local color = theme:GetColor(style, modifier) 89 | if props.TextColor3 ~= nil then 90 | color = props.TextColor3 91 | end 92 | 93 | return React.createElement("TextLabel", { 94 | AnchorPoint = props.AnchorPoint, 95 | Position = props.Position, 96 | Size = props.Size or UDim2.fromScale(1, 1), 97 | LayoutOrder = props.LayoutOrder, 98 | ZIndex = props.ZIndex, 99 | Text = props.Text, 100 | BackgroundTransparency = 1, 101 | Font = Constants.DefaultFont, 102 | TextSize = Constants.DefaultTextSize, 103 | TextColor3 = color, 104 | TextTransparency = props.TextTransparency, 105 | TextXAlignment = props.TextXAlignment, 106 | TextYAlignment = props.TextYAlignment, 107 | TextTruncate = props.TextTruncate, 108 | TextWrapped = props.TextWrapped, 109 | RichText = props.RichText, 110 | MaxVisibleGraphemes = props.MaxVisibleGraphemes, 111 | }, props.children) 112 | end 113 | 114 | return Label 115 | -------------------------------------------------------------------------------- /src/Components/LoadingDots.luau: -------------------------------------------------------------------------------- 1 | --[=[ 2 | @class LoadingDots 3 | 4 | A basic animated loading indicator. This matches similar indicators used in various places 5 | around Studio. This should be used for short processes where the user does not need to see 6 | information about how complete the loading is. For longer or more detailed loading processes, 7 | consider using a [ProgressBar]. 8 | 9 | | Dark | Light | 10 | | - | - | 11 | | ![Dark](/StudioComponents/components/loadingdots/dark.gif) | ![Light](/StudioComponents/components/loadingdots/light.gif) | 12 | 13 | Example of usage: 14 | 15 | ```lua 16 | local function MyComponent() 17 | return React.createElement(StudioComponents.LoadingDots, {}) 18 | end 19 | ``` 20 | ]=] 21 | 22 | local RunService = game:GetService("RunService") 23 | 24 | local React = require("@pkg/@jsdotlua/react") 25 | 26 | local CommonProps = require("../CommonProps") 27 | local useTheme = require("../Hooks/useTheme") 28 | 29 | --[=[ 30 | @within LoadingDots 31 | @interface Props 32 | @tag Component Props 33 | 34 | @field ... CommonProps 35 | ]=] 36 | 37 | type LoadingDotsProps = CommonProps.T 38 | 39 | local function Dot(props: { 40 | LayoutOrder: number, 41 | Transparency: React.Binding, 42 | Disabled: boolean?, 43 | }) 44 | local theme = useTheme() 45 | 46 | return React.createElement("Frame", { 47 | LayoutOrder = props.LayoutOrder, 48 | Size = UDim2.fromOffset(10, 10), 49 | BackgroundColor3 = theme:GetColor(Enum.StudioStyleGuideColor.ButtonText), 50 | BorderColor3 = theme:GetColor(Enum.StudioStyleGuideColor.ButtonBorder), 51 | BackgroundTransparency = if props.Disabled then 0.75 else props.Transparency, 52 | }) 53 | end 54 | 55 | local function LoadingDots(props: LoadingDotsProps) 56 | local clockBinding, setClockBinding = React.useBinding(os.clock()) 57 | React.useEffect(function() 58 | local connection = RunService.Heartbeat:Connect(function() 59 | setClockBinding(os.clock()) 60 | end) 61 | return function() 62 | return connection:Disconnect() 63 | end 64 | end, {}) 65 | 66 | local alphaBinding = clockBinding:map(function(clock: number) 67 | return clock % 1 68 | end) 69 | 70 | return React.createElement("Frame", { 71 | AnchorPoint = props.AnchorPoint, 72 | Position = props.Position, 73 | Size = props.Size or UDim2.fromScale(1, 1), 74 | LayoutOrder = props.LayoutOrder, 75 | ZIndex = props.ZIndex, 76 | BackgroundTransparency = 1, 77 | }, { 78 | Layout = React.createElement("UIListLayout", { 79 | SortOrder = Enum.SortOrder.LayoutOrder, 80 | FillDirection = Enum.FillDirection.Horizontal, 81 | HorizontalAlignment = Enum.HorizontalAlignment.Center, 82 | VerticalAlignment = Enum.VerticalAlignment.Center, 83 | Padding = UDim.new(0, 8), 84 | }), 85 | Dot0 = React.createElement(Dot, { 86 | LayoutOrder = 0, 87 | Transparency = alphaBinding, 88 | Disabled = props.Disabled, 89 | }), 90 | Dot1 = React.createElement(Dot, { 91 | LayoutOrder = 1, 92 | Transparency = alphaBinding:map(function(alpha: number) 93 | return (alpha - 0.2) % 1 94 | end), 95 | Disabled = props.Disabled, 96 | }), 97 | Dot2 = React.createElement(Dot, { 98 | LayoutOrder = 2, 99 | Transparency = alphaBinding:map(function(alpha: number) 100 | return (alpha - 0.4) % 1 101 | end), 102 | Disabled = props.Disabled, 103 | }), 104 | }) 105 | end 106 | 107 | return LoadingDots 108 | -------------------------------------------------------------------------------- /src/Components/MainButton.luau: -------------------------------------------------------------------------------- 1 | --[=[ 2 | @class MainButton 3 | 4 | A variant of a [Button] used to indicate a primary action, for example an 'OK/Accept' button 5 | in a modal. 6 | 7 | | Dark | Light | 8 | | - | - | 9 | | ![Dark](/StudioComponents/components/mainbutton/dark.png) | ![Light](/StudioComponents/components/mainbutton/light.png) | 10 | 11 | See the docs for [Button] for information about customization and usage. 12 | ]=] 13 | 14 | local React = require("@pkg/@jsdotlua/react") 15 | 16 | local BaseButton = require("./Foundation/BaseButton") 17 | 18 | --[=[ 19 | @within MainButton 20 | @interface IconProps 21 | 22 | @field Image string 23 | @field Size Vector2 24 | @field Transparency number? 25 | @field Color Color3? 26 | @field UseThemeColor boolean? 27 | @field Alignment HorizontalAlignment? 28 | @field ResampleMode Enum.ResamplerMode? 29 | @field RectOffset Vector2? 30 | @field RectSize Vector2? 31 | ]=] 32 | 33 | --[=[ 34 | @within MainButton 35 | @interface Props 36 | @tag Component Props 37 | 38 | @field ... CommonProps 39 | @field AutomaticSize AutomaticSize? 40 | @field OnActivated (() -> ())? 41 | @field Text string? 42 | @field Icon IconProps? 43 | ]=] 44 | 45 | local function MainButton(props: BaseButton.BaseButtonConsumerProps) 46 | local merged = table.clone(props) :: BaseButton.BaseButtonProps 47 | merged.BackgroundColorStyle = Enum.StudioStyleGuideColor.DialogMainButton 48 | merged.BorderColorStyle = Enum.StudioStyleGuideColor.DialogButtonBorder 49 | merged.TextColorStyle = Enum.StudioStyleGuideColor.DialogMainButtonText 50 | 51 | return React.createElement(BaseButton, merged) 52 | end 53 | 54 | return MainButton 55 | -------------------------------------------------------------------------------- /src/Components/NumberSequencePicker/AxisLabel.luau: -------------------------------------------------------------------------------- 1 | local React = require("@pkg/@jsdotlua/react") 2 | 3 | local Constants = require("../../Constants") 4 | local useTheme = require("../../Hooks/useTheme") 5 | 6 | local function AxisLabel(props: { 7 | AnchorPoint: Vector2?, 8 | Position: UDim2?, 9 | TextXAlignment: Enum.TextXAlignment?, 10 | Value: number, 11 | Disabled: boolean?, 12 | }) 13 | local theme = useTheme() 14 | 15 | return React.createElement("TextLabel", { 16 | AnchorPoint = props.AnchorPoint, 17 | Position = props.Position, 18 | Size = UDim2.fromOffset(14, 14), 19 | BackgroundTransparency = 1, 20 | Text = tostring(props.Value), 21 | Font = Constants.DefaultFont, 22 | TextSize = Constants.DefaultTextSize, 23 | TextColor3 = theme:GetColor(Enum.StudioStyleGuideColor.DimmedText), 24 | TextTransparency = if props.Disabled then 0.5 else 0, 25 | TextXAlignment = props.TextXAlignment, 26 | ZIndex = -1, 27 | }) 28 | end 29 | 30 | return AxisLabel 31 | -------------------------------------------------------------------------------- /src/Components/NumberSequencePicker/Constants.luau: -------------------------------------------------------------------------------- 1 | return { 2 | EnvelopeTransparency = 0.65, 3 | EnvelopeColorStyle = Enum.StudioStyleGuideColor.DialogMainButton, 4 | EnvelopeHandleHeight = 16, 5 | } 6 | -------------------------------------------------------------------------------- /src/Components/NumberSequencePicker/DashedLine.luau: -------------------------------------------------------------------------------- 1 | local React = require("@pkg/@jsdotlua/react") 2 | 3 | local useTheme = require("../../Hooks/useTheme") 4 | 5 | local TEX_HORIZONTAL = "rbxassetid://15431624045" 6 | local TEX_VERTICAL = "rbxassetid://15431692101" 7 | 8 | local function DashedLine(props: { 9 | AnchorPoint: Vector2?, 10 | Position: UDim2?, 11 | Size: UDim2, 12 | Direction: Enum.FillDirection, 13 | Transparency: number?, 14 | Disabled: boolean?, 15 | }) 16 | local theme = useTheme() 17 | local horizontal = props.Direction == Enum.FillDirection.Horizontal 18 | 19 | local transparency = props.Transparency or 0 20 | if props.Disabled then 21 | transparency = 1 - 0.5 * (1 - transparency) 22 | end 23 | 24 | return React.createElement("ImageLabel", { 25 | Image = if horizontal then TEX_HORIZONTAL else TEX_VERTICAL, 26 | AnchorPoint = props.AnchorPoint, 27 | Position = props.Position, 28 | Size = props.Size, 29 | BorderSizePixel = 0, 30 | ScaleType = Enum.ScaleType.Tile, 31 | TileSize = if horizontal then UDim2.fromOffset(4, 1) else UDim2.fromOffset(1, 4), 32 | BackgroundTransparency = 1, 33 | ImageColor3 = theme:GetColor(Enum.StudioStyleGuideColor.DimmedText), 34 | ImageTransparency = transparency, 35 | ZIndex = 0, 36 | }) 37 | end 38 | 39 | return DashedLine 40 | -------------------------------------------------------------------------------- /src/Components/NumberSequencePicker/FreeLine.luau: -------------------------------------------------------------------------------- 1 | local React = require("@pkg/@jsdotlua/react") 2 | 3 | local TEX = "rbxassetid://15434098501" 4 | 5 | local function FreeLine(props: { 6 | Pos0: Vector2, 7 | Pos1: Vector2, 8 | Color: Color3, 9 | Transparency: number?, 10 | ZIndex: number?, 11 | Disabled: boolean?, 12 | }) 13 | local mid = (props.Pos0 + props.Pos1) / 2 14 | local vector = props.Pos1 - props.Pos0 15 | local rotation = math.atan2(-vector.X, vector.Y) + math.pi / 2 16 | local length = vector.Magnitude 17 | 18 | local transparency = props.Transparency or 0 19 | if props.Disabled then 20 | transparency = 1 - 0.5 * (1 - transparency) 21 | end 22 | 23 | return React.createElement("ImageLabel", { 24 | AnchorPoint = Vector2.one / 2, 25 | Position = UDim2.fromOffset(math.round(mid.X), math.round(mid.Y)), 26 | Size = UDim2.fromOffset(vector.Magnitude, 3), 27 | Rotation = math.deg(rotation), 28 | BackgroundTransparency = 1, 29 | ImageColor3 = props.Color, 30 | ImageTransparency = transparency, 31 | Image = TEX, 32 | ScaleType = if length < 128 then Enum.ScaleType.Crop else Enum.ScaleType.Stretch, 33 | ZIndex = props.ZIndex, 34 | }) 35 | end 36 | 37 | return FreeLine 38 | -------------------------------------------------------------------------------- /src/Components/NumberSequencePicker/LabelledNumericInput.luau: -------------------------------------------------------------------------------- 1 | local React = require("@pkg/@jsdotlua/react") 2 | 3 | local Label = require("../Label") 4 | local NumericInput = require("../NumericInput") 5 | local TextInput = require("../TextInput") 6 | 7 | local getTextSize = require("../../getTextSize") 8 | 9 | local PADDING = 5 10 | local INPUT_WIDTH = 40 11 | 12 | local function format(n: number) 13 | return string.format(`%.3f`, n) 14 | end 15 | 16 | local noop = function() end 17 | 18 | local function LabelledNumericInput(props: { 19 | Label: string, 20 | Value: number?, 21 | Disabled: boolean?, 22 | OnChanged: (value: number) -> (), 23 | OnSubmitted: (value: number) -> (), 24 | LayoutOrder: number, 25 | Min: number?, 26 | Max: number?, 27 | }) 28 | local textWidth = getTextSize(props.Label).X 29 | 30 | local input: React.ReactNode 31 | if props.Value and not props.Disabled then 32 | local value = props.Value :: number 33 | input = React.createElement(NumericInput, { 34 | Value = value, 35 | Min = props.Min, 36 | Max = props.Max, 37 | Step = 0, 38 | FormatValue = format, 39 | OnValidChanged = props.OnChanged, 40 | OnSubmitted = props.OnSubmitted, 41 | }) 42 | else 43 | input = React.createElement(TextInput, { 44 | Text = if props.Value then format(props.Value) else "", 45 | OnChanged = noop, 46 | Disabled = true, 47 | }) 48 | end 49 | 50 | return React.createElement("Frame", { 51 | Size = UDim2.new(0, textWidth + INPUT_WIDTH + PADDING, 1, 0), 52 | BackgroundTransparency = 1, 53 | LayoutOrder = props.LayoutOrder, 54 | }, { 55 | Label = React.createElement(Label, { 56 | Size = UDim2.new(0, textWidth, 1, 0), 57 | Text = props.Label, 58 | Disabled = props.Value == nil, 59 | }), 60 | Input = React.createElement("Frame", { 61 | AnchorPoint = Vector2.new(1, 0), 62 | Position = UDim2.fromScale(1, 0), 63 | Size = UDim2.new(0, INPUT_WIDTH, 1, 0), 64 | BackgroundTransparency = 1, 65 | }, input), 66 | }) 67 | end 68 | 69 | return LabelledNumericInput 70 | -------------------------------------------------------------------------------- /src/Components/NumberSequencePicker/SequenceNode.luau: -------------------------------------------------------------------------------- 1 | local React = require("@pkg/@jsdotlua/react") 2 | 3 | local useMouseDrag = require("../../Hooks/useMouseDrag") 4 | local useMouseIcon = require("../../Hooks/useMouseIcon") 5 | local useTheme = require("../../Hooks/useTheme") 6 | 7 | local PickerConstants = require("./Constants") 8 | 9 | local CATCHER_SIZE = 15 10 | local ENVELOPE_GRAB_HEIGHT = PickerConstants.EnvelopeHandleHeight 11 | local ENVELOPE_TRANSPARENCY = PickerConstants.EnvelopeTransparency 12 | local ENVELOPE_COLOR_STYLE = PickerConstants.EnvelopeColorStyle 13 | 14 | local function EnvelopeHandle(props: { 15 | Top: boolean, 16 | Size: UDim2, 17 | OnDragBegan: () -> (), 18 | OnDragEnded: () -> (), 19 | OnEnvelopeDragged: (y: number, top: boolean) -> (), 20 | Disabled: boolean?, 21 | }) 22 | local theme = useTheme() 23 | 24 | local dragStart = React.useRef(0 :: number?) 25 | local dragOffset = React.useRef(0) 26 | 27 | local function onDragBegin(rbx: GuiObject, input: InputObject) 28 | local pos = input.Position.Y 29 | local reference 30 | if props.Top then 31 | reference = rbx.AbsolutePosition.Y 32 | else 33 | reference = rbx.AbsolutePosition.Y + rbx.AbsoluteSize.Y 34 | end 35 | dragStart.current = pos 36 | dragOffset.current = reference - pos 37 | props.OnDragBegan() 38 | end 39 | 40 | local drag = useMouseDrag(function(_, input: InputObject) 41 | local position = input.Position.Y 42 | if not dragStart.current or math.abs(position - dragStart.current) > 0 then 43 | local outPosition 44 | if props.Top then 45 | outPosition = position + dragOffset.current :: number + ENVELOPE_GRAB_HEIGHT 46 | else 47 | outPosition = position + dragOffset.current :: number - ENVELOPE_GRAB_HEIGHT 48 | end 49 | props.OnEnvelopeDragged(outPosition, props.Top) 50 | dragStart.current = nil 51 | end 52 | end, { props.OnEnvelopeDragged }, onDragBegin, props.OnDragEnded) 53 | 54 | local hovered, setHovered = React.useState(false) 55 | local mouseIcon = useMouseIcon() 56 | 57 | React.useEffect(function() 58 | if (hovered or drag.isActive()) and not props.Disabled then 59 | mouseIcon.setIcon("rbxasset://SystemCursors/SplitNS") 60 | else 61 | mouseIcon.clearIcon() 62 | end 63 | end, { hovered, drag.isActive(), props.Disabled } :: { unknown }) 64 | 65 | React.useEffect(function() 66 | return function() 67 | mouseIcon.clearIcon() 68 | end 69 | end, {}) 70 | 71 | return React.createElement("TextButton", { 72 | Text = "", 73 | AutoButtonColor = false, 74 | Size = props.Size, 75 | AnchorPoint = Vector2.new(0, if props.Top then 0 else 1), 76 | Position = UDim2.fromScale(0, if props.Top then 0 else 1), 77 | BackgroundTransparency = 1, 78 | BorderSizePixel = 0, 79 | [React.Event.InputBegan] = function(rbx, input) 80 | if input.UserInputType == Enum.UserInputType.MouseMovement then 81 | setHovered(true) 82 | end 83 | drag.onInputBegan(rbx, input) 84 | end, 85 | [React.Event.InputChanged] = drag.onInputChanged, 86 | [React.Event.InputEnded] = function(rbx, input) 87 | if input.UserInputType == Enum.UserInputType.MouseMovement then 88 | setHovered(false) 89 | end 90 | drag.onInputEnded(rbx, input) 91 | end, 92 | ZIndex = 2, 93 | }, { 94 | Visual = React.createElement("Frame", { 95 | AnchorPoint = Vector2.new(0.5, if props.Top then 0 else 1), 96 | Position = UDim2.fromScale(0.5, if props.Top then 0 else 1), 97 | Size = UDim2.fromOffset(if drag.isActive() or hovered then 3 else 1, ENVELOPE_GRAB_HEIGHT + 2), 98 | BorderSizePixel = 0, 99 | BackgroundColor3 = theme:GetColor(Enum.StudioStyleGuideColor.MainText), 100 | }, { 101 | Bar = React.createElement("Frame", { 102 | AnchorPoint = Vector2.new(0.5, if props.Top then 1 else 0), 103 | Position = UDim2.fromScale(0.5, if props.Top then 1 else 0), 104 | Size = UDim2.fromOffset(9, if drag.isActive() or hovered then 3 else 1), 105 | BorderSizePixel = 0, 106 | BackgroundColor3 = theme:GetColor(Enum.StudioStyleGuideColor.MainText), 107 | }), 108 | }), 109 | }) 110 | end 111 | 112 | local function SequenceNode(props: { 113 | ContentSize: Vector2, 114 | Keypoint: NumberSequenceKeypoint, 115 | OnNodeDragged: (position: Vector2) -> (), 116 | OnEnvelopeDragged: (y: number, top: boolean) -> (), 117 | Active: boolean, 118 | OnHovered: () -> (), 119 | OnDragBegan: () -> (), 120 | OnDragEnded: () -> (), 121 | Disabled: boolean?, 122 | }) 123 | local theme = useTheme() 124 | local mouseIcon = useMouseIcon() 125 | 126 | local nodeDragStart = React.useRef(Vector2.zero :: Vector2?) 127 | local nodeDragOffset = React.useRef(Vector2.zero) 128 | local function onNodeDragBegin(rbx: GuiObject, input: InputObject) 129 | local pos = Vector2.new(input.Position.X, input.Position.Y) 130 | local corner = rbx.AbsolutePosition 131 | local center = corner + rbx.AbsoluteSize / 2 132 | nodeDragStart.current = pos 133 | nodeDragOffset.current = center - pos 134 | props.OnDragBegan() 135 | end 136 | local nodeDrag = useMouseDrag(function(_, input: InputObject) 137 | local position = Vector2.new(input.Position.X, input.Position.Y) 138 | if not nodeDragStart.current or (position - nodeDragStart.current).Magnitude > 0 then 139 | props.OnNodeDragged(position + nodeDragOffset.current :: Vector2) 140 | nodeDragStart.current = nil 141 | end 142 | end, { props.OnNodeDragged }, onNodeDragBegin, props.OnDragEnded) 143 | 144 | local px = math.round(props.Keypoint.Time * props.ContentSize.X) 145 | local py = math.round((1 - props.Keypoint.Value) * props.ContentSize.Y) 146 | 147 | local envelopeHeight = math.round(props.Keypoint.Envelope * props.ContentSize.Y) * 2 + 1 148 | local fullHeight = envelopeHeight + (ENVELOPE_GRAB_HEIGHT + 1) * 2 149 | local handleClearance = (fullHeight - CATCHER_SIZE) / 2 - 1 150 | 151 | local innerSize = if props.Active then 11 else 7 152 | 153 | local nodeHovered, setNodeHovered = React.useState(false) 154 | React.useEffect(function() 155 | if props.Active and nodeDrag.isActive() then 156 | mouseIcon.setIcon("rbxasset://SystemCursors/ClosedHand") 157 | elseif props.Active and nodeHovered then 158 | mouseIcon.setIcon("rbxasset://SystemCursors/OpenHand") 159 | else 160 | mouseIcon.clearIcon() 161 | end 162 | end, { props.Active, nodeHovered, nodeDrag.isActive() }) 163 | 164 | React.useEffect(function() 165 | if props.Disabled then 166 | mouseIcon.clearIcon() 167 | end 168 | if nodeDrag.isActive() then 169 | nodeDrag.cancel() 170 | end 171 | end, { props.Disabled }) 172 | 173 | local envelopeTransparency = ENVELOPE_TRANSPARENCY 174 | local mainModifier = Enum.StudioStyleGuideModifier.Default 175 | if props.Disabled then 176 | envelopeTransparency = 1 - 0.5 * (1 - envelopeTransparency) 177 | mainModifier = Enum.StudioStyleGuideModifier.Disabled 178 | end 179 | 180 | return React.createElement("Frame", { 181 | Position = UDim2.fromOffset(px - (CATCHER_SIZE - 1) / 2, py - (fullHeight - 1) / 2), 182 | Size = UDim2.fromOffset(CATCHER_SIZE, fullHeight), 183 | BackgroundTransparency = 1, 184 | ZIndex = 2, 185 | }, { 186 | Line = React.createElement("Frame", { 187 | AnchorPoint = Vector2.one / 2, 188 | Position = UDim2.fromScale(0.5, 0.5), 189 | Size = UDim2.fromOffset(1, envelopeHeight), 190 | BorderSizePixel = 0, 191 | BackgroundColor3 = theme:GetColor(ENVELOPE_COLOR_STYLE), 192 | BackgroundTransparency = envelopeTransparency, 193 | ZIndex = 0, 194 | }), 195 | 196 | Node = React.createElement("TextButton", { 197 | Text = "", 198 | AutoButtonColor = false, 199 | AnchorPoint = Vector2.one / 2, 200 | Position = UDim2.fromScale(0.5, 0.5), 201 | Size = UDim2.new(1, 0, 0, CATCHER_SIZE), 202 | BackgroundTransparency = 1, 203 | [React.Event.InputBegan] = function(rbx, input) 204 | if props.Disabled then 205 | return 206 | elseif input.UserInputType == Enum.UserInputType.MouseMovement then 207 | setNodeHovered(true) 208 | props.OnHovered() 209 | end 210 | nodeDrag.onInputBegan(rbx, input) 211 | end, 212 | [React.Event.InputChanged] = function(rbx, input) 213 | if props.Disabled then 214 | return 215 | end 216 | nodeDrag.onInputChanged(rbx, input) 217 | end, 218 | [React.Event.InputEnded] = function(rbx, input) 219 | if props.Disabled then 220 | return 221 | elseif input.UserInputType == Enum.UserInputType.MouseMovement then 222 | setNodeHovered(false) 223 | end 224 | nodeDrag.onInputEnded(rbx, input) 225 | end, 226 | ZIndex = 1, 227 | }, { 228 | Inner = React.createElement("Frame", { 229 | AnchorPoint = Vector2.one / 2, 230 | Position = UDim2.fromScale(0.5, 0.5), 231 | Size = UDim2.fromOffset(innerSize, innerSize), 232 | BackgroundColor3 = if props.Active 233 | then theme:GetColor(Enum.StudioStyleGuideColor.MainBackground, mainModifier) 234 | else theme:GetColor(Enum.StudioStyleGuideColor.InputFieldBackground, mainModifier), 235 | ZIndex = 2, 236 | }, { 237 | Stroke = React.createElement("UIStroke", { 238 | Color = if props.Active 239 | then theme:GetColor(Enum.StudioStyleGuideColor.MainText, mainModifier) 240 | else theme:GetColor(Enum.StudioStyleGuideColor.DimmedText, mainModifier), 241 | Thickness = if nodeDrag.isActive() then 2 else 1, 242 | }), 243 | }), 244 | }), 245 | 246 | Top = props.Active and React.createElement(EnvelopeHandle, { 247 | Top = true, 248 | Size = UDim2.new(1, 0, 0, math.min(ENVELOPE_GRAB_HEIGHT, handleClearance)), 249 | OnDragBegan = props.OnDragBegan, 250 | OnDragEnded = props.OnDragEnded, 251 | OnEnvelopeDragged = props.OnEnvelopeDragged, 252 | }), 253 | 254 | Bottom = props.Active and React.createElement(EnvelopeHandle, { 255 | Top = false, 256 | Size = UDim2.new(1, 0, 0, math.min(ENVELOPE_GRAB_HEIGHT, handleClearance)), 257 | OnDragBegan = props.OnDragBegan, 258 | OnDragEnded = props.OnDragEnded, 259 | OnEnvelopeDragged = props.OnEnvelopeDragged, 260 | }), 261 | }) 262 | end 263 | 264 | return SequenceNode 265 | -------------------------------------------------------------------------------- /src/Components/PluginProvider.luau: -------------------------------------------------------------------------------- 1 | --[=[ 2 | @class PluginProvider 3 | 4 | This component provides an interface to plugin apis for other components in the tree. It should 5 | be provided with a single `Plugin` prop that must point to `plugin` (your plugin's root instance). 6 | 7 | You do not have to use this component unless you want custom mouse icons via the [useMouseIcon] 8 | hook. Right now, the only built-in component that relies on this is [Splitter]. Theming and all 9 | other functionality will work regardless of whether this component is used. 10 | 11 | You should only render one PluginProvider in your tree. Commonly, this is done at the top of 12 | the tree with the rest of your plugin as children/descendants. 13 | 14 | Example of usage: 15 | 16 | ```lua 17 | local function MyComponent() 18 | return React.createElement(StudioComponents.PluginProvider, { 19 | Plugin = plugin, 20 | }, { 21 | MyExample = React.createElement(MyExample, ...) 22 | }) 23 | end 24 | ``` 25 | ]=] 26 | 27 | local HttpService = game:GetService("HttpService") 28 | 29 | local React = require("@pkg/@jsdotlua/react") 30 | 31 | local PluginContext = require("../Contexts/PluginContext") 32 | 33 | type IconStackEntry = { 34 | id: string, 35 | icon: string, 36 | } 37 | 38 | --[=[ 39 | @within PluginProvider 40 | @interface Props 41 | @tag Component Props 42 | 43 | @field Plugin Plugin 44 | @field children React.ReactNode 45 | ]=] 46 | 47 | type PluginProviderProps = { 48 | Plugin: Plugin, 49 | children: React.ReactNode, 50 | } 51 | 52 | local function PluginProvider(props: PluginProviderProps) 53 | local plugin = props.Plugin 54 | local iconStack = React.useRef({}) :: { current: { IconStackEntry } } 55 | 56 | local function updateMouseIcon() 57 | local top = iconStack.current[#iconStack.current] 58 | plugin:GetMouse().Icon = if top then top.icon else "" 59 | end 60 | 61 | local function pushMouseIcon(icon) 62 | local id = HttpService:GenerateGUID(false) 63 | table.insert(iconStack.current, { id = id, icon = icon }) 64 | updateMouseIcon() 65 | return id 66 | end 67 | 68 | local function popMouseIcon(id) 69 | for i = #iconStack.current, 1, -1 do 70 | local item = iconStack.current[i] 71 | if item.id == id then 72 | table.remove(iconStack.current, i) 73 | end 74 | end 75 | updateMouseIcon() 76 | end 77 | 78 | React.useEffect(function() 79 | return function() 80 | table.clear(iconStack.current) 81 | plugin:GetMouse().Icon = "" 82 | end 83 | end, {}) 84 | 85 | return React.createElement(PluginContext.Provider, { 86 | value = { 87 | plugin = plugin, 88 | pushMouseIcon = pushMouseIcon, 89 | popMouseIcon = popMouseIcon, 90 | }, 91 | }, props.children) 92 | end 93 | 94 | return PluginProvider 95 | -------------------------------------------------------------------------------- /src/Components/ProgressBar.luau: -------------------------------------------------------------------------------- 1 | --[=[ 2 | @class ProgressBar 3 | 4 | A basic progress indicator. This should be used for longer or more detailed loading processes. 5 | For shorter loading processes, consider using a [LoadingDots] component. 6 | 7 | | Dark | Light | 8 | | - | - | 9 | | ![Dark](/StudioComponents/components/progressbar/dark.png) | ![Light](/StudioComponents/components/progressbar/light.png) | 10 | 11 | Pass a number representing the current progress into the `Value` prop. You can optionally pass a 12 | number representing the maximum value into the `Max` prop, which defaults to 1 if not provided. 13 | The `Value` prop should be between 0 and `Max`. For example: 14 | 15 | ```lua 16 | local function MyComponent() 17 | return React.createElement(StudioComponents.ProgressBar, { 18 | Value = 5, -- loaded 5 items 19 | Max = 10, -- out of a total of 10 items 20 | }) 21 | end 22 | ``` 23 | 24 | By default, the progress bar will display text indicating the progress as a percentage, 25 | rounded to the nearest whole number. This can be customized by providing a prop to `Formatter`, 26 | which should be a function that takes two numbers representing the current value and the maximum value 27 | and returns a string to be displayed. For example: 28 | 29 | ```lua 30 | local function MyComponent() 31 | return React.createElement(StudioComponents.ProgressBar, { 32 | Value = 3, 33 | Max = 20, 34 | Formatter = function(current, max) 35 | return `Loaded {current} / {max} assets...` 36 | end, 37 | }) 38 | end 39 | ``` 40 | 41 | By default, the height of a progress bar is equal to the value in [Constants.DefaultProgressBarHeight]. 42 | This can be configured via props. 43 | ]=] 44 | 45 | local React = require("@pkg/@jsdotlua/react") 46 | 47 | local CommonProps = require("../CommonProps") 48 | local Constants = require("../Constants") 49 | local useTheme = require("../Hooks/useTheme") 50 | 51 | --[=[ 52 | @within ProgressBar 53 | @interface Props 54 | @tag Component Props 55 | 56 | @field ... CommonProps 57 | @field Value number 58 | @field Max number? 59 | @field Formatter ((value: number, max: number) -> string)? 60 | ]=] 61 | 62 | type ProgressBarProps = CommonProps.T & { 63 | Value: number, 64 | Max: number?, 65 | Formatter: ((value: number, max: number) -> string)?, 66 | } 67 | 68 | local function defaultFormatter(value: number, max: number) 69 | return string.format("%i%%", 100 * value / max) 70 | end 71 | 72 | local function ProgressBar(props: ProgressBarProps) 73 | local theme = useTheme() 74 | 75 | local formatter: (number, number) -> string = defaultFormatter 76 | if props.Formatter then 77 | formatter = props.Formatter 78 | end 79 | 80 | local max = props.Max or 1 81 | local value = math.clamp(props.Value, 0, max) 82 | local alpha = value / max 83 | local text = formatter(value, max) 84 | 85 | local modifier = Enum.StudioStyleGuideModifier.Default 86 | if props.Disabled then 87 | modifier = Enum.StudioStyleGuideModifier.Disabled 88 | end 89 | 90 | return React.createElement("Frame", { 91 | AnchorPoint = props.AnchorPoint, 92 | Position = props.Position, 93 | Size = props.Size or UDim2.new(1, 0, 0, Constants.DefaultProgressBarHeight), 94 | LayoutOrder = props.LayoutOrder, 95 | ZIndex = props.ZIndex, 96 | BackgroundColor3 = theme:GetColor(Enum.StudioStyleGuideColor.InputFieldBackground, modifier), 97 | BorderColor3 = theme:GetColor(Enum.StudioStyleGuideColor.InputFieldBorder, modifier), 98 | }, { 99 | Bar = React.createElement("Frame", { 100 | BackgroundColor3 = theme:GetColor(Enum.StudioStyleGuideColor.DialogMainButton, modifier), 101 | BorderSizePixel = 0, 102 | Size = UDim2.fromScale(alpha, 1), 103 | ClipsDescendants = true, 104 | ZIndex = 1, 105 | }, { 106 | Left = React.createElement("TextLabel", { 107 | BackgroundTransparency = 1, 108 | Size = UDim2.fromScale(1 / alpha, 1) - UDim2.fromOffset(0, 1), 109 | Font = Constants.DefaultFont, 110 | TextSize = Constants.DefaultTextSize, 111 | TextColor3 = Color3.fromRGB(12, 12, 12), 112 | TextTransparency = if props.Disabled then 0.5 else 0, 113 | Text = text, 114 | }), 115 | }), 116 | Under = React.createElement("TextLabel", { 117 | BackgroundTransparency = 1, 118 | Size = UDim2.fromScale(1, 1) - UDim2.fromOffset(0, 1), 119 | Font = Constants.DefaultFont, 120 | TextSize = Constants.DefaultTextSize, 121 | TextColor3 = theme:GetColor(Enum.StudioStyleGuideColor.MainText, modifier), 122 | Text = text, 123 | ZIndex = 0, 124 | }), 125 | }) 126 | end 127 | 128 | return ProgressBar 129 | -------------------------------------------------------------------------------- /src/Components/RadioButton.luau: -------------------------------------------------------------------------------- 1 | --[=[ 2 | @class RadioButton 3 | 4 | An input element similar to a [Checkbox] which can either be selected or not selected. 5 | This should be used for an option in a mutually exclusive group of options (the user can 6 | only select one out of the group). This grouping behavior is not included and must be 7 | implemented separately. 8 | 9 | | Dark | Light | 10 | | - | - | 11 | | ![Dark](/StudioComponents/components/radiobutton/dark.png) | ![Light](/StudioComponents/components/radiobutton/light.png) | 12 | 13 | The props and behavior for this component are the same as [Checkbox]. Importantly, this is 14 | also a controlled component, which means it does not manage its own selected state. A value 15 | must be passed to the `Value` prop and a callback should be passed to the `OnChanged` prop. 16 | For example: 17 | 18 | ```lua 19 | local function MyComponent() 20 | local selected, setSelected = React.useState(false) 21 | return React.createElement(StudioComponents.RadioButton, { 22 | Value = selected, 23 | OnChanged = setSelected, 24 | }) 25 | end 26 | ``` 27 | 28 | For more information about customizing this component via props, refer to the documentation 29 | for [Checkbox]. The default height for this component is found in [Constants.DefaultToggleHeight]. 30 | ]=] 31 | 32 | local React = require("@pkg/@jsdotlua/react") 33 | 34 | local BaseLabelledToggle = require("./Foundation/BaseLabelledToggle") 35 | local useTheme = require("../Hooks/useTheme") 36 | 37 | local PADDING = 1 38 | local INNER_PADDING = 3 39 | 40 | --[=[ 41 | @within RadioButton 42 | @interface Props 43 | @tag Component Props 44 | 45 | @field ... CommonProps 46 | @field Value boolean? 47 | @field OnChanged (() -> ())? 48 | @field Label string? 49 | @field ContentAlignment HorizontalAlignment? 50 | @field ButtonAlignment HorizontalAlignment? 51 | ]=] 52 | 53 | type RadioButtonProps = BaseLabelledToggle.BaseLabelledToggleConsumerProps & { 54 | Value: boolean, 55 | } 56 | 57 | local function RadioButton(props: RadioButtonProps) 58 | local theme = useTheme() 59 | local mergedProps = table.clone(props) :: BaseLabelledToggle.BaseLabelledToggleProps 60 | 61 | function mergedProps.RenderButton(subProps: { Hovered: boolean }) 62 | local mainModifier = Enum.StudioStyleGuideModifier.Default 63 | if props.Disabled then 64 | mainModifier = Enum.StudioStyleGuideModifier.Disabled 65 | elseif subProps.Hovered then 66 | mainModifier = Enum.StudioStyleGuideModifier.Hover 67 | end 68 | 69 | return React.createElement("Frame", { 70 | BackgroundColor3 = theme:GetColor(Enum.StudioStyleGuideColor.CheckedFieldBackground, mainModifier), 71 | BackgroundTransparency = 0, 72 | Size = UDim2.new(1, -PADDING * 2, 1, -PADDING * 2), 73 | Position = UDim2.fromOffset(1, PADDING), 74 | }, { 75 | Corner = React.createElement("UICorner", { 76 | CornerRadius = UDim.new(0.5, 0), 77 | }), 78 | Stroke = React.createElement("UIStroke", { 79 | Color = theme:GetColor(Enum.StudioStyleGuideColor.CheckedFieldBorder, mainModifier), 80 | Transparency = 0, 81 | }), 82 | Inner = if props.Value == true 83 | then React.createElement("Frame", { 84 | Size = UDim2.new(1, -INNER_PADDING * 2, 1, -INNER_PADDING * 2), 85 | Position = UDim2.fromOffset(INNER_PADDING, INNER_PADDING), 86 | BackgroundColor3 = theme:GetColor(Enum.StudioStyleGuideColor.DialogMainButton, mainModifier), 87 | BackgroundTransparency = 0.25, 88 | }, { 89 | Corner = React.createElement("UICorner", { 90 | CornerRadius = UDim.new(0.5, 0), 91 | }), 92 | }) 93 | else nil, 94 | }) 95 | end 96 | 97 | return React.createElement(BaseLabelledToggle, mergedProps) 98 | end 99 | 100 | return RadioButton 101 | -------------------------------------------------------------------------------- /src/Components/ScrollFrame/Constants.luau: -------------------------------------------------------------------------------- 1 | return { 2 | ScrollBarThickness = 16, 3 | WheelScrollAmount = 48, 4 | ArrowScrollAmount = 16, 5 | } 6 | -------------------------------------------------------------------------------- /src/Components/ScrollFrame/ScrollBar.luau: -------------------------------------------------------------------------------- 1 | local React = require("@pkg/@jsdotlua/react") 2 | 3 | local useMouseDrag = require("../../Hooks/useMouseDrag") 4 | local useTheme = require("../../Hooks/useTheme") 5 | 6 | local Constants = require("./Constants") 7 | local ScrollBarArrow = require("./ScrollBarArrow") 8 | local Types = require("./Types") 9 | 10 | local SCROLLBAR_THICKNESS = Constants.ScrollBarThickness 11 | local INPUT_MOVE = Enum.UserInputType.MouseMovement 12 | 13 | local function flipVector2(vector: Vector2, shouldFlip: boolean) 14 | return if shouldFlip then Vector2.new(vector.Y, vector.X) else vector 15 | end 16 | 17 | local function flipUDim2(udim: UDim2, shouldFlip: boolean) 18 | return if shouldFlip then UDim2.new(udim.Height, udim.Width) else udim 19 | end 20 | 21 | type ScrollData = Types.ScrollData 22 | 23 | type ScrollBarProps = { 24 | BumpScroll: (scrollVector: Vector2) -> (), 25 | Orientation: Types.BarOrientation, 26 | ScrollData: React.Binding, 27 | ScrollOffset: React.Binding, 28 | SetScrollOffset: (offset: Vector2) -> (), 29 | Disabled: boolean?, 30 | } 31 | 32 | local function ScrollBar(props: ScrollBarProps) 33 | local vertical = props.Orientation == "Vertical" 34 | local scrollDataBinding = props.ScrollData 35 | 36 | local theme = useTheme() 37 | 38 | local hovered, setHovered = React.useState(false) 39 | local dragStartMouse = React.useRef(nil) :: { current: Vector2? } 40 | local dragStartCanvas = React.useRef(nil) :: { current: Vector2? } 41 | 42 | local function onDragStarted(_, input: InputObject) 43 | dragStartMouse.current = Vector2.new(input.Position.X, input.Position.Y) 44 | dragStartCanvas.current = props.ScrollOffset:getValue() 45 | end 46 | 47 | local function onDragEnded() 48 | dragStartMouse.current = nil 49 | dragStartCanvas.current = nil 50 | end 51 | 52 | local function onDragged(_, input: InputObject) 53 | local scrollData = scrollDataBinding:getValue() 54 | local contentSize = scrollData.ContentSize 55 | local windowSize = scrollData.WindowSize 56 | local innerBarSize = scrollData.InnerBarSize 57 | 58 | local offsetFrom = dragStartCanvas.current :: Vector2 59 | local mouseFrom = dragStartMouse.current :: Vector2 60 | local mouseDelta = Vector2.new(input.Position.X, input.Position.Y) - mouseFrom 61 | 62 | local shiftAlpha = mouseDelta / (innerBarSize - scrollData.BarSize) 63 | local overflow = contentSize - windowSize 64 | 65 | local newOffset = offsetFrom + overflow * shiftAlpha 66 | newOffset = newOffset:Min(overflow) 67 | newOffset = newOffset:Max(Vector2.zero) 68 | 69 | local freshScrollOffset = props.ScrollOffset:getValue() 70 | if vertical then 71 | props.SetScrollOffset(Vector2.new(freshScrollOffset.X, newOffset.Y)) 72 | else 73 | props.SetScrollOffset(Vector2.new(newOffset.X, freshScrollOffset.Y)) 74 | end 75 | end 76 | 77 | local dragDeps = { props.ScrollOffset, props.SetScrollOffset } :: { unknown } 78 | local drag = useMouseDrag(onDragged, dragDeps, onDragStarted, onDragEnded) 79 | 80 | React.useEffect(function() 81 | if props.Disabled and drag.isActive() then 82 | drag.cancel() 83 | onDragEnded() 84 | end 85 | -- if props.Disabled then 86 | -- setHovered(false) 87 | -- end 88 | end, { props.Disabled, drag.isActive() }) 89 | 90 | local modifier = Enum.StudioStyleGuideModifier.Default 91 | if props.Disabled then 92 | modifier = Enum.StudioStyleGuideModifier.Disabled 93 | elseif hovered or drag.isActive() then 94 | modifier = Enum.StudioStyleGuideModifier.Pressed 95 | end 96 | 97 | return React.createElement("Frame", { 98 | BackgroundColor3 = theme:GetColor(Enum.StudioStyleGuideColor.ScrollBarBackground), 99 | BorderColor3 = theme:GetColor(Enum.StudioStyleGuideColor.Border), 100 | Visible = scrollDataBinding:map(function(data: ScrollData) 101 | return if vertical then data.BarVisible.Y else data.BarVisible.X 102 | end), 103 | AnchorPoint = flipVector2(Vector2.new(1, 0), not vertical), 104 | Position = flipUDim2(UDim2.fromScale(1, 0), not vertical), 105 | Size = scrollDataBinding:map(function(data) 106 | local extra = if (vertical and data.BarVisible.X) or (not vertical and data.BarVisible.Y) 107 | then -SCROLLBAR_THICKNESS - 1 108 | else 0 109 | return flipUDim2(UDim2.new(0, SCROLLBAR_THICKNESS, 1, extra), not vertical) 110 | end), 111 | }, { 112 | Arrow0 = React.createElement(ScrollBarArrow, { 113 | Side = 0, 114 | Orientation = props.Orientation, 115 | BumpScroll = props.BumpScroll, 116 | Disabled = props.Disabled, 117 | }), 118 | Arrow1 = React.createElement(ScrollBarArrow, { 119 | Side = 1, 120 | Orientation = props.Orientation, 121 | BumpScroll = props.BumpScroll, 122 | Position = flipUDim2(UDim2.fromScale(0, 1), not vertical), 123 | AnchorPoint = flipVector2(Vector2.new(0, 1), not vertical), 124 | Disabled = props.Disabled, 125 | }), 126 | Region = React.createElement("Frame", { 127 | BackgroundTransparency = 1, 128 | Position = flipUDim2(UDim2.fromOffset(0, SCROLLBAR_THICKNESS + 1), not vertical), 129 | Size = flipUDim2(UDim2.new(1, 0, 1, -(SCROLLBAR_THICKNESS + 1) * 2), not vertical), 130 | }, { 131 | Handle = React.createElement("Frame", { 132 | BackgroundColor3 = theme:GetColor(Enum.StudioStyleGuideColor.ScrollBar, modifier), 133 | BorderColor3 = theme:GetColor(Enum.StudioStyleGuideColor.Border, modifier), 134 | Size = scrollDataBinding:map(function(data: ScrollData) 135 | local size = if vertical then data.BarSize.Y else data.BarSize.X 136 | return flipUDim2(UDim2.new(1, 0, 0, size), not vertical) 137 | end), 138 | Position = scrollDataBinding:map(function(data: ScrollData) 139 | local position = if vertical then data.BarPosition.Y else data.BarPosition.X 140 | return flipUDim2(UDim2.fromScale(0, position), not vertical) 141 | end), 142 | AnchorPoint = scrollDataBinding:map(function(data: ScrollData) 143 | local position = if vertical then data.BarPosition.Y else data.BarPosition.X 144 | return flipVector2(Vector2.new(0, position), not vertical) 145 | end), 146 | [React.Event.InputBegan] = function(rbx: Frame, input: InputObject) 147 | if input.UserInputType == INPUT_MOVE then 148 | setHovered(true) 149 | end 150 | if not props.Disabled then 151 | drag.onInputBegan(rbx, input) 152 | end 153 | end, 154 | [React.Event.InputChanged] = function(rbx: Frame, input: InputObject) 155 | if not props.Disabled then 156 | drag.onInputChanged(rbx, input) 157 | end 158 | end, 159 | [React.Event.InputEnded] = function(rbx: Frame, input: InputObject) 160 | if input.UserInputType == INPUT_MOVE then 161 | setHovered(false) 162 | end 163 | if not props.Disabled then 164 | drag.onInputEnded(rbx, input) 165 | end 166 | end, 167 | }), 168 | }), 169 | }) 170 | end 171 | 172 | return ScrollBar 173 | -------------------------------------------------------------------------------- /src/Components/ScrollFrame/ScrollBarArrow.luau: -------------------------------------------------------------------------------- 1 | local RunService = game:GetService("RunService") 2 | 3 | local React = require("@pkg/@jsdotlua/react") 4 | 5 | local useFreshCallback = require("../../Hooks/useFreshCallback") 6 | local useTheme = require("../../Hooks/useTheme") 7 | 8 | local Constants = require("./Constants") 9 | local Types = require("./Types") 10 | 11 | local ARROW_IMAGE = "rbxassetid://6677623152" 12 | local SCROLLBAR_THICKNESS = Constants.ScrollBarThickness 13 | local ARROW_SCROLL_AMOUNT = Constants.ArrowScrollAmount 14 | 15 | local function getArrowImageOffset(orientation: Types.BarOrientation, side: number) 16 | if orientation == "Vertical" then 17 | return Vector2.new(0, side * SCROLLBAR_THICKNESS) 18 | end 19 | return Vector2.new(SCROLLBAR_THICKNESS, side * SCROLLBAR_THICKNESS) 20 | end 21 | 22 | local function getScrollVector(orientation: Types.BarOrientation, side: number) 23 | local scrollAmount = ARROW_SCROLL_AMOUNT * (if side == 0 then -1 else 1) 24 | local scrollVector = Vector2.new(0, scrollAmount) 25 | if orientation == "Horizontal" then 26 | scrollVector = Vector2.new(scrollAmount, 0) 27 | end 28 | return scrollVector 29 | end 30 | 31 | type ScrollBarArrowProps = { 32 | BumpScroll: (scrollVector: Vector2) -> (), 33 | Orientation: Types.BarOrientation, 34 | Side: number, 35 | Position: UDim2?, 36 | AnchorPoint: Vector2?, 37 | Disabled: boolean?, 38 | } 39 | 40 | local function ScrollBarArrow(props: ScrollBarArrowProps) 41 | local theme = useTheme() 42 | local connection = React.useRef(nil) :: { current: RBXScriptConnection? } 43 | 44 | local pressed, setPressed = React.useState(false) 45 | local hovered, setHovered = React.useState(false) 46 | 47 | local modifier = Enum.StudioStyleGuideModifier.Default 48 | if props.Disabled then 49 | modifier = Enum.StudioStyleGuideModifier.Disabled 50 | elseif pressed then 51 | modifier = Enum.StudioStyleGuideModifier.Pressed 52 | end 53 | 54 | local maybeScroll = useFreshCallback(function() 55 | if hovered then 56 | props.BumpScroll(getScrollVector(props.Orientation, props.Side)) 57 | end 58 | end, { hovered, props.BumpScroll, props.Orientation, props.Side } :: { unknown }) 59 | 60 | local function startHolding() 61 | if connection.current then 62 | connection.current:Disconnect() 63 | end 64 | local nextScroll = os.clock() + 0.35 65 | connection.current = RunService.PostSimulation:Connect(function() 66 | if os.clock() >= nextScroll then 67 | maybeScroll() 68 | nextScroll += 0.05 69 | end 70 | end) 71 | props.BumpScroll(getScrollVector(props.Orientation, props.Side)) 72 | end 73 | 74 | local function stopHolding() 75 | if connection.current then 76 | connection.current:Disconnect() 77 | connection.current = nil 78 | end 79 | end 80 | React.useEffect(stopHolding, {}) 81 | 82 | React.useEffect(function() 83 | if props.Disabled and pressed then 84 | stopHolding() 85 | setPressed(false) 86 | end 87 | if props.Disabled then 88 | setHovered(false) 89 | end 90 | end, { props.Disabled, pressed }) 91 | 92 | local hostClass = "ImageLabel" 93 | local hostProps = { 94 | BackgroundColor3 = theme:GetColor(Enum.StudioStyleGuideColor.ScrollBar, modifier), 95 | BorderColor3 = theme:GetColor(Enum.StudioStyleGuideColor.Border, modifier), 96 | Size = UDim2.fromOffset(SCROLLBAR_THICKNESS, SCROLLBAR_THICKNESS), 97 | Image = ARROW_IMAGE, 98 | ImageRectSize = Vector2.new(SCROLLBAR_THICKNESS, SCROLLBAR_THICKNESS), 99 | ImageRectOffset = getArrowImageOffset(props.Orientation, props.Side), 100 | ImageColor3 = theme:GetColor(Enum.StudioStyleGuideColor.TitlebarText, modifier), 101 | Position = props.Position, 102 | AnchorPoint = props.AnchorPoint, 103 | } 104 | 105 | if props.Disabled ~= true then 106 | hostClass = "ImageButton" 107 | hostProps.AutoButtonColor = false 108 | hostProps[React.Event.InputBegan] = function(_, input: InputObject) 109 | if input.UserInputType == Enum.UserInputType.MouseMovement then 110 | setHovered(true) 111 | elseif input.UserInputType == Enum.UserInputType.MouseButton1 then 112 | setPressed(true) 113 | startHolding() 114 | end 115 | end 116 | hostProps[React.Event.InputEnded] = function(_, input: InputObject) 117 | if input.UserInputType == Enum.UserInputType.MouseMovement then 118 | setHovered(false) 119 | elseif input.UserInputType == Enum.UserInputType.MouseButton1 then 120 | setPressed(false) 121 | stopHolding() 122 | end 123 | end 124 | end 125 | 126 | return React.createElement(hostClass, hostProps) 127 | end 128 | 129 | return ScrollBarArrow 130 | -------------------------------------------------------------------------------- /src/Components/ScrollFrame/Types.luau: -------------------------------------------------------------------------------- 1 | export type ScrollData = { 2 | ContentSize: Vector2, 3 | WindowSize: Vector2, 4 | InnerBarSize: Vector2, 5 | BarVisible: { X: boolean, Y: boolean }, 6 | BarSize: Vector2, 7 | BarPosition: Vector2, 8 | } 9 | 10 | export type BarOrientation = "Horizontal" | "Vertical" 11 | 12 | return {} 13 | -------------------------------------------------------------------------------- /src/Components/Slider.luau: -------------------------------------------------------------------------------- 1 | --[=[ 2 | @class Slider 3 | 4 | A component for selecting a numeric value from a range of values with an optional increment. 5 | These are seen in some number-valued properties in the built-in Properties widget, as well as in 6 | various built-in plugins such as the Terrain Editor. 7 | 8 | | Dark | Light | 9 | | - | - | 10 | | ![Dark](/StudioComponents/components/slider/dark.png) | ![Light](/StudioComponents/components/slider/light.png) | 11 | 12 | As with other components in this library, this is a controlled component. You should pass a 13 | value to the `Value` prop representing the current value, as well as a callback to the `OnChanged` 14 | prop which will be run when the user changes the value via dragging or clicking on the slider. 15 | 16 | In addition to these, you must also provide a `Min` and a `Max` prop, which together define the 17 | range of the slider. Optionally, a `Step` prop can be provided, which defines the increment of 18 | the slider. This defaults to 0, which allows any value in the range. For a complete example: 19 | 20 | ```lua 21 | local function MyComponent() 22 | local value, setValue = React.useState(1) 23 | return React.createElement(StudioComponents.Slider, { 24 | Value = value, 25 | OnChanged = setValue, 26 | Min = 0, 27 | Max = 10, 28 | Step = 1, 29 | }) 30 | end 31 | ``` 32 | 33 | Optionally, an `OnCompleted` callback prop can be provided. This will be called with the latest 34 | value of the Slider when sliding is finished. It is also called if, while sliding is in progress, 35 | the component becomes Disabled via props or is unmounted. 36 | 37 | Two further props can optionally be provided: 38 | 1. `Border` determines whether a border is drawn around the component. 39 | This is useful for giving visual feedback when the slider is hovered or selected. 40 | 2. `Background` determines whether the component has a visible background. 41 | If this is value is missing or set to `false`, any border will also be hidden. 42 | 43 | Both of these props default to `true`. 44 | 45 | By default, the height of sliders is equal to the value found in [Constants.DefaultSliderHeight]. 46 | While this can be overriden by props, in order to keep inputs accessible it is not recommended 47 | to make the component any smaller than this. 48 | ]=] 49 | 50 | local React = require("@pkg/@jsdotlua/react") 51 | 52 | local CommonProps = require("../CommonProps") 53 | local Constants = require("../Constants") 54 | 55 | local useFreshCallback = require("../Hooks/useFreshCallback") 56 | local useMouseDrag = require("../Hooks/useMouseDrag") 57 | local useTheme = require("../Hooks/useTheme") 58 | 59 | --[=[ 60 | @within Slider 61 | @interface Props 62 | @tag Component Props 63 | 64 | @field ... CommonProps 65 | @field Value number 66 | @field OnChanged ((newValue: number) -> ())? 67 | @field OnCompleted ((newValue: number) -> ())? 68 | @field Min number 69 | @field Max number 70 | @field Step number? 71 | @field Border boolean? 72 | @field Background boolean? 73 | ]=] 74 | 75 | type SliderProps = CommonProps.T & { 76 | Value: number, 77 | OnChanged: ((newValue: number) -> ())?, 78 | OnCompleted: ((newValue: number) -> ())?, 79 | Min: number, 80 | Max: number, 81 | Step: number?, 82 | Border: boolean?, 83 | Background: boolean?, 84 | } 85 | 86 | local PADDING_BAR_SIDE = 3 87 | local PADDING_REGION_TOP = 1 88 | local PADDING_REGION_SIDE = 6 89 | 90 | local INPUT_MOVE = Enum.UserInputType.MouseMovement 91 | 92 | local function Slider(props: SliderProps) 93 | local theme = useTheme() 94 | 95 | local onChanged: (number) -> () = props.OnChanged or function() end 96 | 97 | local dragCallback = function(rbx: GuiObject, input: InputObject) 98 | local regionPos = rbx.AbsolutePosition.X + PADDING_REGION_SIDE 99 | local regionSize = rbx.AbsoluteSize.X - PADDING_REGION_SIDE * 2 100 | local inputPos = input.Position.X 101 | 102 | local alpha = (inputPos - regionPos) / regionSize 103 | local step = props.Step or 0 104 | 105 | local value = props.Min * (1 - alpha) + props.Max * alpha 106 | if step > 0 then 107 | value = math.round(value / step) * step 108 | end 109 | value = math.clamp(value, props.Min, props.Max) 110 | if value ~= props.Value then 111 | onChanged(value) 112 | end 113 | end 114 | 115 | local dragEndedCallback = useFreshCallback(function() 116 | if props.OnCompleted then 117 | props.OnCompleted(props.Value) 118 | end 119 | end, { props.Value, props.OnCompleted } :: { unknown }) 120 | 121 | local dragDeps = { props.Value, props.Min, props.Max, props.Step, props.OnCompleted, onChanged } :: { unknown } 122 | local drag = useMouseDrag(dragCallback, dragDeps, nil, dragEndedCallback) 123 | 124 | local hovered, setHovered = React.useState(false) 125 | local mainModifier = Enum.StudioStyleGuideModifier.Default 126 | if props.Disabled then 127 | mainModifier = Enum.StudioStyleGuideModifier.Disabled 128 | end 129 | 130 | local handleModifier = Enum.StudioStyleGuideModifier.Default 131 | if props.Disabled then 132 | handleModifier = Enum.StudioStyleGuideModifier.Disabled 133 | elseif hovered or drag.isActive() then 134 | handleModifier = Enum.StudioStyleGuideModifier.Hover 135 | end 136 | 137 | local handleFill = theme:GetColor(Enum.StudioStyleGuideColor.Button, handleModifier) 138 | local handleBorder = theme:GetColor(Enum.StudioStyleGuideColor.Border, handleModifier) 139 | 140 | React.useEffect(function() 141 | if props.Disabled and drag.isActive() then 142 | drag.cancel() 143 | end 144 | end, { props.Disabled, drag.isActive() }) 145 | 146 | local function inputBegan(rbx: Frame, input: InputObject) 147 | if input.UserInputType == INPUT_MOVE then 148 | setHovered(true) 149 | end 150 | if not props.Disabled then 151 | drag.onInputBegan(rbx, input) 152 | end 153 | end 154 | 155 | local function inputChanged(rbx: Frame, input: InputObject) 156 | if not props.Disabled then 157 | drag.onInputChanged(rbx, input) 158 | end 159 | end 160 | 161 | local function inputEnded(rbx: Frame, input: InputObject) 162 | if input.UserInputType == INPUT_MOVE then 163 | setHovered(false) 164 | end 165 | if not props.Disabled then 166 | drag.onInputEnded(rbx, input) 167 | end 168 | end 169 | 170 | -- if we use a Frame here, the 2d studio selection rectangle will appear when dragging 171 | -- we could prevent that using Active = true, but that displays the Click cursor 172 | -- ... the best workaround is a TextButton with Active = false 173 | return React.createElement("TextButton", { 174 | Text = "", 175 | Active = false, 176 | AutoButtonColor = false, 177 | Size = props.Size or UDim2.new(1, 0, 0, Constants.DefaultSliderHeight), 178 | Position = props.Position, 179 | AnchorPoint = props.AnchorPoint, 180 | LayoutOrder = props.LayoutOrder, 181 | ZIndex = props.ZIndex, 182 | BackgroundColor3 = theme:GetColor(Enum.StudioStyleGuideColor.InputFieldBackground, mainModifier), 183 | BorderColor3 = theme:GetColor(Enum.StudioStyleGuideColor.InputFieldBorder, handleModifier), 184 | BorderMode = Enum.BorderMode.Inset, 185 | BorderSizePixel = if props.Border == false then 0 else 1, 186 | BackgroundTransparency = if props.Background == false then 1 else 0, 187 | [React.Event.InputBegan] = inputBegan, 188 | [React.Event.InputChanged] = inputChanged, 189 | [React.Event.InputEnded] = inputEnded, 190 | }, { 191 | Bar = React.createElement("Frame", { 192 | ZIndex = 1, 193 | AnchorPoint = Vector2.new(0, 0.5), 194 | Position = UDim2.new(0, PADDING_BAR_SIDE, 0.5, 0), 195 | Size = UDim2.new(1, -PADDING_BAR_SIDE * 2, 0, 2), 196 | BorderSizePixel = 0, 197 | BackgroundTransparency = props.Disabled and 0.4 or 0, 198 | BackgroundColor3 = theme:GetColor( 199 | -- surprising values, but provides correct colors 200 | Enum.StudioStyleGuideColor.TitlebarText, 201 | Enum.StudioStyleGuideModifier.Disabled 202 | ), 203 | }), 204 | HandleRegion = React.createElement("Frame", { 205 | ZIndex = 2, 206 | Position = UDim2.fromOffset(PADDING_REGION_SIDE, PADDING_REGION_TOP), 207 | Size = UDim2.new(1, -PADDING_REGION_SIDE * 2, 1, -PADDING_REGION_TOP * 2), 208 | BackgroundTransparency = 1, 209 | }, { 210 | Handle = React.createElement("Frame", { 211 | AnchorPoint = Vector2.new(0.5, 0), 212 | Position = UDim2.fromScale((props.Value - props.Min) / (props.Max - props.Min), 0), 213 | Size = UDim2.new(0, 10, 1, 0), 214 | BorderMode = Enum.BorderMode.Inset, 215 | BorderSizePixel = 1, 216 | BorderColor3 = handleBorder:Lerp(handleFill, props.Disabled and 0.5 or 0), 217 | BackgroundColor3 = handleFill, 218 | }), 219 | }), 220 | }) 221 | end 222 | 223 | return Slider 224 | -------------------------------------------------------------------------------- /src/Components/Splitter.luau: -------------------------------------------------------------------------------- 1 | --[=[ 2 | @class Splitter 3 | 4 | A container frame similar to a [Background] but split into two panels, with a draggable control 5 | for resizing the panels within the container. Resizing one section to be larger will reduce the 6 | size of the other section, and vice versa. This is useful for letting users resize content. 7 | 8 | | Dark | Light | 9 | | - | - | 10 | | ![Dark](/StudioComponents/components/splitter/dark.png) | ![Light](/StudioComponents/components/splitter/light.png) | 11 | 12 | This is a controlled component. The current split location should be passed as a number between 13 | 0 and 1 to the `Alpha` prop, and a callback should be passed to the `OnChanged` prop, which 14 | is run with the new alpha value when the user uses the splitter. 15 | 16 | You can also optionally provide `MinAlpha` and `MaxAlpha` props (numbers between 0 and 1) which 17 | limit the resizing. These values default to 0.1 and 0.9. 18 | 19 | To render children in each side, use the `children` parameters in createElement and provide the 20 | keys `Side0` and `Side1`. For a complete example: 21 | 22 | ```lua 23 | local function MyComponent() 24 | local division, setDivision = React.useState(0.5) 25 | return React.createElement(StudioComponents.Splitter, { 26 | Alpha = division, 27 | OnChanged = setDivision, 28 | }, { 29 | Side0 = React.createElement(...), 30 | Side1 = React.createElement(...), 31 | }) 32 | end 33 | ``` 34 | 35 | By default, the split is horizontal, which means that the frame is split into a left and right 36 | side. This can be changed, for example to a vertical split (top and bottom), by providing an 37 | [Enum.FillDirection] value to the `FillDirection` prop. 38 | 39 | This component can use your system's splitter mouse icons when interacting with the splitter bar. 40 | To enable this behavior, ensure you have rendered a [PluginProvider] somewhere higher up in 41 | the tree. 42 | ]=] 43 | 44 | local React = require("@pkg/@jsdotlua/react") 45 | 46 | local CommonProps = require("../CommonProps") 47 | 48 | local useMouseDrag = require("../Hooks/useMouseDrag") 49 | local useMouseIcon = require("../Hooks/useMouseIcon") 50 | local useTheme = require("../Hooks/useTheme") 51 | 52 | local function flipVector2(vector: Vector2, shouldFlip: boolean) 53 | return if shouldFlip then Vector2.new(vector.Y, vector.X) else vector 54 | end 55 | 56 | local function flipUDim2(udim: UDim2, shouldFlip: boolean) 57 | return if shouldFlip then UDim2.new(udim.Height, udim.Width) else udim 58 | end 59 | 60 | local HANDLE_THICKNESS = 6 61 | local DEFAULT_MIN_ALPHA = 0.1 62 | local DEFAULT_MAX_ALPHA = 0.9 63 | 64 | --[=[ 65 | @within Splitter 66 | @interface Props 67 | @tag Component Props 68 | 69 | @field ... CommonProps 70 | 71 | @field Alpha number 72 | @field OnChanged ((newAlpha: number) -> ())? 73 | @field FillDirection Enum.FillDirection? 74 | @field MinAlpha number? 75 | @field MaxAlpha number? 76 | 77 | @field children { Side0: React.ReactNode?, Side1: React.ReactNode? }? 78 | ]=] 79 | 80 | export type SplitterProps = CommonProps.T & { 81 | Alpha: number, 82 | OnChanged: ((newAlpha: number) -> ())?, 83 | FillDirection: Enum.FillDirection?, 84 | MinAlpha: number?, 85 | MaxAlpha: number?, 86 | children: { 87 | Side0: React.ReactNode, 88 | Side1: React.ReactNode, 89 | }?, 90 | } 91 | 92 | local icons = { 93 | [Enum.FillDirection.Horizontal] = "rbxasset://SystemCursors/SplitEW", 94 | [Enum.FillDirection.Vertical] = "rbxasset://SystemCursors/SplitNS", 95 | } 96 | 97 | local function Splitter(props: SplitterProps) 98 | local theme = useTheme() 99 | local mouseIcon = useMouseIcon() 100 | 101 | local fillDirection = props.FillDirection or Enum.FillDirection.Horizontal 102 | local children = props.children or { 103 | Side0 = nil, 104 | Side1 = nil, 105 | } 106 | 107 | local drag = useMouseDrag(function(bar: GuiObject, input: InputObject) 108 | local region = bar.Parent :: Frame 109 | local position = Vector2.new(input.Position.X, input.Position.Y) 110 | local alpha = (position - region.AbsolutePosition) / region.AbsoluteSize 111 | alpha = alpha:Max(Vector2.one * (props.MinAlpha or DEFAULT_MIN_ALPHA)) 112 | alpha = alpha:Min(Vector2.one * (props.MaxAlpha or DEFAULT_MAX_ALPHA)) 113 | if props.OnChanged then 114 | if fillDirection == Enum.FillDirection.Horizontal and alpha.X ~= props.Alpha then 115 | props.OnChanged(alpha.X) 116 | elseif fillDirection == Enum.FillDirection.Vertical and alpha.Y ~= props.Alpha then 117 | props.OnChanged(alpha.Y) 118 | end 119 | end 120 | end, { props.Alpha, props.OnChanged, props.MinAlpha, props.MaxAlpha, fillDirection } :: { unknown }) 121 | 122 | React.useEffect(function() 123 | if props.Disabled and drag.isActive() then 124 | drag.cancel() 125 | end 126 | end, { props.Disabled, drag.isActive() }) 127 | 128 | local hovered, setHovered = React.useState(false) 129 | 130 | React.useEffect(function() 131 | if (hovered or drag.isActive()) and not props.Disabled then 132 | local icon = icons[fillDirection] 133 | mouseIcon.setIcon(icon) 134 | else 135 | mouseIcon.clearIcon() 136 | end 137 | end, { mouseIcon, hovered, drag.isActive(), props.Disabled, fillDirection } :: { unknown }) 138 | 139 | local function onInputBegan(rbx: Frame, input: InputObject) 140 | if input.UserInputType == Enum.UserInputType.MouseMovement then 141 | setHovered(true) 142 | end 143 | if not props.Disabled then 144 | drag.onInputBegan(rbx, input) 145 | end 146 | end 147 | local function onInputChanged(rbx: Frame, input: InputObject) 148 | if not props.Disabled then 149 | drag.onInputChanged(rbx, input) 150 | end 151 | end 152 | local function onInputEnded(rbx: Frame, input: InputObject) 153 | if input.UserInputType == Enum.UserInputType.MouseMovement then 154 | setHovered(false) 155 | end 156 | if not props.Disabled then 157 | drag.onInputEnded(rbx, input) 158 | end 159 | end 160 | 161 | local shouldFlip = fillDirection == Enum.FillDirection.Vertical 162 | local alpha = props.Alpha 163 | alpha = math.max(alpha, props.MinAlpha or DEFAULT_MIN_ALPHA) 164 | alpha = math.min(alpha, props.MaxAlpha or DEFAULT_MAX_ALPHA) 165 | 166 | local handleTransparency = if props.Disabled then 0.75 else 0 167 | local handleColorStyle = Enum.StudioStyleGuideColor.DialogButton 168 | if props.Disabled then 169 | handleColorStyle = Enum.StudioStyleGuideColor.Border 170 | end 171 | 172 | return React.createElement("Frame", { 173 | AnchorPoint = props.AnchorPoint, 174 | Position = props.Position, 175 | Size = props.Size or UDim2.fromScale(1, 1), 176 | LayoutOrder = props.LayoutOrder, 177 | ZIndex = props.ZIndex, 178 | BackgroundTransparency = 1, 179 | }, { 180 | Handle = React.createElement("Frame", { 181 | Active = true, -- prevents the drag-box when in coregui mode 182 | AnchorPoint = flipVector2(Vector2.new(0.5, 0), shouldFlip), 183 | Position = flipUDim2(UDim2.fromScale(alpha, 0), shouldFlip), 184 | Size = flipUDim2(UDim2.new(0, HANDLE_THICKNESS, 1, 0), shouldFlip), 185 | BackgroundTransparency = handleTransparency, 186 | BackgroundColor3 = theme:GetColor(handleColorStyle), 187 | BorderSizePixel = 0, 188 | [React.Event.InputBegan] = onInputBegan, 189 | [React.Event.InputChanged] = onInputChanged, 190 | [React.Event.InputEnded] = onInputEnded, 191 | ZIndex = 1, 192 | }, { 193 | LeftBorder = not props.Disabled and React.createElement("Frame", { 194 | Size = flipUDim2(UDim2.new(0, 1, 1, 0), shouldFlip), 195 | BackgroundColor3 = theme:GetColor(Enum.StudioStyleGuideColor.Border), 196 | BorderSizePixel = 0, 197 | }), 198 | RightBorder = not props.Disabled and React.createElement("Frame", { 199 | Position = flipUDim2(UDim2.new(1, -1, 0, 0), shouldFlip), 200 | Size = flipUDim2(UDim2.new(0, 1, 1, 0), shouldFlip), 201 | BackgroundColor3 = theme:GetColor(Enum.StudioStyleGuideColor.Border), 202 | BorderSizePixel = 0, 203 | }), 204 | }), 205 | Side0 = React.createElement("Frame", { 206 | Size = flipUDim2(UDim2.new(alpha, -math.floor(HANDLE_THICKNESS / 2), 1, 0), shouldFlip), 207 | BackgroundTransparency = 1, 208 | ClipsDescendants = true, 209 | ZIndex = 0, 210 | }, { 211 | Child = children.Side0, 212 | }), 213 | Side1 = React.createElement("Frame", { 214 | AnchorPoint = flipVector2(Vector2.new(1, 0), shouldFlip), 215 | Position = flipUDim2(UDim2.fromScale(1, 0), shouldFlip), 216 | Size = flipUDim2(UDim2.new(1 - alpha, -math.ceil(HANDLE_THICKNESS / 2), 1, 0), shouldFlip), 217 | BackgroundTransparency = 1, 218 | ClipsDescendants = true, 219 | ZIndex = 0, 220 | }, { 221 | Child = children.Side1, 222 | }), 223 | }) 224 | end 225 | 226 | return Splitter 227 | -------------------------------------------------------------------------------- /src/Components/TabContainer.luau: -------------------------------------------------------------------------------- 1 | --[=[ 2 | @class TabContainer 3 | 4 | A container that displays one content page at a time, where different pages can be selected 5 | via a set of tabs along the top. This is seen in some built-in plugins such as the Toolbox. 6 | 7 | | Dark | Light | 8 | | - | - | 9 | | ![Dark](/StudioComponents/components/tabcontainer/dark.png) | ![Light](/StudioComponents/components/tabcontainer/light.png) | 10 | 11 | This is a controlled component. The identifier of the selected tab should be passed to the 12 | `SelectedTab` prop, and a callback should be passed to the `OnTabSelected` prop which is run 13 | when the user selects a tab from the tab controls along the top. 14 | 15 | The content rendered in each tab's main window should be passed to the `children` parameters in 16 | `createElement` in the [format](TabContainer#Tab) described below. The keys are used as tab names 17 | in the tab controls along the top and should also correspond to the identifier in `SelectedTab` 18 | and the identifiers that `OnTabSelected` prop may be called with. For example: 19 | 20 | ```lua 21 | local function MyComponent() 22 | local selectedTab, setSelectedTab = React.useState("Models") 23 | return React.createElement(TabContainer, { 24 | SelectedTab = selectedTab, 25 | OnTabSelected = setSelectedTab, 26 | }, { 27 | ["Models"] = { 28 | LayoutOrder = 1, 29 | Content = React.createElement(...), 30 | }, 31 | ["Decals"] = { 32 | LayoutOrder = 2, 33 | Content = React.createElement(...), 34 | } 35 | }) 36 | end 37 | ``` 38 | 39 | To override the text displayed on a tab, assign a value to the optional 40 | `DisplayTitle` field in the tab entry; see the "Comments" example below: 41 | ```lua 42 | local function MyPluginApp(props: { 43 | comments: { string } 44 | }) 45 | local selectedTab, setSelectedTab = React.useState("Comments") 46 | local commentsArray = props.comments 47 | 48 | return React.createElement(TabContainer, { 49 | SelectedTab = selectedTab, 50 | OnTabSelected = setSelectedTab, 51 | }, { 52 | ["Comments"] = { 53 | LayoutOrder = 1, 54 | DisplayTitle = `Comments ({#commentsArray})`, 55 | Content = React.createElement(...), 56 | }, 57 | ["Settings"] = { 58 | LayoutOrder = 2, 59 | Content = React.createElement(...), 60 | } 61 | }) 62 | end 63 | ``` 64 | 65 | As well as disabling the entire component via the `Disabled` [CommonProp](CommonProps), individual 66 | tabs can be disabled and made unselectable by passing `Disabled` with a value of `true` inside 67 | the tab's entry in the `Tabs` prop table. 68 | 69 | :::info 70 | The various tab containers found in Studio are inconsistent with each other (for example, Toolbox 71 | and Terrain Editor use different sizes, colors, and highlights). This design of this component 72 | uses the common elements of those designs and has small tweaks to stay consistent with the wider 73 | design of Studio elements. 74 | ::: 75 | ]=] 76 | 77 | local React = require("@pkg/@jsdotlua/react") 78 | 79 | local CommonProps = require("../CommonProps") 80 | local useTheme = require("../Hooks/useTheme") 81 | 82 | local TAB_HEIGHT = 30 83 | 84 | --[=[ 85 | @within TabContainer 86 | @interface Tab 87 | 88 | @field LayoutOrder number 89 | @field DisplayTitle string? 90 | @field Content React.ReactNode 91 | @field Disabled boolean? 92 | ]=] 93 | 94 | type Tab = { 95 | Content: React.ReactNode, 96 | LayoutOrder: number, 97 | DisplayTitle: string?, 98 | Disabled: boolean?, 99 | } 100 | 101 | --[=[ 102 | @within TabContainer 103 | @interface Props 104 | @tag Component Props 105 | 106 | @field ... CommonProps 107 | @field SelectedTab string 108 | @field OnTabSelected ((name: string) -> ())? 109 | @field children { [string]: Tab } 110 | ]=] 111 | 112 | type TabContainerProps = CommonProps.T & { 113 | SelectedTab: string, 114 | OnTabSelected: ((name: string) -> ())?, 115 | children: { [string]: Tab }?, 116 | } 117 | 118 | local function TabButton(props: { 119 | Size: UDim2, 120 | Text: string, 121 | LayoutOrder: number, 122 | Selected: boolean, 123 | OnActivated: () -> (), 124 | Disabled: boolean?, 125 | }) 126 | local theme = useTheme() 127 | 128 | local hovered, setHovered = React.useState(false) 129 | local pressed, setPressed = React.useState(false) 130 | 131 | local onInputBegan = function(_, input) 132 | if input.UserInputType == Enum.UserInputType.MouseButton1 then 133 | setPressed(true) 134 | elseif input.UserInputType == Enum.UserInputType.MouseMovement then 135 | setHovered(true) 136 | end 137 | end 138 | 139 | local onInputEnded = function(_, input) 140 | if input.UserInputType == Enum.UserInputType.MouseButton1 then 141 | setPressed(false) 142 | elseif input.UserInputType == Enum.UserInputType.MouseMovement then 143 | setHovered(false) 144 | end 145 | end 146 | 147 | local backgroundStyle = Enum.StudioStyleGuideColor.Button 148 | if props.Selected then 149 | backgroundStyle = Enum.StudioStyleGuideColor.MainBackground 150 | elseif pressed and not props.Disabled then 151 | backgroundStyle = Enum.StudioStyleGuideColor.ButtonBorder 152 | end 153 | 154 | local modifier = Enum.StudioStyleGuideModifier.Default 155 | if props.Disabled then 156 | modifier = Enum.StudioStyleGuideModifier.Disabled 157 | elseif props.Selected then 158 | modifier = Enum.StudioStyleGuideModifier.Pressed 159 | elseif hovered then 160 | modifier = Enum.StudioStyleGuideModifier.Hover 161 | end 162 | 163 | local indicatorModifier = Enum.StudioStyleGuideModifier.Default 164 | if props.Disabled then 165 | indicatorModifier = Enum.StudioStyleGuideModifier.Disabled 166 | end 167 | 168 | return React.createElement("TextButton", { 169 | AutoButtonColor = false, 170 | BackgroundColor3 = theme:GetColor(backgroundStyle, modifier), 171 | BorderColor3 = theme:GetColor(Enum.StudioStyleGuideColor.Border, modifier), 172 | LayoutOrder = props.LayoutOrder, 173 | Size = props.Size, 174 | Text = props.Text, 175 | Font = Enum.Font.SourceSans, 176 | TextColor3 = theme:GetColor(Enum.StudioStyleGuideColor.MainText, modifier), 177 | TextTruncate = Enum.TextTruncate.AtEnd, 178 | TextSize = 14, 179 | [React.Event.InputBegan] = onInputBegan, 180 | [React.Event.InputEnded] = onInputEnded, 181 | [React.Event.Activated] = function() 182 | if not props.Disabled then 183 | props.OnActivated() 184 | end 185 | end, 186 | }, { 187 | Indicator = props.Selected and React.createElement("Frame", { 188 | BackgroundColor3 = theme:GetColor(Enum.StudioStyleGuideColor.DialogMainButton, indicatorModifier), 189 | BorderSizePixel = 0, 190 | Size = UDim2.new(1, 0, 0, 2), 191 | }), 192 | Under = props.Selected and React.createElement("Frame", { 193 | BackgroundColor3 = theme:GetColor(backgroundStyle, modifier), 194 | BorderSizePixel = 0, 195 | Size = UDim2.new(1, 0, 0, 1), 196 | Position = UDim2.fromScale(0, 1), 197 | }), 198 | }) 199 | end 200 | 201 | local function TabContainer(props: TabContainerProps) 202 | local theme = useTheme() 203 | 204 | local children = props.children :: { [string]: Tab } 205 | local tabs: { [string]: React.ReactNode } = {} 206 | local count = 0 207 | for _ in children do 208 | count += 1 209 | end 210 | 211 | for name, tab in children do 212 | local isSelectedTab = props.SelectedTab == name 213 | tabs[name] = React.createElement(TabButton, { 214 | Size = UDim2.fromScale(1 / count, 1), 215 | LayoutOrder = tab.LayoutOrder, 216 | Text = tab.DisplayTitle or name, 217 | Selected = isSelectedTab, 218 | Disabled = tab.Disabled == true or props.Disabled == true, 219 | OnActivated = function() 220 | if props.OnTabSelected then 221 | props.OnTabSelected(name) 222 | end 223 | end, 224 | }) 225 | end 226 | 227 | local tab = children[props.SelectedTab] 228 | local content = if tab then tab.Content else nil 229 | 230 | local modifier = Enum.StudioStyleGuideModifier.Default 231 | if props.Disabled then 232 | modifier = Enum.StudioStyleGuideModifier.Disabled 233 | end 234 | 235 | return React.createElement("Frame", { 236 | BackgroundColor3 = theme:GetColor(Enum.StudioStyleGuideColor.MainBackground, modifier), 237 | BorderColor3 = theme:GetColor(Enum.StudioStyleGuideColor.Border, modifier), 238 | AnchorPoint = props.AnchorPoint, 239 | Position = props.Position, 240 | Size = props.Size or UDim2.fromScale(1, 1), 241 | LayoutOrder = props.LayoutOrder, 242 | ZIndex = props.ZIndex, 243 | }, { 244 | Top = React.createElement("Frame", { 245 | ZIndex = 2, 246 | Size = UDim2.new(1, 0, 0, TAB_HEIGHT), 247 | BackgroundTransparency = 1, 248 | }, { 249 | TabsContainer = React.createElement("Frame", { 250 | Size = UDim2.fromScale(1, 1), 251 | BackgroundTransparency = 1, 252 | }, { 253 | Layout = React.createElement("UIListLayout", { 254 | SortOrder = Enum.SortOrder.LayoutOrder, 255 | FillDirection = Enum.FillDirection.Horizontal, 256 | }), 257 | }, tabs), 258 | }), 259 | Content = React.createElement("Frame", { 260 | ZIndex = 1, 261 | AnchorPoint = Vector2.new(0, 1), 262 | Position = UDim2.fromScale(0, 1), 263 | Size = UDim2.new(1, 0, 1, -TAB_HEIGHT - 1), -- extra px for outer border 264 | BackgroundTransparency = 1, 265 | ClipsDescendants = true, 266 | }, content), 267 | }) 268 | end 269 | 270 | return TabContainer 271 | -------------------------------------------------------------------------------- /src/Components/TextInput.luau: -------------------------------------------------------------------------------- 1 | --[=[ 2 | @class TextInput 3 | 4 | A basic input field for entering any kind of text. This matches the appearance of the search 5 | boxes in the Explorer and Properties widgets, among other inputs in Studio. 6 | 7 | | Dark | Light | 8 | | - | - | 9 | | ![Dark](/StudioComponents/components/textinput/dark.png) | ![Light](/StudioComponents/components/textinput/light.png) | 10 | 11 | This is a controlled component, which means the current text should be passed in to the 12 | `Text` prop and a callback value to the `OnChanged` prop which gets run when the user attempts 13 | types in the input field. For example: 14 | 15 | ```lua 16 | local function MyComponent() 17 | local text, setText = React.useState("") 18 | return React.createElement(StudioComponents.TextInput, { 19 | Text = text, 20 | OnChanged = setText, 21 | }) 22 | end 23 | ``` 24 | 25 | This allows complete control over the text displayed and keeps the source of truth in your own 26 | code. This is helpful for consistency and controlling the state from elsewhere in the tree. It 27 | also allows you to easily filter what can be typed into the text input. For example, to only 28 | permit entering lowercase letters: 29 | 30 | ```lua 31 | local function MyComponent() 32 | local text, setText = React.useState("") 33 | return React.createElement(StudioComponents.TextInput, { 34 | Text = text, 35 | OnChanged = function(newText), 36 | local filteredText = string.gsub(newText, "[^a-z]", "") 37 | setText(filteredText) 38 | end, 39 | }) 40 | end 41 | ``` 42 | 43 | By default, the height of this component is equal to the value in [Constants.DefaultInputHeight]. 44 | While this can be overriden by props, in order to keep inputs accessible it is not recommended 45 | to make the component any smaller than this. 46 | ]=] 47 | 48 | local React = require("@pkg/@jsdotlua/react") 49 | 50 | local BaseTextInput = require("./Foundation/BaseTextInput") 51 | local Constants = require("../Constants") 52 | 53 | --[=[ 54 | @within TextInput 55 | @interface Props 56 | @tag Component Props 57 | 58 | @field ... CommonProps 59 | 60 | @field Text string 61 | @field OnChanged ((newText: string) -> ())? 62 | 63 | @field PlaceholderText string? 64 | @field ClearTextOnFocus boolean? 65 | @field OnFocused (() -> ())? 66 | @field OnFocusLost ((text: string, enterPressed: boolean, input: InputObject) -> ())? 67 | ]=] 68 | 69 | type TextInputProps = BaseTextInput.BaseTextInputConsumerProps & { 70 | Text: string, 71 | OnChanged: ((newText: string) -> ())?, 72 | } 73 | 74 | local function TextInput(props: TextInputProps) 75 | return React.createElement(BaseTextInput, { 76 | AnchorPoint = props.AnchorPoint, 77 | Position = props.Position, 78 | Size = props.Size or UDim2.new(1, 0, 0, Constants.DefaultInputHeight), 79 | LayoutOrder = props.LayoutOrder, 80 | ZIndex = props.ZIndex, 81 | Disabled = props.Disabled, 82 | Text = props.Text, 83 | PlaceholderText = props.PlaceholderText, 84 | ClearTextOnFocus = props.ClearTextOnFocus, 85 | OnFocused = props.OnFocused, 86 | OnFocusLost = props.OnFocusLost, 87 | OnChanged = props.OnChanged or function() end, 88 | }, props.children) 89 | end 90 | 91 | return TextInput 92 | -------------------------------------------------------------------------------- /src/Constants.luau: -------------------------------------------------------------------------------- 1 | --[=[ 2 | @class Constants 3 | This module exposes values that are read from in various components. 4 | These can be used to, for example, match the appearance of custom components with components 5 | from this library. 6 | 7 | :::warning 8 | The table returned by this module is read-only. It is not a config. 9 | ::: 10 | ]=] 11 | 12 | local Constants = {} 13 | 14 | --- @within Constants 15 | --- @prop DefaultFont Font 16 | --- The default font for text. 17 | Constants.DefaultFont = Enum.Font.SourceSans 18 | 19 | --- @within Constants 20 | --- @prop DefaultTextSize number 21 | --- The default size for text. 22 | Constants.DefaultTextSize = 14 23 | 24 | --- @within Constants 25 | --- @prop DefaultButtonHeight number 26 | --- The default height of buttons. 27 | Constants.DefaultButtonHeight = 24 28 | 29 | --- @within Constants 30 | --- @prop DefaultToggleHeight number 31 | --- The default height of toggles (Checkbox and RadioButton). 32 | Constants.DefaultToggleHeight = 20 33 | 34 | --- @within Constants 35 | --- @prop DefaultInputHeight number 36 | --- The default height of text and numeric inputs. 37 | Constants.DefaultInputHeight = 22 38 | 39 | --- @within Constants 40 | --- @prop DefaultSliderHeight number 41 | --- The default height of sliders. 42 | Constants.DefaultSliderHeight = 22 43 | 44 | --- @within Constants 45 | --- @prop DefaultDropdownHeight number 46 | --- The default height of the permanent section of dropdowns. 47 | Constants.DefaultDropdownHeight = 20 48 | 49 | --- @within Constants 50 | --- @prop DefaultDropdownRowHeight number 51 | --- The default height of rows in dropdown lists. 52 | Constants.DefaultDropdownRowHeight = 16 53 | 54 | --- @within Constants 55 | --- @prop DefaultProgressBarHeight number 56 | --- The default height of progress bars. 57 | Constants.DefaultProgressBarHeight = 14 58 | 59 | --- @within Constants 60 | --- @prop DefaultColorPickerSize UDim2 61 | --- The default window size of color pickers. 62 | Constants.DefaultColorPickerSize = UDim2.fromOffset(260, 285) 63 | 64 | --- @within Constants 65 | --- @prop DefaultNumberSequencePickerSize UDim2 66 | --- The default window size of number sequence pickers. 67 | Constants.DefaultNumberSequencePickerSize = UDim2.fromOffset(425, 285) 68 | 69 | --- @within Constants 70 | --- @prop DefaultDatePickerSize UDim2 71 | --- The default window size of date pickers. 72 | Constants.DefaultDatePickerSize = UDim2.fromOffset(202, 160) 73 | 74 | return table.freeze(Constants) 75 | -------------------------------------------------------------------------------- /src/Contexts/PluginContext.luau: -------------------------------------------------------------------------------- 1 | local React = require("@pkg/@jsdotlua/react") 2 | 3 | export type PluginContext = { 4 | plugin: Plugin, 5 | pushMouseIcon: (icon: string) -> string, 6 | popMouseIcon: (id: string) -> (), 7 | } 8 | 9 | return React.createContext(nil :: PluginContext?) 10 | -------------------------------------------------------------------------------- /src/Contexts/ThemeContext.luau: -------------------------------------------------------------------------------- 1 | local React = require("@pkg/@jsdotlua/react") 2 | 3 | return React.createContext(nil :: StudioTheme?) 4 | -------------------------------------------------------------------------------- /src/Hooks/useFreshCallback.luau: -------------------------------------------------------------------------------- 1 | local React = require("@pkg/@jsdotlua/react") 2 | 3 | type Callback = (Args...) -> Rets... 4 | 5 | local function useFreshCallback( 6 | -- stylua: ignore 7 | callback: Callback, 8 | deps: { any }? 9 | ): Callback 10 | local ref = React.useRef(callback) :: { current: Callback } 11 | 12 | React.useEffect(function() 13 | ref.current = callback 14 | end, deps) 15 | 16 | return function(...) 17 | return ref.current(...) 18 | end 19 | end 20 | 21 | return useFreshCallback 22 | -------------------------------------------------------------------------------- /src/Hooks/useMouseDrag.luau: -------------------------------------------------------------------------------- 1 | local React = require("@pkg/@jsdotlua/react") 2 | 3 | local useFreshCallback = require("../Hooks/useFreshCallback") 4 | 5 | local function useMouseDrag( 6 | callback: (rbx: GuiObject, input: InputObject) -> (), 7 | deps: { any }?, 8 | onBeganCallback: ((rbx: GuiObject, input: InputObject) -> ())?, -- NB: consumer needs to guard against stale state 9 | onEndedCallback: (() -> ())? 10 | ) 11 | local freshCallback = useFreshCallback(callback, deps) 12 | 13 | -- we use a state so consumers can re-render 14 | -- ... as well as a ref so we have an immediately-updated/available value 15 | local holdingState, setHoldingState = React.useState(false) 16 | local holding = React.useRef(false) 17 | 18 | local lastRbx = React.useRef(nil :: GuiObject?) 19 | local moveInput = React.useRef(nil :: InputObject?) 20 | local moveConnection = React.useRef(nil :: RBXScriptConnection?) 21 | 22 | local function runCallback(input: InputObject) 23 | freshCallback(lastRbx.current :: GuiObject, input) 24 | end 25 | 26 | local function connect() 27 | if moveConnection.current then 28 | moveConnection.current:Disconnect() 29 | end 30 | local input = moveInput.current :: InputObject 31 | local signal = input:GetPropertyChangedSignal("Position") 32 | moveConnection.current = signal:Connect(function() 33 | runCallback(input) 34 | end) 35 | runCallback(input) 36 | end 37 | 38 | local function disconnect() 39 | if moveConnection.current then 40 | moveConnection.current:Disconnect() 41 | moveConnection.current = nil 42 | end 43 | if onEndedCallback and holding.current == true then 44 | onEndedCallback() 45 | end 46 | end 47 | 48 | -- React.useEffect(function() 49 | -- if moveInput.current then 50 | -- runCallback(moveInput.current) 51 | -- end 52 | -- end, deps) 53 | 54 | React.useEffect(function() 55 | return disconnect 56 | end, {}) 57 | 58 | local function onInputBegan(rbx: GuiObject, input: InputObject) 59 | lastRbx.current = rbx 60 | if input.UserInputType == Enum.UserInputType.MouseMovement then 61 | moveInput.current = input 62 | elseif input.UserInputType == Enum.UserInputType.MouseButton1 then 63 | holding.current = true 64 | setHoldingState(true) 65 | if onBeganCallback then 66 | onBeganCallback(rbx, input) 67 | end 68 | if moveInput.current then 69 | connect() 70 | else 71 | -- case: clicked without move input first 72 | -- this can happen if the instance moves to be under the mouse 73 | runCallback(input) 74 | end 75 | end 76 | end 77 | 78 | local function onInputChanged(rbx: GuiObject, input: InputObject) 79 | lastRbx.current = rbx 80 | if input.UserInputType == Enum.UserInputType.MouseMovement then 81 | moveInput.current = input 82 | if holding.current and not moveConnection.current then 83 | -- handles the case above and connects listener on first move 84 | connect() 85 | end 86 | end 87 | end 88 | 89 | local function onInputEnded(rbx: GuiObject, input: InputObject) 90 | lastRbx.current = rbx 91 | if input.UserInputType == Enum.UserInputType.MouseButton1 then 92 | disconnect() 93 | holding.current = false 94 | setHoldingState(false) 95 | end 96 | end 97 | 98 | local function isActive() 99 | return holdingState == true 100 | end 101 | 102 | local function cancel() 103 | disconnect() 104 | holding.current = false 105 | moveInput.current = nil 106 | setHoldingState(false) 107 | end 108 | 109 | return { 110 | isActive = isActive, 111 | cancel = cancel, 112 | onInputBegan = onInputBegan, 113 | onInputChanged = onInputChanged, 114 | onInputEnded = onInputEnded, 115 | } 116 | end 117 | 118 | return useMouseDrag 119 | -------------------------------------------------------------------------------- /src/Hooks/useMouseIcon.luau: -------------------------------------------------------------------------------- 1 | --[=[ 2 | @class useMouseIcon 3 | 4 | A hook used internally by components for setting and clearing custom mouse icons. To use this 5 | hook, you need to also render a single [PluginProvider] somewhere higher up in the tree. 6 | 7 | To set the mouse icon, use the `setIcon` function and pass an asset url. All components under 8 | the PluginProvider that use this hook share an icon stack; the most recent component to call 9 | `setIcon` will have its icon set as the final mouse icon. Calling `setIcon` twice without 10 | clearing it in between will override the previous icon set by this component. 11 | 12 | Calling `clearIcon` removes the icon set by this component from the stack, which may mean the 13 | mouse icon falls back to the next icon on the stack set by another component. Ensure you call 14 | `clearIcon` on unmount otherwise your icon may never get unset. For example: 15 | 16 | ```lua 17 | local function MyComponent() 18 | local mouseIconApi = useMouseIcon() 19 | 20 | React.useEffect(function() -- clear icon on unmount 21 | return function() 22 | mouseIconApi.clearIcon() 23 | end 24 | end, {}) 25 | 26 | return React.createElement(SomeComponent, { 27 | OnHoverStart = function() 28 | mouseIconApi.setIcon(...) -- some icon for hover 29 | end, 30 | OnHoverEnd = function() 31 | mouseIconApi.clearIcon() 32 | end 33 | }) 34 | end 35 | ``` 36 | ]=] 37 | 38 | --[=[ 39 | @within useMouseIcon 40 | @interface mouseIconApi 41 | 42 | @field setIcon (icon: string) -> () 43 | @field getIcon () -> string? 44 | @field clearIcon () -> () 45 | ]=] 46 | 47 | local React = require("@pkg/@jsdotlua/react") 48 | 49 | local PluginContext = require("../Contexts/PluginContext") 50 | local useFreshCallback = require("../Hooks/useFreshCallback") 51 | 52 | local function useMouseIcon() 53 | local plugin = React.useContext(PluginContext) 54 | 55 | local lastIconId: string?, setLastIconId = React.useState(nil :: string?) 56 | local lastIconAssetUrl: string?, setLastIconAssetUrl = React.useState(nil :: string?) 57 | 58 | local function getIcon(): string? 59 | return lastIconAssetUrl 60 | end 61 | 62 | local function setIcon(assetUrl: string) 63 | if plugin ~= nil and assetUrl ~= lastIconAssetUrl then 64 | if lastIconId ~= nil then 65 | plugin.popMouseIcon(lastIconId) 66 | end 67 | local newId = plugin.pushMouseIcon(assetUrl) 68 | setLastIconId(newId) 69 | setLastIconAssetUrl(assetUrl) 70 | end 71 | end 72 | 73 | local clearIcon = useFreshCallback(function() 74 | if plugin ~= nil and lastIconId ~= nil then 75 | plugin.popMouseIcon(lastIconId) 76 | setLastIconId(nil) 77 | setLastIconAssetUrl(nil) 78 | end 79 | end, { lastIconId }) 80 | 81 | React.useEffect(function() 82 | return clearIcon 83 | end, {}) 84 | 85 | return { 86 | getIcon = getIcon, 87 | setIcon = setIcon, 88 | clearIcon = clearIcon, 89 | } 90 | end 91 | 92 | return useMouseIcon 93 | -------------------------------------------------------------------------------- /src/Hooks/usePlugin.luau: -------------------------------------------------------------------------------- 1 | --[=[ 2 | @class usePlugin 3 | 4 | A hook used to obtain a reference to the root [plugin](https://create.roblox.com/docs/reference/engine/classes/Plugin) 5 | instance associated with the current plugin. It requires a single [PluginProvider] to be present 6 | higher up in the tree. 7 | 8 | ```lua 9 | local function MyComponent() 10 | local plugin = usePlugin() 11 | ... 12 | end 13 | ``` 14 | ]=] 15 | 16 | local React = require("@pkg/@jsdotlua/react") 17 | 18 | local PluginContext = require("../Contexts/PluginContext") 19 | 20 | local function usePlugin() 21 | local pluginContext = React.useContext(PluginContext) 22 | return pluginContext and pluginContext.plugin 23 | end 24 | 25 | return usePlugin 26 | -------------------------------------------------------------------------------- /src/Hooks/useTheme.luau: -------------------------------------------------------------------------------- 1 | --[=[ 2 | @class useTheme 3 | 4 | A hook used internally by components for reading the selected Studio Theme and thereby visually 5 | theming components appropriately. It is exposed here so that custom components can use this 6 | API to achieve the same effect. Calling the hook returns a [StudioTheme] instance. For example: 7 | 8 | ```lua 9 | local function MyThemedComponent() 10 | local theme = useTheme() 11 | local color = theme:GetColor( 12 | Enum.StudioStyleGuideColor.ScriptBackground, 13 | Enum.StudioStyleGuideModifier.Default 14 | ) 15 | return React.createElement("Frame", { 16 | BackgroundColor3 = color, 17 | ... 18 | }) 19 | end 20 | ``` 21 | ]=] 22 | 23 | local Studio = settings().Studio 24 | 25 | local React = require("@pkg/@jsdotlua/react") 26 | 27 | local ThemeContext = require("../Contexts/ThemeContext") 28 | 29 | local function useTheme() 30 | local theme = React.useContext(ThemeContext) 31 | local studioTheme, setStudioTheme = React.useState(Studio.Theme) 32 | 33 | React.useEffect(function() 34 | if theme then 35 | return 36 | end 37 | local connection = Studio.ThemeChanged:Connect(function() 38 | setStudioTheme(Studio.Theme) 39 | end) 40 | return function() 41 | connection:Disconnect() 42 | end 43 | end, { theme }) 44 | 45 | return theme or studioTheme 46 | end 47 | 48 | return useTheme 49 | -------------------------------------------------------------------------------- /src/Stories/Background.story.luau: -------------------------------------------------------------------------------- 1 | local React = require("@pkg/@jsdotlua/react") 2 | 3 | local Background = require("../Components/Background") 4 | local createStory = require("./Helpers/createStory") 5 | 6 | local function Story() 7 | return React.createElement(Background) 8 | end 9 | 10 | return createStory(Story) 11 | -------------------------------------------------------------------------------- /src/Stories/Button.story.luau: -------------------------------------------------------------------------------- 1 | local React = require("@pkg/@jsdotlua/react") 2 | 3 | local Button = require("../Components/Button") 4 | local createStory = require("./Helpers/createStory") 5 | 6 | local function StoryButton(props: { 7 | Text: string?, 8 | HasIcon: boolean?, 9 | Disabled: boolean?, 10 | }) 11 | return React.createElement(Button, { 12 | LayoutOrder = if props.Disabled then 2 else 1, 13 | Icon = props.HasIcon and { 14 | Image = "rbxassetid://18786011824", 15 | UseThemeColor = true, 16 | Size = Vector2.new(16, 16), 17 | Alignment = Enum.HorizontalAlignment.Left, 18 | RectOffset = Vector2.new(1000, 0), 19 | RectSize = Vector2.new(16, 16), 20 | } :: any, 21 | Text = props.Text, 22 | OnActivated = if not props.Disabled then function() end else nil, 23 | Disabled = props.Disabled, 24 | AutomaticSize = Enum.AutomaticSize.XY, 25 | }) 26 | end 27 | 28 | local function StoryItem(props: { 29 | LayoutOrder: number, 30 | Text: string?, 31 | HasIcon: boolean?, 32 | Disabled: boolean?, 33 | }) 34 | local height, setHeight = React.useBinding(0) 35 | 36 | return React.createElement("Frame", { 37 | Size = height:map(function(value) 38 | return UDim2.new(1, 0, 0, value) 39 | end), 40 | LayoutOrder = props.LayoutOrder, 41 | BackgroundTransparency = 1, 42 | }, { 43 | Layout = React.createElement("UIListLayout", { 44 | Padding = UDim.new(0, 10), 45 | SortOrder = Enum.SortOrder.LayoutOrder, 46 | FillDirection = Enum.FillDirection.Horizontal, 47 | HorizontalAlignment = Enum.HorizontalAlignment.Center, 48 | [React.Change.AbsoluteContentSize] = function(rbx) 49 | setHeight(rbx.AbsoluteContentSize.Y) 50 | end, 51 | }), 52 | Enabled = React.createElement(StoryButton, { 53 | Text = props.Text, 54 | HasIcon = props.HasIcon, 55 | }), 56 | Disabled = React.createElement(StoryButton, { 57 | Text = props.Text, 58 | HasIcon = props.HasIcon, 59 | Disabled = true, 60 | }), 61 | }) 62 | end 63 | 64 | local function Story() 65 | return React.createElement(React.Fragment, {}, { 66 | Icon = React.createElement(StoryItem, { 67 | LayoutOrder = 1, 68 | HasIcon = true, 69 | }), 70 | Text = React.createElement(StoryItem, { 71 | LayoutOrder = 2, 72 | Text = "Example Text", 73 | }), 74 | TextLonger = React.createElement(StoryItem, { 75 | LayoutOrder = 3, 76 | Text = "Example Longer Text", 77 | }), 78 | TextMulti = React.createElement(StoryItem, { 79 | LayoutOrder = 4, 80 | Text = "Example Text\nover two lines", 81 | }), 82 | IconTextIcon = React.createElement(StoryItem, { 83 | LayoutOrder = 5, 84 | HasIcon = true, 85 | Text = "Example Text with Icon", 86 | }), 87 | }) 88 | end 89 | 90 | return createStory(Story) 91 | -------------------------------------------------------------------------------- /src/Stories/Checkbox.story.luau: -------------------------------------------------------------------------------- 1 | local React = require("@pkg/@jsdotlua/react") 2 | 3 | local Checkbox = require("../Components/Checkbox") 4 | local createStory = require("./Helpers/createStory") 5 | 6 | local function StoryItem(props: { 7 | LayoutOrder: number, 8 | Value: boolean?, 9 | Label: string, 10 | }) 11 | return React.createElement("Frame", { 12 | Size = UDim2.new(0, 200, 0, 50), 13 | BackgroundTransparency = 1, 14 | LayoutOrder = props.LayoutOrder, 15 | }, { 16 | Layout = React.createElement("UIListLayout", { 17 | SortOrder = Enum.SortOrder.LayoutOrder, 18 | Padding = UDim.new(0, 2), 19 | VerticalAlignment = Enum.VerticalAlignment.Center, 20 | }), 21 | Enabled = React.createElement(Checkbox, { 22 | Label = props.Label, 23 | Value = props.Value, 24 | OnChanged = function() end, 25 | LayoutOrder = 1, 26 | }), 27 | Disabled = React.createElement(Checkbox, { 28 | Label = `{props.Label} (Disabled)`, 29 | Value = props.Value, 30 | OnChanged = function() end, 31 | Disabled = true, 32 | LayoutOrder = 2, 33 | }), 34 | }) 35 | end 36 | 37 | local function Story() 38 | local value, setValue = React.useState(true) 39 | 40 | return React.createElement(React.Fragment, {}, { 41 | Interactive = React.createElement(Checkbox, { 42 | Size = UDim2.fromOffset(200, 20), 43 | Label = "Interactive (try me)", 44 | Value = value, 45 | OnChanged = function() 46 | setValue(not value) 47 | end, 48 | LayoutOrder = 1, 49 | }), 50 | 51 | True = React.createElement(StoryItem, { 52 | Label = "True", 53 | Value = true, 54 | LayoutOrder = 2, 55 | }), 56 | 57 | False = React.createElement(StoryItem, { 58 | Label = "False", 59 | Value = false, 60 | LayoutOrder = 3, 61 | }), 62 | 63 | Indeterminate = React.createElement(StoryItem, { 64 | Label = "Indeterminate", 65 | Value = nil, 66 | LayoutOrder = 4, 67 | }), 68 | }) 69 | end 70 | 71 | return createStory(Story) 72 | -------------------------------------------------------------------------------- /src/Stories/ColorPicker.story.luau: -------------------------------------------------------------------------------- 1 | local React = require("@pkg/@jsdotlua/react") 2 | 3 | local ColorPicker = require("../Components/ColorPicker") 4 | local createStory = require("./Helpers/createStory") 5 | 6 | local function StoryItem(props: { Disabled: boolean? }) 7 | local color, setColor = React.useState(Color3.fromRGB(255, 255, 0)) 8 | 9 | return React.createElement(ColorPicker, { 10 | Color = color, 11 | OnChanged = setColor, 12 | Disabled = props.Disabled, 13 | }) 14 | end 15 | 16 | local function Story() 17 | return React.createElement(React.Fragment, {}, { 18 | Enabled = React.createElement(StoryItem), 19 | Disabled = React.createElement(StoryItem, { Disabled = true }), 20 | }) 21 | end 22 | 23 | return createStory(Story) 24 | -------------------------------------------------------------------------------- /src/Stories/DatePicker.story.luau: -------------------------------------------------------------------------------- 1 | local React = require("@pkg/@jsdotlua/react") 2 | 3 | local DatePicker = require("../Components/DatePicker") 4 | local Label = require("../Components/Label") 5 | local createStory = require("./Helpers/createStory") 6 | 7 | local function Story() 8 | local date, setDate = React.useState(DateTime.now()) 9 | 10 | return React.createElement(React.Fragment, {}, { 11 | Picker = React.createElement(DatePicker, { 12 | Date = date, 13 | OnChanged = setDate, 14 | LayoutOrder = 1, 15 | }), 16 | Display = React.createElement(Label, { 17 | LayoutOrder = 2, 18 | Size = UDim2.new(1, 0, 0, 20), 19 | Text = `Selected: {date:FormatUniversalTime("LL", "en-us")}`, 20 | }), 21 | }) 22 | end 23 | 24 | return createStory(Story) 25 | -------------------------------------------------------------------------------- /src/Stories/DropShadowFrame.story.luau: -------------------------------------------------------------------------------- 1 | local React = require("@pkg/@jsdotlua/react") 2 | 3 | local Checkbox = require("../Components/Checkbox") 4 | local DropShadowFrame = require("../Components/DropShadowFrame") 5 | local Label = require("../Components/Label") 6 | 7 | local createStory = require("./Helpers/createStory") 8 | 9 | local function Story() 10 | local boxValue, setBoxValue = React.useState(false) 11 | 12 | return React.createElement(DropShadowFrame, { 13 | Size = UDim2.fromOffset(175, 75), 14 | }, { 15 | Layout = React.createElement("UIListLayout", { 16 | SortOrder = Enum.SortOrder.LayoutOrder, 17 | Padding = UDim.new(0, 10), 18 | }), 19 | Padding = React.createElement("UIPadding", { 20 | PaddingLeft = UDim.new(0, 10), 21 | PaddingRight = UDim.new(0, 10), 22 | PaddingTop = UDim.new(0, 10), 23 | PaddingBottom = UDim.new(0, 10), 24 | }), 25 | Label = React.createElement(Label, { 26 | LayoutOrder = 1, 27 | Text = "Example label", 28 | Size = UDim2.new(1, 0, 0, 16), 29 | TextXAlignment = Enum.TextXAlignment.Left, 30 | }), 31 | Checkbox = React.createElement(Checkbox, { 32 | LayoutOrder = 2, 33 | Value = boxValue, 34 | OnChanged = function() 35 | setBoxValue(not boxValue) 36 | end, 37 | Label = "Example checkbox", 38 | }), 39 | }) 40 | end 41 | 42 | return createStory(Story) 43 | -------------------------------------------------------------------------------- /src/Stories/Dropdown.story.luau: -------------------------------------------------------------------------------- 1 | local React = require("@pkg/@jsdotlua/react") 2 | 3 | local Constants = require("../Constants") 4 | local Dropdown = require("../Components/Dropdown") 5 | 6 | local createStory = require("./Helpers/createStory") 7 | local useTheme = require("../Hooks/useTheme") 8 | 9 | local classNames = { 10 | "Part", 11 | "Script", 12 | "Player", 13 | "Folder", 14 | "Tool", 15 | "SpawnLocation", 16 | "MeshPart", 17 | "Model", 18 | "ClickDetector", 19 | "Decal", 20 | "ProximityPrompt", 21 | "SurfaceAppearance", 22 | "Texture", 23 | "Animation", 24 | "Accessory", 25 | "Humanoid", 26 | } 27 | 28 | -- hack to get themed class images 29 | local function getClassImage(className: string, theme: StudioTheme) 30 | return `rbxasset://studio_svg_textures/Shared/InsertableObjects/{theme}/Standard/{className}.png` 31 | end 32 | 33 | local function StoryItem(props: { 34 | LayoutOrder: number, 35 | Disabled: boolean?, 36 | }) 37 | local theme = useTheme() 38 | 39 | local selectedClassName: string?, setSelectedClassName = React.useState(nil :: string?) 40 | local classes = {} 41 | for i, className in classNames do 42 | classes[i] = { 43 | Id = className, 44 | Text = className, 45 | Icon = { 46 | Image = getClassImage(className, theme), 47 | Size = Vector2.one * 16, 48 | }, 49 | } 50 | end 51 | 52 | return React.createElement(Dropdown, { 53 | Size = UDim2.fromOffset(200, Constants.DefaultDropdownHeight), 54 | BackgroundTransparency = 1, 55 | LayoutOrder = props.LayoutOrder, 56 | Items = classes, 57 | SelectedItem = selectedClassName, 58 | OnItemSelected = function(newName: string?) 59 | setSelectedClassName(newName) 60 | end, 61 | DefaultText = "Select a Class...", 62 | MaxVisibleRows = 8, 63 | RowHeight = 24, 64 | ClearButton = true, 65 | Disabled = props.Disabled, 66 | }) 67 | end 68 | 69 | local function Story() 70 | return React.createElement(React.Fragment, {}, { 71 | Enabled = React.createElement(StoryItem, { 72 | LayoutOrder = 1, 73 | }), 74 | Disabled = React.createElement(StoryItem, { 75 | LayoutOrder = 2, 76 | Disabled = true, 77 | }), 78 | }) 79 | end 80 | 81 | return createStory(Story) 82 | -------------------------------------------------------------------------------- /src/Stories/Helpers/createStory.luau: -------------------------------------------------------------------------------- 1 | local React = require("@pkg/@jsdotlua/react") 2 | local ReactRoblox = require("@pkg/@jsdotlua/react-roblox") 3 | 4 | local PluginProvider = require("../../Components/PluginProvider") 5 | local ScrollFrame = require("../../Components/ScrollFrame") 6 | local ThemeContext = require("../../Contexts/ThemeContext") 7 | local getStoryPlugin = require("./getStoryPlugin") 8 | 9 | local themes = settings().Studio:GetAvailableThemes() 10 | themes[1], themes[2] = themes[2], themes[1] 11 | 12 | local function StoryTheme(props: { 13 | Theme: StudioTheme, 14 | Size: UDim2, 15 | LayoutOrder: number, 16 | } & { 17 | children: React.ReactNode, 18 | }) 19 | return React.createElement("Frame", { 20 | Size = props.Size, 21 | LayoutOrder = props.LayoutOrder, 22 | BackgroundColor3 = props.Theme:GetColor(Enum.StudioStyleGuideColor.MainBackground), 23 | BorderSizePixel = 0, 24 | }, { 25 | Provider = React.createElement(ThemeContext.Provider, { 26 | value = props.Theme, 27 | }, { 28 | Inner = React.createElement(ScrollFrame, { 29 | Layout = { 30 | ClassName = "UIListLayout", 31 | SortOrder = Enum.SortOrder.LayoutOrder, 32 | HorizontalAlignment = Enum.HorizontalAlignment.Center, 33 | VerticalAlignment = Enum.VerticalAlignment.Center, 34 | Padding = UDim.new(0, 10), 35 | }, 36 | PaddingLeft = UDim.new(0, 10), 37 | PaddingRight = UDim.new(0, 10), 38 | PaddingTop = UDim.new(0, 10), 39 | PaddingBottom = UDim.new(0, 10), 40 | }, props.children), 41 | }), 42 | }) 43 | end 44 | 45 | local function createStory(component: React.FC) 46 | return function(target: Frame) 47 | local items: { React.ReactNode } = {} 48 | local order = 0 49 | local function getOrder() 50 | order += 1 51 | return order 52 | end 53 | for i, theme in themes do 54 | local widthOffset = if #themes > 2 and i == #themes then -1 else 0 55 | if i == 1 and #themes > 1 then 56 | widthOffset -= 1 57 | end 58 | table.insert( 59 | items, 60 | React.createElement(StoryTheme, { 61 | Theme = theme, 62 | Size = UDim2.new(1 / #themes, widthOffset, 1, 0), 63 | LayoutOrder = getOrder(), 64 | }, React.createElement(component)) 65 | ) 66 | -- invisible divider to prevent scrollframe edges overlapping as 67 | -- they have default borders (outside, not inset) 68 | if i < #themes then 69 | table.insert( 70 | items, 71 | React.createElement("Frame", { 72 | Size = UDim2.new(0, 2, 1, 0), 73 | BackgroundTransparency = 1, 74 | LayoutOrder = getOrder(), 75 | }) 76 | ) 77 | end 78 | end 79 | 80 | local element = React.createElement(PluginProvider, { 81 | Plugin = getStoryPlugin(), 82 | }, { 83 | Layout = React.createElement("UIListLayout", { 84 | SortOrder = Enum.SortOrder.LayoutOrder, 85 | FillDirection = Enum.FillDirection.Horizontal, 86 | }), 87 | Padding = React.createElement("UIPadding", { 88 | PaddingBottom = UDim.new(0, 45), -- tray buttons 89 | }), 90 | }, items) 91 | 92 | local root = ReactRoblox.createRoot(Instance.new("Folder")) 93 | local portal = ReactRoblox.createPortal(element, target) 94 | root:render(portal) 95 | return function() 96 | root:unmount() 97 | end 98 | end 99 | end 100 | 101 | return createStory 102 | -------------------------------------------------------------------------------- /src/Stories/Helpers/getStoryPlugin.luau: -------------------------------------------------------------------------------- 1 | --!nocheck 2 | --!nolint UnknownGlobal 3 | 4 | -- selene: allow(undefined_variable) 5 | local plugin = PluginManager():CreatePlugin() 6 | 7 | return function() 8 | return plugin 9 | end 10 | -------------------------------------------------------------------------------- /src/Stories/Helpers/studiocomponents.storybook.luau: -------------------------------------------------------------------------------- 1 | --!nocheck 2 | 3 | local React = require("@pkg/@jsdotlua/react") 4 | local ReactRoblox = require("@pkg/@jsdotlua/react-roblox") 5 | 6 | return { 7 | name = "StudioComponents", 8 | storyRoots = { 9 | script.Parent.Parent, 10 | }, 11 | packages = { 12 | React = React, 13 | ReactRoblox = ReactRoblox, 14 | }, 15 | } 16 | -------------------------------------------------------------------------------- /src/Stories/Label.story.luau: -------------------------------------------------------------------------------- 1 | local React = require("@pkg/@jsdotlua/react") 2 | 3 | local Label = require("../Components/Label") 4 | local createStory = require("./Helpers/createStory") 5 | 6 | local styles = { 7 | Enum.StudioStyleGuideColor.MainText, 8 | Enum.StudioStyleGuideColor.SubText, 9 | Enum.StudioStyleGuideColor.TitlebarText, 10 | Enum.StudioStyleGuideColor.BrightText, 11 | Enum.StudioStyleGuideColor.DimmedText, 12 | Enum.StudioStyleGuideColor.ButtonText, 13 | Enum.StudioStyleGuideColor.LinkText, 14 | Enum.StudioStyleGuideColor.WarningText, 15 | Enum.StudioStyleGuideColor.ErrorText, 16 | Enum.StudioStyleGuideColor.InfoText, 17 | } 18 | 19 | local function StoryItem(props: { 20 | TextColorStyle: Enum.StudioStyleGuideColor, 21 | LayoutOrder: number, 22 | }) 23 | return React.createElement("Frame", { 24 | Size = UDim2.new(0, 170, 0, 40), 25 | LayoutOrder = props.LayoutOrder, 26 | BackgroundTransparency = 1, 27 | }, { 28 | Enabled = React.createElement(Label, { 29 | Text = props.TextColorStyle.Name, 30 | TextColorStyle = props.TextColorStyle, 31 | TextXAlignment = Enum.TextXAlignment.Center, 32 | Size = UDim2.new(1, 0, 0, 20), 33 | }), 34 | Disabled = React.createElement(Label, { 35 | Text = `{props.TextColorStyle.Name} (Disabled)`, 36 | Size = UDim2.new(1, 0, 0, 20), 37 | Position = UDim2.fromOffset(0, 20), 38 | TextColorStyle = props.TextColorStyle, 39 | TextXAlignment = Enum.TextXAlignment.Center, 40 | Disabled = true, 41 | }), 42 | }) 43 | end 44 | 45 | local function Story() 46 | local items = {} 47 | for i, style in styles do 48 | items[i] = React.createElement(StoryItem, { 49 | TextColorStyle = style, 50 | LayoutOrder = i, 51 | }) 52 | end 53 | return React.createElement(React.Fragment, {}, items) 54 | end 55 | 56 | return createStory(Story) 57 | -------------------------------------------------------------------------------- /src/Stories/LoadingDots.story.luau: -------------------------------------------------------------------------------- 1 | local React = require("@pkg/@jsdotlua/react") 2 | 3 | local LoadingDots = require("../Components/LoadingDots") 4 | local createStory = require("./Helpers/createStory") 5 | 6 | local function Story() 7 | return React.createElement(LoadingDots, {}) 8 | end 9 | 10 | return createStory(Story) 11 | -------------------------------------------------------------------------------- /src/Stories/MainButton.story.luau: -------------------------------------------------------------------------------- 1 | local React = require("@pkg/@jsdotlua/react") 2 | 3 | local MainButton = require("../Components/MainButton") 4 | local createStory = require("./Helpers/createStory") 5 | 6 | local function StoryButton(props: { 7 | Text: string?, 8 | HasIcon: boolean?, 9 | Disabled: boolean?, 10 | }) 11 | return React.createElement(MainButton, { 12 | LayoutOrder = if props.Disabled then 2 else 1, 13 | Icon = if props.HasIcon 14 | then { 15 | Image = "rbxasset://studio_svg_textures/Shared/InsertableObjects/Dark/Standard/Part.png", 16 | Size = Vector2.one * 16, 17 | UseThemeColor = true, 18 | Alignment = Enum.HorizontalAlignment.Left, 19 | } 20 | else nil, 21 | Text = props.Text, 22 | OnActivated = if not props.Disabled then function() end else nil, 23 | Disabled = props.Disabled, 24 | AutomaticSize = Enum.AutomaticSize.XY, 25 | }) 26 | end 27 | 28 | local function StoryItem(props: { 29 | LayoutOrder: number, 30 | Text: string?, 31 | HasIcon: boolean?, 32 | Disabled: boolean?, 33 | }) 34 | local height, setHeight = React.useBinding(0) 35 | 36 | return React.createElement("Frame", { 37 | Size = height:map(function(value) 38 | return UDim2.new(1, 0, 0, value) 39 | end), 40 | LayoutOrder = props.LayoutOrder, 41 | BackgroundTransparency = 1, 42 | }, { 43 | Layout = React.createElement("UIListLayout", { 44 | Padding = UDim.new(0, 10), 45 | SortOrder = Enum.SortOrder.LayoutOrder, 46 | FillDirection = Enum.FillDirection.Horizontal, 47 | HorizontalAlignment = Enum.HorizontalAlignment.Center, 48 | [React.Change.AbsoluteContentSize] = function(rbx) 49 | setHeight(rbx.AbsoluteContentSize.Y) 50 | end, 51 | }), 52 | Enabled = React.createElement(StoryButton, { 53 | Text = props.Text, 54 | HasIcon = props.HasIcon, 55 | }), 56 | -- Disabled = React.createElement(StoryButton, { 57 | -- Text = props.Text, 58 | -- HasIcon = props.HasIcon, 59 | -- Disabled = true, 60 | -- }), 61 | }) 62 | end 63 | 64 | local function Story() 65 | return React.createElement(React.Fragment, {}, { 66 | Icon = React.createElement(StoryItem, { 67 | LayoutOrder = 1, 68 | HasIcon = true, 69 | }), 70 | Text = React.createElement(StoryItem, { 71 | LayoutOrder = 2, 72 | Text = "Example Text", 73 | }), 74 | TextLonger = React.createElement(StoryItem, { 75 | LayoutOrder = 3, 76 | Text = "Example Longer Text", 77 | }), 78 | TextMulti = React.createElement(StoryItem, { 79 | LayoutOrder = 4, 80 | Text = "Example Text\nover two lines", 81 | }), 82 | IconTextIcon = React.createElement(StoryItem, { 83 | LayoutOrder = 5, 84 | HasIcon = true, 85 | Text = "Example Text with Icon", 86 | }), 87 | }) 88 | end 89 | 90 | return createStory(Story) 91 | -------------------------------------------------------------------------------- /src/Stories/NumberSequencePicker.story.luau: -------------------------------------------------------------------------------- 1 | local React = require("@pkg/@jsdotlua/react") 2 | 3 | local NumberSequencePicker = require("../Components/NumberSequencePicker") 4 | local createStory = require("./Helpers/createStory") 5 | 6 | local function Story() 7 | local value, setValue = React.useState(NumberSequence.new({ 8 | NumberSequenceKeypoint.new(0.0, 0.00), 9 | NumberSequenceKeypoint.new(0.4, 0.75, 0.10), 10 | NumberSequenceKeypoint.new(0.5, 0.45, 0.15), 11 | NumberSequenceKeypoint.new(0.8, 0.75), 12 | NumberSequenceKeypoint.new(1.0, 0.50), 13 | })) 14 | 15 | return React.createElement("Frame", { 16 | BackgroundTransparency = 1, 17 | Size = UDim2.fromScale(1, 1), 18 | }, { 19 | Picker = React.createElement(NumberSequencePicker, { 20 | Value = value, 21 | OnChanged = setValue, 22 | AnchorPoint = Vector2.new(0.5, 0.5), 23 | Position = UDim2.fromScale(0.5, 0.5), 24 | }), 25 | }) 26 | end 27 | 28 | return createStory(Story) 29 | -------------------------------------------------------------------------------- /src/Stories/NumericInput.story.luau: -------------------------------------------------------------------------------- 1 | local React = require("@pkg/@jsdotlua/react") 2 | 3 | local Constants = require("../Constants") 4 | local NumericInput = require("../Components/NumericInput") 5 | local createStory = require("./Helpers/createStory") 6 | 7 | local function StoryItem(props: { 8 | LayoutOrder: number, 9 | Arrows: boolean?, 10 | Slider: boolean?, 11 | }) 12 | local value, setValue = React.useState(5) 13 | 14 | local min = 0 15 | local max = 10 16 | local step = 0.25 17 | 18 | local function format(n: number) 19 | return string.format("%.2f", n) 20 | end 21 | 22 | return React.createElement("Frame", { 23 | LayoutOrder = props.LayoutOrder, 24 | Size = UDim2.new(0, 150, 0, Constants.DefaultInputHeight * 2 + 10), 25 | BackgroundTransparency = 1, 26 | }, { 27 | Enabled = React.createElement(NumericInput, { 28 | LayoutOrder = 1, 29 | Size = UDim2.new(1, 0, 0, Constants.DefaultInputHeight), 30 | Value = value, 31 | Min = min, 32 | Max = max, 33 | Step = step, 34 | ClearTextOnFocus = false, 35 | OnValidChanged = setValue, 36 | FormatValue = format, 37 | Arrows = props.Arrows, 38 | Slider = props.Slider, 39 | }), 40 | Disabled = React.createElement(NumericInput, { 41 | LayoutOrder = 3, 42 | Size = UDim2.new(1, 0, 0, Constants.DefaultInputHeight), 43 | Position = UDim2.fromOffset(0, Constants.DefaultInputHeight + 5), 44 | Value = value, 45 | Min = min, 46 | Max = max, 47 | Step = step, 48 | ClearTextOnFocus = false, 49 | OnValidChanged = function() end, 50 | FormatValue = format, 51 | Arrows = props.Arrows, 52 | Slider = props.Slider, 53 | Disabled = true, 54 | }), 55 | }) 56 | end 57 | 58 | local function Story() 59 | return React.createElement(React.Fragment, {}, { 60 | Regular = React.createElement(StoryItem, { 61 | LayoutOrder = 1, 62 | }), 63 | Arrows = React.createElement(StoryItem, { 64 | LayoutOrder = 2, 65 | Arrows = true, 66 | }), 67 | Slider = React.createElement(StoryItem, { 68 | LayoutOrder = 3, 69 | Slider = true, 70 | }), 71 | Both = React.createElement(StoryItem, { 72 | LayoutOrder = 4, 73 | Arrows = true, 74 | Slider = true, 75 | }), 76 | }) 77 | end 78 | 79 | return createStory(Story) 80 | -------------------------------------------------------------------------------- /src/Stories/ProgressBar.story.luau: -------------------------------------------------------------------------------- 1 | local React = require("@pkg/@jsdotlua/react") 2 | 3 | local ProgressBar = require("../Components/ProgressBar") 4 | local createStory = require("./Helpers/createStory") 5 | 6 | local HEIGHT = 14 7 | 8 | local function StoryItem(props: { 9 | Value: number, 10 | Max: number?, 11 | Formatter: ((number, number) -> string)?, 12 | LayoutOrder: number, 13 | }) 14 | return React.createElement("Frame", { 15 | Size = UDim2.new(1, 0, 0, HEIGHT), 16 | LayoutOrder = props.LayoutOrder, 17 | BackgroundTransparency = 1, 18 | }, { 19 | Enabled = React.createElement(ProgressBar, { 20 | Value = props.Value, 21 | Max = props.Max, 22 | Formatter = props.Formatter, 23 | --Size = UDim2.new(0.5, -5, 1, 0), 24 | Size = UDim2.new(0, 225, 1, 0), 25 | Position = UDim2.fromOffset(20, 0), 26 | }), 27 | -- Disabled = React.createElement(ProgressBar, { 28 | -- Value = props.Value, 29 | -- Max = props.Max, 30 | -- Formatter = props.Formatter, 31 | -- AnchorPoint = Vector2.new(1, 0), 32 | -- Position = UDim2.fromScale(1, 0), 33 | -- Size = UDim2.new(0.5, -5, 1, 0), 34 | -- Disabled = true, 35 | -- }), 36 | }) 37 | end 38 | 39 | local function Story() 40 | return React.createElement(React.Fragment, {}, { 41 | Zero = React.createElement(StoryItem, { 42 | Value = 0, 43 | LayoutOrder = 1, 44 | }), 45 | Fifty = React.createElement(StoryItem, { 46 | Value = 0.5, 47 | LayoutOrder = 1, 48 | }), 49 | Hundred = React.createElement(StoryItem, { 50 | Value = 1, 51 | LayoutOrder = 2, 52 | }), 53 | Custom = React.createElement(StoryItem, { 54 | Value = 5, 55 | Max = 14, 56 | LayoutOrder = 3, 57 | Formatter = function(value, max) 58 | return `loaded {value} / {max} assets` 59 | end, 60 | }), 61 | }) 62 | end 63 | 64 | return createStory(Story) 65 | -------------------------------------------------------------------------------- /src/Stories/RadioButton.story.luau: -------------------------------------------------------------------------------- 1 | local React = require("@pkg/@jsdotlua/react") 2 | 3 | local RadioButton = require("../Components/RadioButton") 4 | local createStory = require("./Helpers/createStory") 5 | 6 | local function Story() 7 | local value, setValue = React.useState(true) 8 | 9 | return React.createElement(React.Fragment, {}, { 10 | Enabled = React.createElement(RadioButton, { 11 | Label = "Enabled", 12 | Value = value, 13 | OnChanged = function() 14 | setValue(not value) 15 | end, 16 | LayoutOrder = 1, 17 | }), 18 | Disabled = React.createElement(RadioButton, { 19 | Label = "Disabled", 20 | Value = value, 21 | Disabled = true, 22 | LayoutOrder = 2, 23 | }), 24 | }) 25 | end 26 | 27 | return createStory(Story) 28 | -------------------------------------------------------------------------------- /src/Stories/ScrollFrame.story.luau: -------------------------------------------------------------------------------- 1 | local React = require("@pkg/@jsdotlua/react") 2 | 3 | local Constants = require("../Constants") 4 | local ScrollFrame = require("../Components/ScrollFrame") 5 | local createStory = require("./Helpers/createStory") 6 | 7 | local numRows = 10 8 | local numCols = 10 9 | 10 | local size = Vector2.new(48, 32) 11 | 12 | local function StoryRow(props: { 13 | Row: number, 14 | }) 15 | local children = {} 16 | for i = 1, numCols do 17 | children[i] = React.createElement("TextLabel", { 18 | LayoutOrder = i, 19 | Text = string.format("%i,%i", i - 1, props.Row - 1), 20 | Font = Constants.DefaultFont, 21 | TextSize = Constants.DefaultTextSize, 22 | TextColor3 = Color3.fromRGB(0, 0, 0), 23 | Size = UDim2.new(0, size.X, 1, 0), 24 | BorderSizePixel = 0, 25 | BackgroundTransparency = 0, 26 | BackgroundColor3 = Color3.fromHSV((i + props.Row) % numCols / numCols, 0.6, 0.8), 27 | }) 28 | end 29 | return React.createElement("Frame", { 30 | LayoutOrder = props.Row, 31 | Size = UDim2.fromOffset(numCols * size.X, size.Y), 32 | BackgroundTransparency = 1, 33 | }, { 34 | Layout = React.createElement("UIListLayout", { 35 | FillDirection = Enum.FillDirection.Horizontal, 36 | SortOrder = Enum.SortOrder.LayoutOrder, 37 | }), 38 | }, children) 39 | end 40 | 41 | local function StoryScroller(props: { 42 | Size: UDim2, 43 | LayoutOrder: number, 44 | Disabled: boolean?, 45 | }) 46 | local rows = {} 47 | for i = 1, numRows do 48 | rows[i] = React.createElement(StoryRow, { Row = i }) 49 | end 50 | 51 | return React.createElement(ScrollFrame, { 52 | ScrollingDirection = Enum.ScrollingDirection.XY, 53 | Size = props.Size, 54 | Disabled = props.Disabled, 55 | Layout = { 56 | ClassName = "UIListLayout", 57 | SortOrder = Enum.SortOrder.LayoutOrder, 58 | }, 59 | }, rows) 60 | end 61 | 62 | local function Story() 63 | return React.createElement(React.Fragment, {}, { 64 | Enabled = React.createElement(StoryScroller, { 65 | Size = UDim2.new(1, -10, 0, 220), 66 | LayoutOrder = 1, 67 | }), 68 | Disabled = React.createElement(StoryScroller, { 69 | Size = UDim2.new(1, -10, 0, 220), 70 | LayoutOrder = 2, 71 | Disabled = true, 72 | }), 73 | }) 74 | end 75 | 76 | return createStory(Story) 77 | -------------------------------------------------------------------------------- /src/Stories/Slider.story.luau: -------------------------------------------------------------------------------- 1 | local React = require("@pkg/@jsdotlua/react") 2 | 3 | local Slider = require("../Components/Slider") 4 | local createStory = require("./Helpers/createStory") 5 | 6 | local function StoryItem(props: { 7 | LayoutOrder: number, 8 | Disabled: boolean?, 9 | }) 10 | local value, setValue = React.useState(3) 11 | return React.createElement(Slider, { 12 | Value = value, 13 | Min = 0, 14 | Max = 10, 15 | Step = 0, 16 | OnChanged = setValue, 17 | Disabled = props.Disabled, 18 | }) 19 | end 20 | 21 | local function Story() 22 | return React.createElement(React.Fragment, {}, { 23 | Enabled = React.createElement(StoryItem, { 24 | LayoutOrder = 1, 25 | }), 26 | Disabled = React.createElement(StoryItem, { 27 | LayoutOrder = 2, 28 | Disabled = true, 29 | }), 30 | }) 31 | end 32 | 33 | return createStory(Story) 34 | -------------------------------------------------------------------------------- /src/Stories/Splitter.story.luau: -------------------------------------------------------------------------------- 1 | local React = require("@pkg/@jsdotlua/react") 2 | 3 | local Label = require("../Components/Label") 4 | local Splitter = require("../Components/Splitter") 5 | local createStory = require("./Helpers/createStory") 6 | local useTheme = require("../Hooks/useTheme") 7 | 8 | local function StoryItem(props: { 9 | Size: UDim2, 10 | LayoutOrder: number, 11 | Disabled: boolean?, 12 | }) 13 | local theme = useTheme() 14 | 15 | local alpha0, setAlpha0 = React.useState(0.5) 16 | local alpha1, setAlpha1 = React.useState(0.5) 17 | 18 | local postText = if props.Disabled then "\n(Disabled)" else "" 19 | 20 | return React.createElement("Frame", { 21 | Size = props.Size, 22 | LayoutOrder = props.LayoutOrder, 23 | BackgroundColor3 = theme:GetColor(Enum.StudioStyleGuideColor.MainBackground), 24 | BorderColor3 = theme:GetColor(Enum.StudioStyleGuideColor.Border), 25 | BorderMode = Enum.BorderMode.Inset, 26 | }, { 27 | Splitter = React.createElement(Splitter, { 28 | Alpha = alpha0, 29 | OnChanged = setAlpha0, 30 | FillDirection = Enum.FillDirection.Vertical, 31 | Disabled = props.Disabled, 32 | }, { 33 | Side0 = React.createElement(Label, { 34 | Text = "Top" .. postText, 35 | Disabled = props.Disabled, 36 | }), 37 | Side1 = React.createElement(Splitter, { 38 | Alpha = alpha1, 39 | OnChanged = setAlpha1, 40 | FillDirection = Enum.FillDirection.Horizontal, 41 | Disabled = props.Disabled, 42 | }, { 43 | Side0 = React.createElement(Label, { 44 | Text = "Bottom Left" .. postText, 45 | Disabled = props.Disabled, 46 | }), 47 | Side1 = React.createElement(Label, { 48 | Text = "Bottom Right" .. postText, 49 | Disabled = props.Disabled, 50 | }), 51 | }), 52 | }), 53 | }) 54 | end 55 | 56 | local function Story() 57 | return React.createElement(React.Fragment, {}, { 58 | Enabled = React.createElement(StoryItem, { 59 | Size = UDim2.new(1, 0, 0.5, -5), 60 | LayoutOrder = 1, 61 | }), 62 | Disabled = React.createElement(StoryItem, { 63 | Size = UDim2.new(1, 0, 0.5, -5), 64 | LayoutOrder = 2, 65 | Disabled = true, 66 | }), 67 | }) 68 | end 69 | 70 | return createStory(Story) 71 | -------------------------------------------------------------------------------- /src/Stories/TabContainer.story.luau: -------------------------------------------------------------------------------- 1 | local React = require("@pkg/@jsdotlua/react") 2 | 3 | local TabContainer = require("../Components/TabContainer") 4 | local createStory = require("./Helpers/createStory") 5 | 6 | local function StoryItemContent(props: { 7 | BackgroundColor3: Color3, 8 | }) 9 | return React.createElement("Frame", { 10 | Position = UDim2.fromOffset(10, 10), 11 | Size = UDim2.fromOffset(50, 50), 12 | BackgroundColor3 = props.BackgroundColor3, 13 | }) 14 | end 15 | 16 | local function StoryItem(props: { 17 | LayoutOrder: number, 18 | Disabled: boolean?, 19 | }) 20 | local selected, setSelected = React.useState("First") 21 | 22 | return React.createElement(TabContainer, { 23 | Size = UDim2.new(1, -50, 0.5, -50), 24 | LayoutOrder = props.LayoutOrder, 25 | SelectedTab = selected, 26 | OnTabSelected = setSelected, 27 | Disabled = props.Disabled, 28 | }, { 29 | First = { 30 | LayoutOrder = 1, 31 | Content = React.createElement(StoryItemContent, { 32 | BackgroundColor3 = Color3.fromRGB(255, 0, 255), 33 | }), 34 | }, 35 | Second = { 36 | LayoutOrder = 2, 37 | Content = React.createElement(StoryItemContent, { 38 | BackgroundColor3 = Color3.fromRGB(255, 255, 0), 39 | }), 40 | }, 41 | Third = { 42 | LayoutOrder = 3, 43 | Content = React.createElement(StoryItemContent, { 44 | BackgroundColor3 = Color3.fromRGB(0, 255, 255), 45 | }), 46 | Disabled = true, 47 | }, 48 | }) 49 | end 50 | 51 | local function Story() 52 | return React.createElement(React.Fragment, {}, { 53 | Enabled = React.createElement(StoryItem, { 54 | LayoutOrder = 1, 55 | }), 56 | Disabled = React.createElement(StoryItem, { 57 | LayoutOrder = 2, 58 | Disabled = true, 59 | }), 60 | }) 61 | end 62 | 63 | return createStory(Story) 64 | -------------------------------------------------------------------------------- /src/Stories/TextInput.story.luau: -------------------------------------------------------------------------------- 1 | local React = require("@pkg/@jsdotlua/react") 2 | 3 | local TextInput = require("../Components/TextInput") 4 | local createStory = require("./Helpers/createStory") 5 | 6 | local function StoryItem(props: { 7 | Label: string, 8 | LayoutOrder: number, 9 | Disabled: boolean?, 10 | Filter: ((s: string) -> string)?, 11 | }) 12 | local text, setText = React.useState(if props.Disabled then props.Label else "") 13 | 14 | return React.createElement("Frame", { 15 | Size = UDim2.fromOffset(175, 20), 16 | LayoutOrder = props.LayoutOrder, 17 | BackgroundTransparency = 1, 18 | }, { 19 | Input = React.createElement(TextInput, { 20 | Text = text, 21 | PlaceholderText = props.Label, 22 | Disabled = props.Disabled, 23 | OnChanged = function(newText) 24 | local filtered = newText 25 | if props.Filter then 26 | filtered = props.Filter(newText) 27 | end 28 | setText(filtered) 29 | end, 30 | }), 31 | }) 32 | end 33 | 34 | local function Story() 35 | return React.createElement(React.Fragment, {}, { 36 | Enabled = React.createElement(StoryItem, { 37 | Label = "Any text allowed", 38 | LayoutOrder = 1, 39 | }), 40 | Filtered = React.createElement(StoryItem, { 41 | Label = "Numbers only", 42 | LayoutOrder = 2, 43 | Filter = function(text) 44 | return (string.gsub(text, "%D", "")) 45 | end, 46 | }), 47 | Disabled = React.createElement(StoryItem, { 48 | Label = "Disabled", 49 | LayoutOrder = 3, 50 | Disabled = true, 51 | }), 52 | }) 53 | end 54 | 55 | return createStory(Story) 56 | -------------------------------------------------------------------------------- /src/getTextSize.luau: -------------------------------------------------------------------------------- 1 | local TextService = game:GetService("TextService") 2 | 3 | local Constants = require("./Constants") 4 | 5 | local TEXT_SIZE = Constants.DefaultTextSize 6 | local FONT = Constants.DefaultFont 7 | local FRAME_SIZE = Vector2.one * math.huge 8 | 9 | local function getTextSize(text: string) 10 | local size = TextService:GetTextSize(text, TEXT_SIZE, FONT, FRAME_SIZE) 11 | return Vector2.new(math.ceil(size.X), math.ceil(size.Y)) + Vector2.one 12 | end 13 | 14 | return getTextSize 15 | -------------------------------------------------------------------------------- /src/init.luau: -------------------------------------------------------------------------------- 1 | return { 2 | Constants = require("./Constants"), 3 | 4 | Background = require("./Components/Background"), 5 | Button = require("./Components/Button"), 6 | Checkbox = require("./Components/Checkbox"), 7 | ColorPicker = require("./Components/ColorPicker"), 8 | DatePicker = require("./Components/DatePicker"), 9 | Dropdown = require("./Components/Dropdown"), 10 | DropShadowFrame = require("./Components/DropShadowFrame"), 11 | Label = require("./Components/Label"), 12 | LoadingDots = require("./Components/LoadingDots"), 13 | MainButton = require("./Components/MainButton"), 14 | NumberSequencePicker = require("./Components/NumberSequencePicker"), 15 | NumericInput = require("./Components/NumericInput"), 16 | PluginProvider = require("./Components/PluginProvider"), 17 | ProgressBar = require("./Components/ProgressBar"), 18 | RadioButton = require("./Components/RadioButton"), 19 | ScrollFrame = require("./Components/ScrollFrame"), 20 | Slider = require("./Components/Slider"), 21 | Splitter = require("./Components/Splitter"), 22 | TabContainer = require("./Components/TabContainer"), 23 | TextInput = require("./Components/TextInput"), 24 | 25 | ThemeContext = require("./Contexts/ThemeContext"), 26 | 27 | useTheme = require("./Hooks/useTheme"), 28 | usePlugin = require("./Hooks/usePlugin"), 29 | useMouseIcon = require("./Hooks/useMouseIcon"), 30 | } 31 | -------------------------------------------------------------------------------- /stylua.toml: -------------------------------------------------------------------------------- 1 | [sort_requires] 2 | enabled = true 3 | --------------------------------------------------------------------------------