├── .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 | |  |  |
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 | |  |  |
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 | |  |  |
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 | |  |  |
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 | |  |  |
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 | |  |  |
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 | |  |  |
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 | |  |  |
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 | |  |  |
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 | |  |  |
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 | |  |  |
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 | |  |  |
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 | |  |  |
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 |
--------------------------------------------------------------------------------