diff --git a/.darklua-dev.json b/.darklua-dev.json
new file mode 100644
index 0000000..54984fd
--- /dev/null
+++ b/.darklua-dev.json
@@ -0,0 +1,20 @@
+{
+ "rules": [
+ {
+ "rule": "convert_require",
+ "current": {
+ "name": "path",
+ "sources": {
+ "@pkg": "Packages",
+ "@dev-pkg": "DevPackages",
+ "@root": "src"
+ }
+ },
+ "target": {
+ "name": "roblox",
+ "rojo_sourcemap": "./sourcemap.json",
+ "indexing_style": "wait_for_child"
+ }
+ }
+ ]
+}
diff --git a/.darklua-roblox.json b/.darklua-roblox.json
new file mode 100644
index 0000000..0daa011
--- /dev/null
+++ b/.darklua-roblox.json
@@ -0,0 +1,19 @@
+{
+ "rules": [
+ {
+ "rule": "convert_require",
+ "current": {
+ "name": "path",
+ "sources": {
+ "@pkg": "Packages",
+ "@root": "src"
+ }
+ },
+ "target": {
+ "name": "roblox",
+ "rojo_sourcemap": "./sourcemap.json",
+ "indexing_style": "wait_for_child"
+ }
+ }
+ ]
+}
diff --git a/.darklua-wally.json b/.darklua-wally.json
new file mode 100644
index 0000000..0daa011
--- /dev/null
+++ b/.darklua-wally.json
@@ -0,0 +1,19 @@
+{
+ "rules": [
+ {
+ "rule": "convert_require",
+ "current": {
+ "name": "path",
+ "sources": {
+ "@pkg": "Packages",
+ "@root": "src"
+ }
+ },
+ "target": {
+ "name": "roblox",
+ "rojo_sourcemap": "./sourcemap.json",
+ "indexing_style": "wait_for_child"
+ }
+ }
+ ]
+}
diff --git a/.github/dependabot.yml b/.github/dependabot.yml
deleted file mode 100644
index 619d664..0000000
--- a/.github/dependabot.yml
+++ /dev/null
@@ -1,19 +0,0 @@
-# credits: https://github.com/roblox-ts/types/blob/e3a610314ae49c945b917df654e1094f7f1987f4/.github/dependabot.yml
-
-version: 2
-updates:
- - package-ecosystem: "npm"
- versioning-strategy: increase
- open-pull-requests-limit: 99
- directory: "/"
- schedule:
- interval: weekly
- commit-message:
- prefix: "build(deps)"
- ignore:
- - dependency-name: "typescript"
- - dependency-name: "roblox-ts"
- groups:
- packages:
- patterns:
- - "*"
diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
deleted file mode 100644
index 4d4a0f1..0000000
--- a/.github/workflows/build.yml
+++ /dev/null
@@ -1,28 +0,0 @@
-name: Build package
-on:
- push:
- pull_request:
-
-jobs:
- build:
- runs-on: ubuntu-latest
-
- steps:
- - uses: actions/checkout@v3
-
- - uses: pnpm/action-setup@v2
- with:
- version: 9
-
- - name: Use Node.js 20
- uses: actions/setup-node@v3
- with:
- node-version: 20
- registry-url: 'https://registry.npmjs.org'
- cache: 'pnpm'
-
- - name: Install dependencies
- run: pnpm install
-
- - name: Build package
- run: pnpm build
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
deleted file mode 100644
index 45011c5..0000000
--- a/.github/workflows/release.yml
+++ /dev/null
@@ -1,34 +0,0 @@
-name: Release
-
-on:
- push:
- tags:
- - 'v*'
-
-jobs:
- release:
- runs-on: ubuntu-latest
-
- steps:
- - uses: actions/checkout@v3
- - uses: pnpm/action-setup@v2
- with:
- version: 9
-
- - name: Use Node.js 20
- uses: actions/setup-node@v3
- with:
- node-version: 20
- registry-url: 'https://registry.npmjs.org'
- cache: 'pnpm'
-
- - name: Install dependencies
- run: pnpm install
-
- - name: Build package
- run: pnpm build
-
- - name: Publish to NPM
- run: pnpm publish
- env:
- NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
diff --git a/.gitignore b/.gitignore
index f484755..a5fea83 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,11 +1,9 @@
-/node_modules
-/out
-/include
+Packages/
+DevPackages/
+out/
+
+sourcemap.json
.DS_Store
-*.tsbuildinfo
*.log
-
-# development tarballs
-rbxts-better-react-components-*.tgz
-package.tgz
\ No newline at end of file
+*.old
diff --git a/.luaurc b/.luaurc
new file mode 100644
index 0000000..8091482
--- /dev/null
+++ b/.luaurc
@@ -0,0 +1,7 @@
+{
+ "aliases": {
+ "pkg": "./Packages",
+ "dev-pkg": "./DevPackages",
+ "root": "./src"
+ }
+}
diff --git a/.npmrc b/.npmrc
deleted file mode 100644
index d67f374..0000000
--- a/.npmrc
+++ /dev/null
@@ -1 +0,0 @@
-node-linker=hoisted
diff --git a/Makefile b/Makefile
new file mode 100644
index 0000000..bd1c50d
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,29 @@
+.PHONY: install cleanup install-toolchain cleanup-build serve watch build/*
+
+install:
+ aftman install
+
+cleanup:
+ git clean -Xf build # remove ignored files in build/
+
+serve:
+ rojo serve out.project.json
+
+watch: Packages/ DevPackages/ sourcemap.json .darklua-dev.json
+ darklua process -w -c .darklua-dev.json src out
+
+# files/folders
+
+build/roblox:
+ ./scripts/roblox.sh
+
+build/wally:
+ ./scripts/wally.sh
+
+Packages DevPackages: wally.toml wally.lock
+ wally install
+ wally-package-types --sourcemap sourcemap.json Packages/
+ wally-package-types --sourcemap sourcemap.json DevPackages/
+
+sourcemap.json: src/* default.project.json
+ rojo sourcemap default.project.json --output sourcemap.json
diff --git a/README.md b/README.md
index 0164bd5..f19eaea 100644
--- a/README.md
+++ b/README.md
@@ -1,83 +1,79 @@
-# Better React Components
+# better-react-components
-
-
+Roblox UI elements, but with builtin modifiers
-Roblox's ui elements, with builtin modifiers
+> [!NOTE]
+>
+> Currently, I am rewriting this library from Typescript to Lua.
+>
+> I'll still publish this library to NPM.
-Also see **[introduction](docs/1_Introduction.md)**
+## Setup guide
-## Example
+```
+Dear Future Me.
-
+Please, publish and write here, how to install this library from wally or npm
-
- better-react-components code
+Respectfully,
+ Past Me
+```
- ```
-
-
+1. Install Aftman and Make
+2. Run:
+ ```
+ make install # installs toolchain
+
+ # Development stuff:
+ make watch # starts darklua's processer
+ make serve # starts rojo server
-
-
-
- ;
- ```
-
+ # Buildin' stuff:
+ make wally-package # builds wally package at `build/wally/`
+ make roblox-package # builds roblox model file at `build/wally/better-react-components.rbxmx`
+ ```
-
-## Support
-
-### TODO
-
-- [ ] Upgrade ESLint to `9.*.*` version
-- [ ] Better documentation
+## Supported Components/Modifiers
### Components
-- [X] Frame
-- [X] ScrollableFrame
-- [X] Button
+- [ ] CanvasGroup
+- [x] Frame
- [ ] ImageButton
-- [X] Image
-- [X] Text
-- [X] TextBox
-- [X] CanvasGroup
+- [x] TextButton
+- [ ] ImageLabel
+- [x] TextLabel
+- [x] ScrollingFrame
+- [x] TextBox
+- [ ] VideoDisplay
+- [ ] VideoFrame
+- [ ] ViewportFrame
+- [ ] LayerCollector
+ - [ ] BillboardGui
+ - [x] ScreenGui
+ - [ ] SurfaceGui
+- [ ] Path2D
### Modifiers
-- [X] UIAspectRatioConstraint
-- [X] UICorner
-- [X] UIGradient
-- [X] UIGridLayout [(see GridLayout)](src/components/GridLayout.tsx)
-- [X] UIListLayout [(see ListLayout)](src/components/ListLayout.tsx)
-- [X] UIFlexLayout [(see ListLayout)](src/components/ListLayout.tsx)
-- [X] UIPadding
-- [ ] UIPageLayout
-- [X] UIScale
-- [X] UISizeConstraint
-- [X] UIStroke
-- [ ] UITableLayout
-- [X] UITextSizeConstraint
+- [x] UIConstraint
+ - [x] UIAspectRatioConstraint
+ - [x] UISizeConstraint
+ - [x] UITextSizeConstraint
+- [x] UICorner (`CornerRadius` property)
+- [ ] UIDragDetector
+- [ ] UIGradient
+- [x] UIPadding
+- [ ] UIScale
+- [x] UIStroke (`Border` property)
+
-### Custom Modifiers
+### Layouts
-- ~~[ ] FlowLayout [(view)](https://devforum.roblox.com/t/flow-flexbox-layout-for-lua/2614394)~~ (Removed due release of flex features to ListLayout. [Devforum](https://devforum.roblox.com/t/flex-features-for-uilistlayout-client-release/3096190) )
+- [ ] UIGridLayout
+- [x] UIListLayout
+- [x] UIFlexItem
+- [ ] UITableLayout
+- [ ] UIPageLayout
diff --git a/aftman.toml b/aftman.toml
index e989d3b..368754c 100644
--- a/aftman.toml
+++ b/aftman.toml
@@ -1,7 +1,5 @@
-# This file lists tools managed by Aftman, a cross-platform toolchain manager.
-# For more information, see https://github.com/LPGhatguy/aftman
-
-# To add a new tool, add an entry to this table.
[tools]
-rojo = "rojo-rbx/rojo@7.4.1"
-# rojo = "rojo-rbx/rojo@6.2.0"
\ No newline at end of file
+rojo = "rojo-rbx/rojo@7.4.4"
+wally = "UpliftGames/wally@0.3.2"
+wally-package-types = "JohnnyMorganz/wally-package-types@1.4.2"
+darklua = "seaofvoices/darklua@0.16.0"
diff --git a/build/roblox/.gitignore b/build/roblox/.gitignore
new file mode 100644
index 0000000..45d22a3
--- /dev/null
+++ b/build/roblox/.gitignore
@@ -0,0 +1,10 @@
+# This folder used for storing Roblox package files
+#
+# `default.project.json` is used for generating sourcemap.json and building .rbxmx file
+#
+# To build roblox package run:
+# $ make roblox-package
+
+*
+!.gitignore
+!default.project.json
diff --git a/build/roblox/default.project.json b/build/roblox/default.project.json
new file mode 100644
index 0000000..213019b
--- /dev/null
+++ b/build/roblox/default.project.json
@@ -0,0 +1,9 @@
+{
+ "name": "better-react-components",
+ "tree": {
+ "_Vendor": {
+ "$path": "Packages"
+ },
+ "$path": "src"
+ }
+}
diff --git a/build/wally/.gitignore b/build/wally/.gitignore
new file mode 100644
index 0000000..61d72fd
--- /dev/null
+++ b/build/wally/.gitignore
@@ -0,0 +1,11 @@
+# This folder used for storing Wally package files
+#
+# `sourcemap.project.json` is used for generating sourcemap.json
+#
+# To build wally package run:
+# $ make wally-package
+
+*
+!.gitignore
+!default.project.json
+!sourcemap.project.json
diff --git a/build/wally/default.project.json b/build/wally/default.project.json
new file mode 100644
index 0000000..8a94842
--- /dev/null
+++ b/build/wally/default.project.json
@@ -0,0 +1,6 @@
+{
+ "name": "better-react-components",
+ "tree": {
+ "$path": "src"
+ }
+}
diff --git a/build/wally/sourcemap.project.json b/build/wally/sourcemap.project.json
new file mode 100644
index 0000000..8375ec8
--- /dev/null
+++ b/build/wally/sourcemap.project.json
@@ -0,0 +1,9 @@
+{
+ "name": "sourcemap",
+ "tree": {
+ "$path": "Packages",
+ "better-react-components": {
+ "$path": "src"
+ }
+ }
+}
diff --git a/default.project.json b/default.project.json
new file mode 100644
index 0000000..f2cf788
--- /dev/null
+++ b/default.project.json
@@ -0,0 +1,18 @@
+{
+ "name": "better-react-components-development",
+ "tree": {
+ "$className": "DataModel",
+ "ReplicatedStorage": {
+ "$className": "ReplicatedStorage",
+ "DevPackages": {
+ "$path": "DevPackages"
+ },
+ "Packages": {
+ "$path": "Packages"
+ },
+ "better-react-components": {
+ "$path": "src"
+ }
+ }
+ }
+}
diff --git a/out.project.json b/out.project.json
new file mode 100644
index 0000000..874445f
--- /dev/null
+++ b/out.project.json
@@ -0,0 +1,18 @@
+{
+ "name": "better-react-components-development",
+ "tree": {
+ "$className": "DataModel",
+ "ReplicatedStorage": {
+ "$className": "ReplicatedStorage",
+ "DevPackages": {
+ "$path": "DevPackages"
+ },
+ "Packages": {
+ "$path": "Packages"
+ },
+ "better-react-components": {
+ "$path": "out"
+ }
+ }
+ }
+}
diff --git a/package.json b/package.json
deleted file mode 100644
index ef9f84a..0000000
--- a/package.json
+++ /dev/null
@@ -1,45 +0,0 @@
-{
- "name": "@rbxts/better-react-components",
- "version": "2.9.1",
- "license": "MIT",
- "repository": {
- "type": "git",
- "url": "git+https://github.com/idkncc/better-react-components.git"
- },
- "bugs": {
- "url": "https://github.com/idkncc/better-react-components/issues"
- },
- "homepage": "https://github.com/idkncc/better-react-components#readme",
- "main": "out/init.lua",
- "typings": "out/index.d.ts",
- "files": [
- "out",
- "!out/*.tsbuildinfo",
- "!out/stories/**/*"
- ],
- "scripts": {
- "build": "pnpm clean && rbxtsc",
- "dev": "rbxtsc -w --type game --rojo story.project.json",
- "dev-serve": "rojo serve story.project.json",
- "lint": "eslint src",
- "clean": "rimraf out/"
- },
- "devDependencies": {
- "@rbxts/compiler-types": "3.0.0-types.0",
- "@rbxts/types": "^1.0.813",
- "rimraf": "^6.0.1",
- "roblox-ts": "3.0.0",
- "typescript": "^5.6.3"
- },
- "dependencies": {
- "@rbxts/object-utils": "^1.0.4",
- "@rbxts/pretty-react-hooks": "^0.6.0",
- "@rbxts/react": "^17.2.1",
- "@rbxts/react-roblox": "^17.2.1",
- "@rbxts/ripple": "^0.9.1",
- "@rbxts/services": "^1.5.5"
- },
- "publishConfig": {
- "access": "public"
- }
-}
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
deleted file mode 100644
index 73e6307..0000000
--- a/pnpm-lock.yaml
+++ /dev/null
@@ -1,710 +0,0 @@
-lockfileVersion: '9.0'
-
-settings:
- autoInstallPeers: true
- excludeLinksFromLockfile: false
-
-importers:
-
- .:
- dependencies:
- '@rbxts/object-utils':
- specifier: ^1.0.4
- version: 1.0.4
- '@rbxts/pretty-react-hooks':
- specifier: ^0.6.0
- version: 0.6.0
- '@rbxts/react':
- specifier: ^17.2.1
- version: 17.2.1
- '@rbxts/react-roblox':
- specifier: ^17.2.1
- version: 17.2.1
- '@rbxts/ripple':
- specifier: ^0.9.1
- version: 0.9.1
- '@rbxts/services':
- specifier: ^1.5.5
- version: 1.5.5
- devDependencies:
- '@rbxts/compiler-types':
- specifier: 3.0.0-types.0
- version: 3.0.0-types.0
- '@rbxts/types':
- specifier: ^1.0.813
- version: 1.0.813
- rimraf:
- specifier: ^6.0.1
- version: 6.0.1
- roblox-ts:
- specifier: 3.0.0
- version: 3.0.0
- typescript:
- specifier: ^5.6.3
- version: 5.6.3
-
-packages:
-
- '@isaacs/cliui@8.0.2':
- resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==}
- engines: {node: '>=12'}
-
- '@rbxts/compiler-types@3.0.0-types.0':
- resolution: {integrity: sha512-VGOHJPoL7+56NTatMGqQj3K7xWuzEV+aP4QD5vZiHu+bcff3kiTmtoadaF6NkJrmwfFAvbsd4Dg764ZjWNceag==}
-
- '@rbxts/object-utils@1.0.4':
- resolution: {integrity: sha512-dLLhf022ipV+9i910sOE7kl9losKHoon0WgeerHqVMQA5EYsLUsVT2AxhJuhk8MiDn5oJ2GiFofE/LadY9TpJQ==}
-
- '@rbxts/pretty-react-hooks@0.6.0':
- resolution: {integrity: sha512-KPaJ9xvbE6UJ1ECe4bwHBM3iqQ+IYcn0QfHjyRNZgADrz6/An+2zpxydY4ACjawAqRYybV7jwd6MqCJJiXHjOA==}
-
- '@rbxts/react-roblox@17.2.1':
- resolution: {integrity: sha512-qKCz63iMl6fFq2SUXoMG6paAW8TGxsEyUwoRk6bojg7b3NzBqYMz5/eRcJsbPqMjwR56PWBhCOH3fdf4sOcFbw==}
-
- '@rbxts/react-vendor@17.2.1':
- resolution: {integrity: sha512-7DxFweCbhhso4M4tl1pO7Kil9SZrvOl1SoZ4lnwUfoxdHvSmWgeKgaUqd+ZS3/u1E8kRraqGQ56Q8zjUEEUzlA==}
-
- '@rbxts/react@17.2.1':
- resolution: {integrity: sha512-iu0RsVvfHroI7l+QXjwE0FiWV2rXoKMxuznDZ8vnx2BsFqrPnCqFjXuNeGYZwibibZC6jbyq7uKl+/8cz7XlsA==}
-
- '@rbxts/ripple@0.9.1':
- resolution: {integrity: sha512-gLjKYy+mJO67NoN/9d7/8kBy6+SNG1/09GNNnh89DYWT5iHYdxC0/XKbKrKZpx7Fcsz5iY9Sl6vOvSnFwFbQuw==}
-
- '@rbxts/services@1.5.5':
- resolution: {integrity: sha512-Hu7QH2ecefS60zpakKG3T2vgABydoOJqpSG7nXr59wFAbPk/cA+OcsntYPHUWyHAat0VFGZ5jNZLTLdiqfssvg==}
-
- '@rbxts/set-timeout@1.1.2':
- resolution: {integrity: sha512-P/A0IiH9wuZdSJYr4Us0MDFm61nvIFR0acfKFHLkcOsgvIgELC90Up9ugiSsaMEHRIcIcO5UjE39LuS3xTzQHw==}
-
- '@rbxts/types@1.0.813':
- resolution: {integrity: sha512-Qpsmqnl+TNdl5AYqKFkYHyUrv652r30qK6A3GObOyDz+cvn8i+HE61mO021oq0nu1FcA4pMHbYqLSbv2CMF9Ig==}
-
- '@roblox-ts/luau-ast@2.0.0':
- resolution: {integrity: sha512-cmMi093IdwBOLVxwuordhM8AmtbyTIyRpsTbB0D/JauidW4SXsQRQowSwWjHo4QP0DRJBXvOIlxtqEQi50uNzQ==}
-
- '@roblox-ts/path-translator@1.1.0':
- resolution: {integrity: sha512-D0akTmnNYqBw+ZIek5JxocT3BjmbgGOuOy0x1nIIxHBPNLGCpzseToY8jyYs/0mlvnN2xnSP/k8Tv+jvGOQSwQ==}
-
- '@roblox-ts/rojo-resolver@1.1.0':
- resolution: {integrity: sha512-QmvVryu1EeME+3QUoG5j/gHGJoJUaffCgZ92mhlG7cJSd1uyhgpY4CNWriZAwZJYkTlzd5Htkpn+18yDFbOFXA==}
-
- ajv@8.17.1:
- resolution: {integrity: sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==}
-
- ansi-regex@5.0.1:
- resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==}
- engines: {node: '>=8'}
-
- ansi-regex@6.1.0:
- resolution: {integrity: sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==}
- engines: {node: '>=12'}
-
- ansi-styles@4.3.0:
- resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==}
- engines: {node: '>=8'}
-
- ansi-styles@6.2.1:
- resolution: {integrity: sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==}
- engines: {node: '>=12'}
-
- anymatch@3.1.3:
- resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==}
- engines: {node: '>= 8'}
-
- balanced-match@1.0.2:
- resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==}
-
- binary-extensions@2.3.0:
- resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==}
- engines: {node: '>=8'}
-
- brace-expansion@2.0.1:
- resolution: {integrity: sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==}
-
- braces@3.0.3:
- resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==}
- engines: {node: '>=8'}
-
- chokidar@3.6.0:
- resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==}
- engines: {node: '>= 8.10.0'}
-
- cliui@8.0.1:
- resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==}
- engines: {node: '>=12'}
-
- color-convert@2.0.1:
- resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==}
- engines: {node: '>=7.0.0'}
-
- color-name@1.1.4:
- resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==}
-
- cross-spawn@7.0.3:
- resolution: {integrity: sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==}
- engines: {node: '>= 8'}
-
- eastasianwidth@0.2.0:
- resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==}
-
- emoji-regex@8.0.0:
- resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==}
-
- emoji-regex@9.2.2:
- resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==}
-
- escalade@3.2.0:
- resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==}
- engines: {node: '>=6'}
-
- fast-deep-equal@3.1.3:
- resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==}
-
- fast-uri@3.0.3:
- resolution: {integrity: sha512-aLrHthzCjH5He4Z2H9YZ+v6Ujb9ocRuW6ZzkJQOrTxleEijANq4v1TsaPaVG1PZcuurEzrLcWRyYBYXD5cEiaw==}
-
- fill-range@7.1.1:
- resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==}
- engines: {node: '>=8'}
-
- foreground-child@3.3.0:
- resolution: {integrity: sha512-Ld2g8rrAyMYFXBhEqMz8ZAHBi4J4uS1i/CxGMDnjyFWddMXLVcDp051DZfu+t7+ab7Wv6SMqpWmyFIj5UbfFvg==}
- engines: {node: '>=14'}
-
- fs-extra@11.2.0:
- resolution: {integrity: sha512-PmDi3uwK5nFuXh7XDTlVnS17xJS7vW36is2+w3xcv8SVxiB4NyATf4ctkVY5bkSjX0Y4nbvZCq1/EjtEyr9ktw==}
- engines: {node: '>=14.14'}
-
- fsevents@2.3.3:
- resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==}
- engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
- os: [darwin]
-
- function-bind@1.1.2:
- resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==}
-
- get-caller-file@2.0.5:
- resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==}
- engines: {node: 6.* || 8.* || >= 10.*}
-
- glob-parent@5.1.2:
- resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==}
- engines: {node: '>= 6'}
-
- glob@11.0.0:
- resolution: {integrity: sha512-9UiX/Bl6J2yaBbxKoEBRm4Cipxgok8kQYcOPEhScPwebu2I0HoQOuYdIO6S3hLuWoZgpDpwQZMzTFxgpkyT76g==}
- engines: {node: 20 || >=22}
- hasBin: true
-
- graceful-fs@4.2.11:
- resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==}
-
- hasown@2.0.2:
- resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==}
- engines: {node: '>= 0.4'}
-
- is-binary-path@2.1.0:
- resolution: {integrity: sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==}
- engines: {node: '>=8'}
-
- is-core-module@2.15.1:
- resolution: {integrity: sha512-z0vtXSwucUJtANQWldhbtbt7BnL0vxiFjIdDLAatwhDYty2bad6s+rijD6Ri4YuYJubLzIJLUidCh09e1djEVQ==}
- engines: {node: '>= 0.4'}
-
- is-extglob@2.1.1:
- resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==}
- engines: {node: '>=0.10.0'}
-
- is-fullwidth-code-point@3.0.0:
- resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==}
- engines: {node: '>=8'}
-
- is-glob@4.0.3:
- resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==}
- engines: {node: '>=0.10.0'}
-
- is-number@7.0.0:
- resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==}
- engines: {node: '>=0.12.0'}
-
- isexe@2.0.0:
- resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==}
-
- jackspeak@4.0.2:
- resolution: {integrity: sha512-bZsjR/iRjl1Nk1UkjGpAzLNfQtzuijhn2g+pbZb98HQ1Gk8vM9hfbxeMBP+M2/UUdwj0RqGG3mlvk2MsAqwvEw==}
- engines: {node: 20 || >=22}
-
- json-schema-traverse@1.0.0:
- resolution: {integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==}
-
- jsonfile@6.1.0:
- resolution: {integrity: sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==}
-
- kleur@4.1.5:
- resolution: {integrity: sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==}
- engines: {node: '>=6'}
-
- lru-cache@11.0.1:
- resolution: {integrity: sha512-CgeuL5uom6j/ZVrg7G/+1IXqRY8JXX4Hghfy5YE0EhoYQWvndP1kufu58cmZLNIDKnRhZrXfdS9urVWx98AipQ==}
- engines: {node: 20 || >=22}
-
- minimatch@10.0.1:
- resolution: {integrity: sha512-ethXTt3SGGR+95gudmqJ1eNhRO7eGEGIgYA9vnPatK4/etz2MEVDno5GMCibdMTuBMyElzIlgxMna3K94XDIDQ==}
- engines: {node: 20 || >=22}
-
- minipass@7.1.2:
- resolution: {integrity: sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==}
- engines: {node: '>=16 || 14 >=14.17'}
-
- normalize-path@3.0.0:
- resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==}
- engines: {node: '>=0.10.0'}
-
- package-json-from-dist@1.0.1:
- resolution: {integrity: sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==}
-
- path-key@3.1.1:
- resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==}
- engines: {node: '>=8'}
-
- path-parse@1.0.7:
- resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==}
-
- path-scurry@2.0.0:
- resolution: {integrity: sha512-ypGJsmGtdXUOeM5u93TyeIEfEhM6s+ljAhrk5vAvSx8uyY/02OvrZnA0YNGUrPXfpJMgI1ODd3nwz8Npx4O4cg==}
- engines: {node: 20 || >=22}
-
- picomatch@2.3.1:
- resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==}
- engines: {node: '>=8.6'}
-
- readdirp@3.6.0:
- resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==}
- engines: {node: '>=8.10.0'}
-
- require-directory@2.1.1:
- resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==}
- engines: {node: '>=0.10.0'}
-
- require-from-string@2.0.2:
- resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==}
- engines: {node: '>=0.10.0'}
-
- resolve@1.22.8:
- resolution: {integrity: sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==}
- hasBin: true
-
- rimraf@6.0.1:
- resolution: {integrity: sha512-9dkvaxAsk/xNXSJzMgFqqMCuFgt2+KsOFek3TMLfo8NCPfWpBmqwyNn5Y+NX56QUYfCtsyhF3ayiboEoUmJk/A==}
- engines: {node: 20 || >=22}
- hasBin: true
-
- roblox-ts@3.0.0:
- resolution: {integrity: sha512-hwAC2frIFlLJOtHd6F+5opMEhBgfAMK9z5l1mP+bykLBbMO5cn1q5lIwhhXbeh9Pq07rlhF8uGHlmeRLPd/3AA==}
- hasBin: true
-
- shebang-command@2.0.0:
- resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==}
- engines: {node: '>=8'}
-
- shebang-regex@3.0.0:
- resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==}
- engines: {node: '>=8'}
-
- signal-exit@4.1.0:
- resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==}
- engines: {node: '>=14'}
-
- string-width@4.2.3:
- resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==}
- engines: {node: '>=8'}
-
- string-width@5.1.2:
- resolution: {integrity: sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==}
- engines: {node: '>=12'}
-
- strip-ansi@6.0.1:
- resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==}
- engines: {node: '>=8'}
-
- strip-ansi@7.1.0:
- resolution: {integrity: sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==}
- engines: {node: '>=12'}
-
- supports-preserve-symlinks-flag@1.0.0:
- resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==}
- engines: {node: '>= 0.4'}
-
- to-regex-range@5.0.1:
- resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==}
- engines: {node: '>=8.0'}
-
- typescript@5.5.3:
- resolution: {integrity: sha512-/hreyEujaB0w76zKo6717l3L0o/qEUtRgdvUBvlkhoWeOVMjMuHNHk0BRBzikzuGDqNmPQbg5ifMEqsHLiIUcQ==}
- engines: {node: '>=14.17'}
- hasBin: true
-
- typescript@5.6.3:
- resolution: {integrity: sha512-hjcS1mhfuyi4WW8IWtjP7brDrG2cuDZukyrYrSauoXGNgx0S7zceP07adYkJycEr56BOUTNPzbInooiN3fn1qw==}
- engines: {node: '>=14.17'}
- hasBin: true
-
- universalify@2.0.1:
- resolution: {integrity: sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==}
- engines: {node: '>= 10.0.0'}
-
- which@2.0.2:
- resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==}
- engines: {node: '>= 8'}
- hasBin: true
-
- wrap-ansi@7.0.0:
- resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==}
- engines: {node: '>=10'}
-
- wrap-ansi@8.1.0:
- resolution: {integrity: sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==}
- engines: {node: '>=12'}
-
- y18n@5.0.8:
- resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==}
- engines: {node: '>=10'}
-
- yargs-parser@21.1.1:
- resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==}
- engines: {node: '>=12'}
-
- yargs@17.7.2:
- resolution: {integrity: sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==}
- engines: {node: '>=12'}
-
-snapshots:
-
- '@isaacs/cliui@8.0.2':
- dependencies:
- string-width: 5.1.2
- string-width-cjs: string-width@4.2.3
- strip-ansi: 7.1.0
- strip-ansi-cjs: strip-ansi@6.0.1
- wrap-ansi: 8.1.0
- wrap-ansi-cjs: wrap-ansi@7.0.0
-
- '@rbxts/compiler-types@3.0.0-types.0': {}
-
- '@rbxts/object-utils@1.0.4': {}
-
- '@rbxts/pretty-react-hooks@0.6.0':
- dependencies:
- '@rbxts/react': 17.2.1
- '@rbxts/react-roblox': 17.2.1
- '@rbxts/ripple': 0.9.1
- '@rbxts/services': 1.5.5
- '@rbxts/set-timeout': 1.1.2
-
- '@rbxts/react-roblox@17.2.1':
- dependencies:
- '@rbxts/react': 17.2.1
- '@rbxts/react-vendor': 17.2.1
-
- '@rbxts/react-vendor@17.2.1': {}
-
- '@rbxts/react@17.2.1':
- dependencies:
- '@rbxts/react-vendor': 17.2.1
-
- '@rbxts/ripple@0.9.1': {}
-
- '@rbxts/services@1.5.5': {}
-
- '@rbxts/set-timeout@1.1.2':
- dependencies:
- '@rbxts/services': 1.5.5
-
- '@rbxts/types@1.0.813': {}
-
- '@roblox-ts/luau-ast@2.0.0': {}
-
- '@roblox-ts/path-translator@1.1.0':
- dependencies:
- ajv: 8.17.1
- fs-extra: 11.2.0
-
- '@roblox-ts/rojo-resolver@1.1.0':
- dependencies:
- ajv: 8.17.1
- fs-extra: 11.2.0
-
- ajv@8.17.1:
- dependencies:
- fast-deep-equal: 3.1.3
- fast-uri: 3.0.3
- json-schema-traverse: 1.0.0
- require-from-string: 2.0.2
-
- ansi-regex@5.0.1: {}
-
- ansi-regex@6.1.0: {}
-
- ansi-styles@4.3.0:
- dependencies:
- color-convert: 2.0.1
-
- ansi-styles@6.2.1: {}
-
- anymatch@3.1.3:
- dependencies:
- normalize-path: 3.0.0
- picomatch: 2.3.1
-
- balanced-match@1.0.2: {}
-
- binary-extensions@2.3.0: {}
-
- brace-expansion@2.0.1:
- dependencies:
- balanced-match: 1.0.2
-
- braces@3.0.3:
- dependencies:
- fill-range: 7.1.1
-
- chokidar@3.6.0:
- dependencies:
- anymatch: 3.1.3
- braces: 3.0.3
- glob-parent: 5.1.2
- is-binary-path: 2.1.0
- is-glob: 4.0.3
- normalize-path: 3.0.0
- readdirp: 3.6.0
- optionalDependencies:
- fsevents: 2.3.3
-
- cliui@8.0.1:
- dependencies:
- string-width: 4.2.3
- strip-ansi: 6.0.1
- wrap-ansi: 7.0.0
-
- color-convert@2.0.1:
- dependencies:
- color-name: 1.1.4
-
- color-name@1.1.4: {}
-
- cross-spawn@7.0.3:
- dependencies:
- path-key: 3.1.1
- shebang-command: 2.0.0
- which: 2.0.2
-
- eastasianwidth@0.2.0: {}
-
- emoji-regex@8.0.0: {}
-
- emoji-regex@9.2.2: {}
-
- escalade@3.2.0: {}
-
- fast-deep-equal@3.1.3: {}
-
- fast-uri@3.0.3: {}
-
- fill-range@7.1.1:
- dependencies:
- to-regex-range: 5.0.1
-
- foreground-child@3.3.0:
- dependencies:
- cross-spawn: 7.0.3
- signal-exit: 4.1.0
-
- fs-extra@11.2.0:
- dependencies:
- graceful-fs: 4.2.11
- jsonfile: 6.1.0
- universalify: 2.0.1
-
- fsevents@2.3.3:
- optional: true
-
- function-bind@1.1.2: {}
-
- get-caller-file@2.0.5: {}
-
- glob-parent@5.1.2:
- dependencies:
- is-glob: 4.0.3
-
- glob@11.0.0:
- dependencies:
- foreground-child: 3.3.0
- jackspeak: 4.0.2
- minimatch: 10.0.1
- minipass: 7.1.2
- package-json-from-dist: 1.0.1
- path-scurry: 2.0.0
-
- graceful-fs@4.2.11: {}
-
- hasown@2.0.2:
- dependencies:
- function-bind: 1.1.2
-
- is-binary-path@2.1.0:
- dependencies:
- binary-extensions: 2.3.0
-
- is-core-module@2.15.1:
- dependencies:
- hasown: 2.0.2
-
- is-extglob@2.1.1: {}
-
- is-fullwidth-code-point@3.0.0: {}
-
- is-glob@4.0.3:
- dependencies:
- is-extglob: 2.1.1
-
- is-number@7.0.0: {}
-
- isexe@2.0.0: {}
-
- jackspeak@4.0.2:
- dependencies:
- '@isaacs/cliui': 8.0.2
-
- json-schema-traverse@1.0.0: {}
-
- jsonfile@6.1.0:
- dependencies:
- universalify: 2.0.1
- optionalDependencies:
- graceful-fs: 4.2.11
-
- kleur@4.1.5: {}
-
- lru-cache@11.0.1: {}
-
- minimatch@10.0.1:
- dependencies:
- brace-expansion: 2.0.1
-
- minipass@7.1.2: {}
-
- normalize-path@3.0.0: {}
-
- package-json-from-dist@1.0.1: {}
-
- path-key@3.1.1: {}
-
- path-parse@1.0.7: {}
-
- path-scurry@2.0.0:
- dependencies:
- lru-cache: 11.0.1
- minipass: 7.1.2
-
- picomatch@2.3.1: {}
-
- readdirp@3.6.0:
- dependencies:
- picomatch: 2.3.1
-
- require-directory@2.1.1: {}
-
- require-from-string@2.0.2: {}
-
- resolve@1.22.8:
- dependencies:
- is-core-module: 2.15.1
- path-parse: 1.0.7
- supports-preserve-symlinks-flag: 1.0.0
-
- rimraf@6.0.1:
- dependencies:
- glob: 11.0.0
- package-json-from-dist: 1.0.1
-
- roblox-ts@3.0.0:
- dependencies:
- '@roblox-ts/luau-ast': 2.0.0
- '@roblox-ts/path-translator': 1.1.0
- '@roblox-ts/rojo-resolver': 1.1.0
- chokidar: 3.6.0
- fs-extra: 11.2.0
- kleur: 4.1.5
- resolve: 1.22.8
- typescript: 5.5.3
- yargs: 17.7.2
-
- shebang-command@2.0.0:
- dependencies:
- shebang-regex: 3.0.0
-
- shebang-regex@3.0.0: {}
-
- signal-exit@4.1.0: {}
-
- string-width@4.2.3:
- dependencies:
- emoji-regex: 8.0.0
- is-fullwidth-code-point: 3.0.0
- strip-ansi: 6.0.1
-
- string-width@5.1.2:
- dependencies:
- eastasianwidth: 0.2.0
- emoji-regex: 9.2.2
- strip-ansi: 7.1.0
-
- strip-ansi@6.0.1:
- dependencies:
- ansi-regex: 5.0.1
-
- strip-ansi@7.1.0:
- dependencies:
- ansi-regex: 6.1.0
-
- supports-preserve-symlinks-flag@1.0.0: {}
-
- to-regex-range@5.0.1:
- dependencies:
- is-number: 7.0.0
-
- typescript@5.5.3: {}
-
- typescript@5.6.3: {}
-
- universalify@2.0.1: {}
-
- which@2.0.2:
- dependencies:
- isexe: 2.0.0
-
- wrap-ansi@7.0.0:
- dependencies:
- ansi-styles: 4.3.0
- string-width: 4.2.3
- strip-ansi: 6.0.1
-
- wrap-ansi@8.1.0:
- dependencies:
- ansi-styles: 6.2.1
- string-width: 5.1.2
- strip-ansi: 7.1.0
-
- y18n@5.0.8: {}
-
- yargs-parser@21.1.1: {}
-
- yargs@17.7.2:
- dependencies:
- cliui: 8.0.1
- escalade: 3.2.0
- get-caller-file: 2.0.5
- require-directory: 2.1.1
- string-width: 4.2.3
- y18n: 5.0.8
- yargs-parser: 21.1.1
diff --git a/scripts/roblox.sh b/scripts/roblox.sh
new file mode 100755
index 0000000..b2fa355
--- /dev/null
+++ b/scripts/roblox.sh
@@ -0,0 +1,25 @@
+#!/bin/bash
+
+set -o pipefail
+set -e
+
+git clean -Xf build/roblox
+mkdir -p build/roblox
+
+cp README.md .darklua-roblox.json wally.toml wally.lock build/roblox/
+cp -r src build/roblox/
+rm -r build/roblox/src/Stories
+
+cd build/roblox
+
+wally install
+
+rojo sourcemap default.project.json --output sourcemap.json
+wally-package-types --sourcemap sourcemap.json Packages/
+darklua process -c .darklua-roblox.json src out
+
+rm -r src
+mv out src
+
+# Build model
+rojo build default.project.json --output better-react-components.rbxmx
diff --git a/scripts/wally.sh b/scripts/wally.sh
new file mode 100755
index 0000000..7159b77
--- /dev/null
+++ b/scripts/wally.sh
@@ -0,0 +1,20 @@
+#!/bin/bash
+
+set -o pipefail
+set -e
+
+git clean -Xf build/roblox
+mkdir -p build/wally
+
+cp README.md .darklua-wally.json wally.toml wally.lock build/wally/
+cp -r src build/wally/
+rm -r build/wally/src/Stories
+
+cd build/wally
+
+wally install
+rojo sourcemap sourcemap.project.json --output sourcemap.json
+darklua process -c .darklua-wally.json src out
+
+rm -rf src
+mv out src
diff --git a/src/Builder.luau b/src/Builder.luau
new file mode 100644
index 0000000..2b19ce1
--- /dev/null
+++ b/src/Builder.luau
@@ -0,0 +1,66 @@
+local React = require("@pkg/react")
+local Utils = require("@root/Utils")
+
+local ComponentBuilder = {}
+ComponentBuilder.__index = ComponentBuilder
+
+--- Transformer is a function, that transforms user-friendly props to component props and children nodes
+export type Transformer
= (props: P) -> (OP, { React.ReactNode })
+
+export type ComponentBuilderData
= {
+ transformer: Transformer
,
+}
+
+export type ComponentBuilder
= typeof(setmetatable(
+ {} :: ComponentBuilderData
,
+ ComponentBuilder
+))
+
+function ComponentBuilder.new
(transformer: Transformer
): ComponentBuilder
+ local self = {
+ transformer = transformer,
+ }
+
+ return setmetatable(self, ComponentBuilder)
+end
+
+--- Expand
+function ComponentBuilder.expand
(
+ self: ComponentBuilder
,
+ transformer: Transformer
+): ComponentBuilder
+ return ComponentBuilder.new(function(props: P & NP)
+ local rbxProps, rbxChildren = self.transformer(props)
+ local rbxProps2, rbxChildren2 = transformer(props)
+
+ return Utils.Assign(rbxProps, rbxProps2), Utils.Assign(rbxChildren, rbxChildren2)
+ end)
+end
+
+function ComponentBuilder.build
(
+ self: ComponentBuilder
,
+ robloxComponent: string
+): (P, React.Ref) -> React.ReactNode
+ return React.forwardRef(
+ function(props: React.ElementProps & P, ref: React.Ref)
+ local outputProps, outputChildren = self.transformer(props)
+
+ outputProps.ref = ref
+
+ for k, v in pairs(props) do
+ -- 99.98% sure, that this is some special ahh key
+ if typeof(k) == "table" then
+ outputProps[k] = v
+ end
+ end
+
+ return React.createElement(
+ robloxComponent,
+ outputProps,
+ outputChildren,
+ props.children
+ )
+ end
+ ) :: any
+end
+return ComponentBuilder
diff --git a/src/Builders/GuiObjectBuilder.luau b/src/Builders/GuiObjectBuilder.luau
new file mode 100644
index 0000000..65bcd79
--- /dev/null
+++ b/src/Builders/GuiObjectBuilder.luau
@@ -0,0 +1,65 @@
+local React = require("@pkg/react")
+local Builder = require("@root/Builder")
+
+local Utils = require("@root/Utils")
+local Typings = require("@root/Typings")
+
+local ListLayout = require("@root/Helpers/ListLayout")
+
+return Builder.new(function(props: Typings.GuiObjectProps)
+ return {
+ Active = props.Active,
+
+ AnchorPoint = props.AnchorPoint and Utils.ConvertAnchorPoint(props.AnchorPoint),
+ AutomaticSize = props.AutomaticSize,
+
+ BackgroundColor3 = props.BackgroundColor and Utils.ConvertColor(props.BackgroundColor),
+ BackgroundTransparency = props.NoBackground and 1 or props.BackgroundTransparency,
+
+ BorderSizePixel = 0, -- we are using `UIStroke` instead.
+
+ ClipsDescendants = props.ClipsDescendants,
+ Interactable = props.Interactable,
+
+ LayoutOrder = props.LayoutOrder,
+
+ Position = props.Position,
+ Rotation = props.Rotation,
+
+ Selectable = props.Selectable,
+
+ Size = props.Size,
+ SizeConstraint = props.SizeConstraint,
+
+ Transparency = props.Transparency,
+ Visible = props.Visible,
+
+ ZIndex = props.ZIndex,
+ }, {
+ BRCBorder = props.Border and React.createElement("UIStroke", {
+ Color = props.Border.Color and Utils.ConvertColor(props.Border.Color),
+ Thickness = props.Border.Thickness,
+ LineJoinMode = props.Border.LineJoinMode,
+ Transparency = props.Border.Transparency,
+ }),
+ BRCCorner = props.CornerRadius and React.createElement("UICorner", {
+ CornerRadius = Utils.ConvertUDim(props.CornerRadius),
+ }),
+ BRCAspectRatio = props.Aspect and React.createElement("UIAspectRatioConstraint", {
+ AspectRatio = props.Aspect.Ratio,
+ AspectType = props.Aspect.Type,
+ DominantAxis = props.Aspect.DominantAxis,
+ }),
+ BRCPadding = props.Padding
+ and React.createElement("UIPadding", Utils.ConvertPadding(props.Padding):getValue()),
+
+ BRCSizeConstraint = (props.MinSize ~= nil or props.MaxSize ~= nil)
+ and React.createElement("UISizeConstraint", {
+ MinSize = props.MinSize,
+ MaxSize = props.MaxSize,
+ }),
+ BRCListLayout = props.ListLayout and React.createElement(ListLayout, props.ListLayout),
+ BRCFlexItem = props.FlexMode
+ and React.createElement("UIFlexItem", { FlexMode = props.FlexMode }),
+ }
+end)
diff --git a/src/Builders/TextBoxBuilder.luau b/src/Builders/TextBoxBuilder.luau
new file mode 100644
index 0000000..49289d3
--- /dev/null
+++ b/src/Builders/TextBoxBuilder.luau
@@ -0,0 +1,17 @@
+local Typings = require("@root/Typings")
+local Utils = require("@root/Utils")
+
+local TextBuilder = require("./TextBuilder")
+
+return TextBuilder:expand(function(props: Typings.TextBoxProps)
+ return {
+ ClearTextOnFocus = props.ClearTextOnFocus,
+ CursorPosition = props.CursorPosition,
+ MultiLine = props.MultiLine,
+ PlaceholderColor3 = props.PlaceholderColor and Utils.ConvertColor(props.PlaceholderColor),
+ PlaceholderText = props.PlaceholderText,
+ SelectionStart = props.SelectionStart,
+ ShowNativeInput = props.ShowNativeInput,
+ TextEditable = props.TextEditable,
+ }, {}
+end)
diff --git a/src/Builders/TextBuilder.luau b/src/Builders/TextBuilder.luau
new file mode 100644
index 0000000..db4f2bb
--- /dev/null
+++ b/src/Builders/TextBuilder.luau
@@ -0,0 +1,38 @@
+local React = require("@pkg/react")
+
+local Utils = require("@root/Utils")
+local Typings = require("@root/Typings")
+
+local GuiObjectBuilder = require("./GuiObjectBuilder")
+
+--- Builder for text-based objects (`TextLabel`, `TextButton`, etc.)
+return GuiObjectBuilder:expand(function(props: Typings.TextProps)
+ return {
+ FontFace = props.Font and Utils.ConvertFont(props.Font),
+ LineHeight = props.LineHeight,
+
+ MaxVisibleGraphemes = props.MaxVisibleGraphemes,
+
+ RichText = props.RichText,
+ Text = props.Text,
+
+ TextColor3 = props.TextColor and Utils.ConvertColor(props.TextColor),
+ TextDirection = props.TextDirection,
+ TextFits = props.TextFits,
+ TextTransparency = props.TextTransparency,
+ TextTruncate = props.TextTruncate,
+ TextWrapped = props.TextWrapped,
+
+ TextXAlignment = props.TextXAlignment,
+ TextYAlignment = props.TextYAlignment,
+
+ TextSize = props.TextSize == "auto" and 1 or props.TextSize,
+ TextScaled = props.TextSize == "auto",
+ }, {
+ BRCTextSizeConstraint = (props.MinTextSize ~= nil or props.MaxTextSize ~= nil)
+ and React.createElement("UITextSizeConstraint", {
+ MinTextSize = props.MinTextSize,
+ MaxTextSize = props.MaxTextSize,
+ }),
+ }
+end)
diff --git a/src/Builders/TextButtonBuilder.luau b/src/Builders/TextButtonBuilder.luau
new file mode 100644
index 0000000..321b4bc
--- /dev/null
+++ b/src/Builders/TextButtonBuilder.luau
@@ -0,0 +1,14 @@
+local Typings = require("@root/Typings")
+
+local TextLabelBuilder = require("./TextBuilder")
+
+return TextLabelBuilder:expand(function(props: Typings.TextButtonProps)
+ return {
+ AutoButtonColor = props.AutoButtonColor,
+ HoverHapticEffect = props.HoverHapticEffect,
+ PressHapticEffect = props.PressHapticEffect,
+ Modal = props.Modal,
+ Selected = props.Selected,
+ Style = props.Style,
+ }, {}
+end)
diff --git a/src/Builders/init.luau b/src/Builders/init.luau
new file mode 100644
index 0000000..f5cda9e
--- /dev/null
+++ b/src/Builders/init.luau
@@ -0,0 +1,6 @@
+return {
+ GuiObjectBuilder = require(script.GuiObjectBuilder),
+ TextBuilder = require(script.TextBuilder),
+ TextBoxBuilder = require(script.TextBoxBuilder),
+ TextButtonBuilder = require(script.TextButtonBuilder),
+}
diff --git a/src/Components/Frame.luau b/src/Components/Frame.luau
new file mode 100644
index 0000000..f209985
--- /dev/null
+++ b/src/Components/Frame.luau
@@ -0,0 +1,3 @@
+local GuiObjectBuilder = require("@root/Builders/GuiObjectBuilder")
+
+return GuiObjectBuilder:build("Frame")
diff --git a/src/Components/ScreenGui.luau b/src/Components/ScreenGui.luau
new file mode 100644
index 0000000..479da28
--- /dev/null
+++ b/src/Components/ScreenGui.luau
@@ -0,0 +1,13 @@
+local Builder = require("@root/Builder")
+local Typings = require("@root/Typings")
+
+return Builder.new(function(props: Typings.ScreenGuiProps)
+ return {
+ Enabled = props.Enabled,
+ ResetOnSpawn = props.ResetOnSpawn,
+ ZIndexBehavior = props.ZIndexBehavior,
+
+ ScreenInsets = props.ScreenInsets,
+ DisplayOrder = props.DisplayOrder,
+ }, {}
+end):build("ScreenGui")
diff --git a/src/Components/ScrollingFrame.luau b/src/Components/ScrollingFrame.luau
new file mode 100644
index 0000000..8751656
--- /dev/null
+++ b/src/Components/ScrollingFrame.luau
@@ -0,0 +1,28 @@
+local GuiObjectBuilder = require("@root/Builders/GuiObjectBuilder")
+
+local Utils = require("@root/Utils")
+local Typings = require("@root/Typings")
+
+return GuiObjectBuilder:expand(function(props: Typings.ScrollingFrameProps)
+ return {
+ AutomaticCanvasSize = props.AutomaticCanvasSize,
+ CanvasPosition = props.CanvasPosition,
+ CanvasSize = props.CanvasSize,
+
+ ScrollingDirection = props.ScrollingDirection,
+ ScrollingEnabled = props.ScrollingEnabled,
+
+ TopImage = props.ScrollBar and props.ScrollBar.TopImage,
+ MidImage = props.ScrollBar and props.ScrollBar.MidImage,
+ BottomImage = props.ScrollBar and props.ScrollBar.BottomImage,
+
+ ScrollBarImageColor3 = (props.ScrollBar and props.ScrollBar.Color)
+ and Utils.ConvertColor(props.ScrollBar.Color),
+ ScrollBarImageTransparency = props.ScrollBar and props.ScrollBar.Transparency,
+ ScrollBarThickness = props.ScrollBar and props.ScrollBar.Thickness,
+
+ HorizontalScrollBarInset = props.ScrollBar and props.ScrollBar.HorizontalInset,
+ VerticalScrollBarInset = props.ScrollBar and props.ScrollBar.VerticalInset,
+ VerticalScrollBarPosition = props.ScrollBar and props.ScrollBar.VerticalPosition,
+ }, {}
+end):build("ScrollingFrame")
diff --git a/src/Components/TextBox.luau b/src/Components/TextBox.luau
new file mode 100644
index 0000000..6531c20
--- /dev/null
+++ b/src/Components/TextBox.luau
@@ -0,0 +1,3 @@
+local TextBuilder = require("@root/Builders/TextBuilder")
+
+return TextBuilder:build("TextBox")
diff --git a/src/Components/TextButton.luau b/src/Components/TextButton.luau
new file mode 100644
index 0000000..a479a02
--- /dev/null
+++ b/src/Components/TextButton.luau
@@ -0,0 +1,3 @@
+local TextButtonBuilder = require("@root/Builders/TextButtonBuilder")
+
+return TextButtonBuilder:build("TextButton")
diff --git a/src/Components/TextLabel.luau b/src/Components/TextLabel.luau
new file mode 100644
index 0000000..c313b13
--- /dev/null
+++ b/src/Components/TextLabel.luau
@@ -0,0 +1,3 @@
+local TextBuilder = require("@root/Builders/TextBuilder")
+
+return TextBuilder:build("TextLabel")
diff --git a/src/Components/init.luau b/src/Components/init.luau
new file mode 100644
index 0000000..9aaa35f
--- /dev/null
+++ b/src/Components/init.luau
@@ -0,0 +1,8 @@
+return {
+ ScreenGui = require(script.ScreenGui),
+ Frame = require(script.Frame),
+ ScrollingFrame = require(script.ScrollingFrame),
+ TextButton = require(script.TextButton),
+ TextLabel = require(script.TextLabel),
+ TextBox = require(script.TextBox),
+}
diff --git a/src/Helpers/ListLayout.luau b/src/Helpers/ListLayout.luau
new file mode 100644
index 0000000..55c01ec
--- /dev/null
+++ b/src/Helpers/ListLayout.luau
@@ -0,0 +1,32 @@
+local React = require("@pkg/react")
+
+local Utils = require("@root/Utils")
+local Typings = require("@root/Typings")
+
+return function(props: Typings.ListLayoutSettings)
+ local rbxProps = React.useMemo(function()
+ local MainAxis = props.Direction or Enum.FillDirection.Vertical
+ local CrossAxis = if MainAxis == Enum.FillDirection.Horizontal
+ then Enum.FillDirection.Vertical
+ else Enum.FillDirection.Horizontal
+
+ local MainAxisResult = if props.JustifyContent ~= nil
+ then Utils.ConvertFlexValue(MainAxis, props.JustifyContent)
+ else {}
+ local CrossAxisResult = if props.AlignItems ~= nil
+ then Utils.ConvertFlexValue(CrossAxis, props.AlignItems)
+ else {}
+
+ local result = {
+ Padding = props.Gap and Utils.ConvertUDim(props.Gap),
+
+ FillDirection = MainAxis,
+ SortOrder = props.Order,
+ Wraps = props.Wraps,
+ }
+
+ return Utils.MergeMulti(result, MainAxisResult, CrossAxisResult)
+ end, { props })
+
+ return React.createElement("UIListLayout", rbxProps)
+end
diff --git a/src/Stories/Components/Frame.story.luau b/src/Stories/Components/Frame.story.luau
new file mode 100644
index 0000000..6c4787c
--- /dev/null
+++ b/src/Stories/Components/Frame.story.luau
@@ -0,0 +1,34 @@
+local React = require("@pkg/react")
+local ReactRoblox = require("@pkg/react-roblox")
+local UILabs = require("@dev-pkg/ui-labs")
+
+local BRC = require("@root/init")
+
+local story = {
+ react = React,
+ reactRoblox = ReactRoblox,
+ controls = {
+ AnchorPoint = UILabs.EnumList({
+ TopLeft = "tl",
+ TopCenter = "t",
+ TopRight = "tr",
+ MiddleLeft = "ml",
+ MiddleCenter = "m",
+ MiddleRight = "mr",
+ BottomLeft = "bl",
+ BottomCenter = "b",
+ BottomRight = "br",
+ }, "MiddleCenter"),
+ BackgroundColor = Color3.fromHex("#FF0000"),
+ },
+ story = function(props)
+ return React.createElement(BRC.Frame, {
+ AnchorPoint = props.controls.AnchorPoint,
+ Position = UDim2.new(0.5, 0, 0.5, 0),
+ Size = UDim2.new(0.5, 0, 0.5, 0),
+ BackgroundColor = props.controls.BackgroundColor,
+ })
+ end,
+}
+
+return story
diff --git a/src/Stories/Components/ScrollingFrame.story.luau b/src/Stories/Components/ScrollingFrame.story.luau
new file mode 100644
index 0000000..42b800c
--- /dev/null
+++ b/src/Stories/Components/ScrollingFrame.story.luau
@@ -0,0 +1,24 @@
+local React = require("@pkg/react")
+local ReactRoblox = require("@pkg/react-roblox")
+local UILabs = require("@dev-pkg/ui-labs")
+
+local BRC = require("@root/init")
+
+return {
+ react = React,
+ reactRoblox = ReactRoblox,
+ controls = {},
+ story = function(props)
+ -- TODO: complete this story
+ return React.createElement(BRC.ScrollingFrame, {
+ AnchorPoint = "m",
+ Position = UDim2.new(0.5, 0, 0.5, 0),
+ Size = UDim2.new(0.5, 0, 0.5, 0),
+
+ BackgroundColor = "#C00000",
+ CanvasSize = UDim2.new(1, 0, 2, 0),
+ }, {
+ React.createElement("Frame", { Size = UDim2.new(0, 100, 0, 100) }),
+ })
+ end,
+}
diff --git a/src/Stories/Components/TextButton.story.luau b/src/Stories/Components/TextButton.story.luau
new file mode 100644
index 0000000..6ad4a13
--- /dev/null
+++ b/src/Stories/Components/TextButton.story.luau
@@ -0,0 +1,36 @@
+local React = require("@pkg/react")
+local ReactRoblox = require("@pkg/react-roblox")
+local UILabs = require("@dev-pkg/ui-labs")
+
+local BRC = require("@root/init")
+
+local function TextButtonStory(props)
+ local count, setCount = React.useState(0)
+
+ return React.createElement(BRC.TextButton, {
+ AnchorPoint = "m",
+ Position = UDim2.new(0.5, 0, 0.5, 0),
+ Size = UDim2.new(0.5, 0, 0.5, 0),
+ BackgroundColor = "#080808",
+
+ TextColor = "#FFF",
+ TextSize = 18,
+ Text = ("This button was clicked %d times. How cool is that???"):format(count),
+ TextWrapped = true,
+
+ [React.Event.Activated] = function()
+ setCount(function(oldCount)
+ return oldCount + 1
+ end)
+ end,
+ })
+end
+
+return {
+ react = React,
+ reactRoblox = ReactRoblox,
+ controls = {},
+ story = function(props)
+ return React.createElement(TextButtonStory, props)
+ end,
+}
diff --git a/src/Stories/Components/TextLabel.story.luau b/src/Stories/Components/TextLabel.story.luau
new file mode 100644
index 0000000..37cadfe
--- /dev/null
+++ b/src/Stories/Components/TextLabel.story.luau
@@ -0,0 +1,37 @@
+local React = require("@pkg/react")
+local ReactRoblox = require("@pkg/react-roblox")
+local UILabs = require("@dev-pkg/ui-labs")
+
+local BRC = require("@root/init")
+
+local Fonts = {}
+for _, font in pairs(Enum.Font:GetEnumItems()) do
+ Fonts[tostring(font):gsub("Enum.Font.", "")] = font
+end
+
+print(Fonts)
+
+return {
+ react = React,
+ reactRoblox = ReactRoblox,
+ controls = {
+ Text = "Hello, world!",
+ Font = UILabs.EnumList(Fonts, "Arimo"),
+ TextColor = Color3.fromHex("#FFFFFF"),
+ BackgroundColor = Color3.fromHex("#CC0000"),
+ CornerRadius = 0,
+ },
+ story = function(props)
+ return React.createElement(BRC.TextLabel, {
+ AnchorPoint = "m",
+ Position = UDim2.new(0.5, 0, 0.5, 0),
+ Size = UDim2.new(0.5, 0, 0.5, 0),
+ BackgroundColor = props.controls.BackgroundColor,
+
+ Font = Font.fromEnum(props.controls.Font),
+ TextColor = props.controls.TextColor,
+ TextSize = 24,
+ Text = props.controls.Text,
+ })
+ end,
+}
diff --git a/src/Stories/Examples/Todos.story.luau b/src/Stories/Examples/Todos.story.luau
new file mode 100644
index 0000000..eb5c55b
--- /dev/null
+++ b/src/Stories/Examples/Todos.story.luau
@@ -0,0 +1,163 @@
+local React = require("@pkg/react")
+local ReactRoblox = require("@pkg/react-roblox")
+local Collections = require("@pkg/collections")
+
+local UILabs = require("@dev-pkg/ui-labs")
+
+local BRC = require("@root/")
+
+type Todo = { text: string, completed: boolean }
+
+local function MenuStory(props)
+ local todos, setTodos = React.useState({} :: { Todo })
+ local input, setInput = React.useBinding("")
+
+ return React.createElement(BRC.Frame, {
+ -- PRO TIP: use this utillity to calcalate aspect ratio for component:
+ -- > ref = BRC.Utils.CalculateAspectRatio,
+ AnchorPoint = "m",
+ Size = UDim2.new(0.5, 0, 0.8, 0),
+ Position = UDim2.new(0.5, 0, 0.5, 0),
+ Aspect = { Ratio = 1.6 },
+
+ BackgroundColor = "#FFF",
+ BackgroundTransparency = 0.3,
+
+ CornerRadius = 8 + 8, -- inner corner + padding
+ Padding = 8,
+
+ ListLayout = {
+ Gap = 8,
+ Order = Enum.SortOrder.LayoutOrder,
+ },
+ }, {
+ Header = React.createElement(BRC.Frame, {
+ LayoutOrder = 1,
+
+ Size = UDim2.new(1, 0, 0, 48),
+ NoBackground = true,
+
+ ListLayout = {
+ Gap = 8,
+ Direction = Enum.FillDirection.Horizontal,
+ Order = Enum.SortOrder.LayoutOrder,
+ },
+ }, {
+ TodoText = React.createElement(BRC.TextBox, {
+ LayoutOrder = 1,
+
+ Size = UDim2.new(0, 0, 1, 0),
+ FlexMode = Enum.UIFlexMode.Fill,
+
+ CornerRadius = 8,
+
+ Text = input,
+ [React.Change.Text] = function(inst)
+ setInput(inst.Text)
+ end,
+ }),
+ CreateTodo = React.createElement(BRC.TextButton, {
+ LayoutOrder = 2,
+ Size = UDim2.new(0.2, 0, 1, 0),
+
+ CornerRadius = 8,
+
+ BackgroundColor = "#00a63e",
+ TextColor = "#FFFFFF",
+
+ Text = "Add",
+
+ [React.Event.Activated] = function()
+ setTodos(function(todos)
+ local text = input:getValue()
+ setInput("")
+
+ return BRC.Utils.MergeMulti(
+ todos,
+ { [#todos + 1] = { text = text, completed = false } }
+ )
+ end)
+ end,
+ }),
+ }),
+
+ Content = React.createElement(
+ BRC.ScrollingFrame,
+ {
+ LayoutOrder = 2,
+
+ NoBackground = true,
+
+ Padding = { 6, 18, 6, 0 },
+ Size = UDim2.new(1, 0, 1, 0),
+ FlexMode = Enum.UIFlexMode.Fill,
+
+ ListLayout = {
+ Gap = 8,
+ Direction = Enum.FillDirection.Vertical,
+ Order = Enum.SortOrder.LayoutOrder,
+ },
+ },
+
+ Collections.Array.map(todos, function(todo: Todo, index: number)
+ return React.createElement(BRC.Frame, {
+ LayoutOrder = 10 + index,
+ Size = UDim2.new(1, 0, 0, 42),
+
+ CornerRadius = 8,
+ Padding = 4,
+
+ ListLayout = {
+ Gap = 8,
+ Direction = Enum.FillDirection.Horizontal,
+ Order = Enum.SortOrder.LayoutOrder,
+ AlignItems = "center",
+ },
+ }, {
+ React.createElement(BRC.TextLabel, {
+ LayoutOrder = 1,
+
+ Size = UDim2.new(1, 0, 1, 0),
+ FlexMode = Enum.UIFlexMode.Fill,
+ NoBackground = true,
+
+ Padding = { 0, 8 },
+
+ TextXAlignment = Enum.TextXAlignment.Left,
+ Text = todo.text,
+ }),
+ React.createElement(BRC.TextButton, {
+ LayoutOrder = 2,
+
+ AnchorPoint = "mr",
+ Size = UDim2.new(0.1, 0, 1, 0),
+ Aspect = { Ratio = 1 },
+
+ CornerRadius = 4,
+
+ BackgroundColor = todo.completed and "#DC2626" or "#00A63E",
+ TextColor = "#FFF",
+ Text = todo.completed and "X" or "√",
+
+ [React.Event.Activated] = function()
+ setTodos(function(todos)
+ return BRC.Utils.MergeMulti(todos, {
+ [index] = { completed = not todos[index].completed },
+ })
+ end)
+ end,
+ }),
+ })
+ end)
+ ),
+ })
+end
+
+return {
+ react = React,
+ reactRoblox = ReactRoblox,
+ controls = {},
+ story = function(props)
+ return React.createElement(MenuStory, props)
+ end,
+}
diff --git a/src/Stories/Layouts/FlexItem.story.luau b/src/Stories/Layouts/FlexItem.story.luau
new file mode 100644
index 0000000..0cc6947
--- /dev/null
+++ b/src/Stories/Layouts/FlexItem.story.luau
@@ -0,0 +1,57 @@
+local React = require("@pkg/react")
+local ReactRoblox = require("@pkg/react-roblox")
+local UILabs = require("@dev-pkg/ui-labs")
+
+local BRC = require("@root/init")
+
+local FlexAlignmentValues = {
+ Start = "start",
+ End = "end",
+ Center = "center",
+ SpaceBetween = "space-between",
+ SpaceAround = "space-around",
+ SpaceEvenly = "space-evenly",
+ Stretch = "stretch",
+}
+
+return {
+ react = React,
+ reactRoblox = ReactRoblox,
+ controls = {
+ ParentSizePercent = UILabs.Slider(70, 0, 100, 1),
+ },
+ story = function(props)
+ return React.createElement(BRC.Frame, {
+ AnchorPoint = "m",
+ Position = UDim2.new(0.5, 0, 0.5, 0),
+ Size = UDim2.new(props.controls.ParentSizePercent / 100, 0, 0.25, 0),
+ BackgroundColor = "#FFF",
+
+ Padding = props.controls.Gap,
+ ListLayout = {
+ Gap = props.controls.Gap,
+ Direction = Enum.FillDirection.Horizontal,
+ },
+ }, {
+ React.createElement(BRC.TextLabel, {
+ Size = UDim2.new(0.5, 0, 1, 0),
+ BackgroundColor = "#E80000",
+ Text = "This component has SizeX = {0.5, 0} (NOTE: Scale units), FlexMode = Fill",
+ TextWrapped = true,
+
+ TextSize = 10,
+
+ FlexMode = Enum.UIFlexMode.Fill,
+ }),
+ React.createElement(BRC.TextLabel, {
+ Size = UDim2.new(0, 300, 1, 0),
+ BackgroundColor = "#080808",
+ TextColor = "#FFFFFF",
+
+ Text = "But this component has SizeX = {0, 300} (NOTE: Offset units). Without FlexMode, this would be painful to recreate",
+ TextWrapped = true,
+ TextSize = 10,
+ }),
+ })
+ end,
+}
diff --git a/src/Stories/Layouts/List.story.luau b/src/Stories/Layouts/List.story.luau
new file mode 100644
index 0000000..20dee48
--- /dev/null
+++ b/src/Stories/Layouts/List.story.luau
@@ -0,0 +1,56 @@
+local React = require("@pkg/react")
+local ReactRoblox = require("@pkg/react-roblox")
+local UILabs = require("@dev-pkg/ui-labs")
+
+local BRC = require("@root/init")
+
+local FlexAlignmentValues = {
+ Start = "start",
+ End = "end",
+ Center = "center",
+ SpaceBetween = "space-between",
+ SpaceAround = "space-around",
+ SpaceEvenly = "space-evenly",
+ Stretch = "stretch",
+}
+
+return {
+ react = React,
+ reactRoblox = ReactRoblox,
+ controls = {
+ Gap = UILabs.Slider(0, 0, 24, 1),
+ JustifyContent = UILabs.EnumList(FlexAlignmentValues, "Center"),
+ AlignItems = UILabs.EnumList(FlexAlignmentValues, "Center"),
+ },
+ story = function(props)
+ return React.createElement(BRC.Frame, {
+ AnchorPoint = "m",
+ Position = UDim2.new(0.5, 0, 0.5, 0),
+ Size = UDim2.new(0.5, 0, 0.75, 0),
+ BackgroundColor = "#FFF",
+
+ Padding = props.controls.Gap,
+ ListLayout = {
+ -- JustifyContent is alignment at main axis
+ -- AlignItems is alignment at cross axis
+
+ Gap = props.controls.Gap,
+ JustifyContent = props.controls.JustifyContent, -- CSS like values: start, end, center, space-between, space-around, space-evenly and stretch
+ AlignItems = props.controls.AlignItems, -- same as above
+ },
+ }, {
+ React.createElement(
+ BRC.Frame,
+ { Size = UDim2.new(0.5, 0, 0.25, 0), BackgroundColor = "#E80000" }
+ ),
+ React.createElement(
+ BRC.Frame,
+ { Size = UDim2.new(0.5, 0, 0.25, 0), BackgroundColor = "#00E800" }
+ ),
+ React.createElement(
+ BRC.Frame,
+ { Size = UDim2.new(0.5, 0, 0.25, 0), BackgroundColor = "#0000E8" }
+ ),
+ })
+ end,
+}
diff --git a/src/Stories/Modifiers/AspectRatio.story.luau b/src/Stories/Modifiers/AspectRatio.story.luau
new file mode 100644
index 0000000..e42328d
--- /dev/null
+++ b/src/Stories/Modifiers/AspectRatio.story.luau
@@ -0,0 +1,29 @@
+local React = require("@pkg/react")
+local ReactRoblox = require("@pkg/react-roblox")
+local UILabs = require("@dev-pkg/ui-labs")
+
+local BRC = require("@root/init")
+
+local story = {
+ react = React,
+ reactRoblox = ReactRoblox,
+ controls = {
+ AspectRatio = UILabs.Slider(1, 0.5, 5, 0.1),
+ },
+ story = function(props)
+ return React.createElement(BRC.Frame, {
+ AnchorPoint = "m",
+ Position = UDim2.new(0.5, 0, 0.5, 0),
+ Size = UDim2.new(0.5, 0, 0.5, 0),
+ BackgroundColor = "#FFF",
+
+ Aspect = {
+ Ratio = props.controls.AspectRatio, -- number
+ Type = Enum.AspectType.FitWithinMaxSize,
+ DominantAxis = Enum.DominantAxis.Width,
+ },
+ })
+ end,
+}
+
+return story
diff --git a/src/Stories/Modifiers/Padding.story.luau b/src/Stories/Modifiers/Padding.story.luau
new file mode 100644
index 0000000..ed050cf
--- /dev/null
+++ b/src/Stories/Modifiers/Padding.story.luau
@@ -0,0 +1,47 @@
+local React = require("@pkg/react")
+local ReactRoblox = require("@pkg/react-roblox")
+local UILabs = require("@dev-pkg/ui-labs")
+
+local BRC = require("@root/init")
+
+local function PaddingStory(props)
+ local Padding = React.useMemo(function()
+ local Padding = {}
+ for v in string.gmatch(props.controls.Padding, "%d+") do
+ Padding[#Padding + 1] = tonumber(v)
+ end
+ return Padding
+ end, { props.controls.Padding })
+
+ return React.createElement(
+ BRC.Frame,
+ {
+ AnchorPoint = "m",
+ Position = UDim2.new(0.5, 0, 0.5, 0),
+ Size = UDim2.new(0.5, 0, 0.5, 0),
+ BackgroundColor = "#F00",
+
+ Padding = Padding,
+ },
+ React.createElement(BRC.TextLabel, {
+ Size = UDim2.new(1, 0, 1, 0),
+ BackgroundColor = "#FFF",
+ Text = ("Padding = {%s}"):format(table.concat(Padding, ", ")),
+ Font = Enum.Font.Roboto,
+ TextSize = 30,
+ })
+ )
+end
+
+local story = {
+ react = React,
+ reactRoblox = ReactRoblox,
+ controls = {
+ Padding = "15",
+ },
+ story = function(props)
+ return React.createElement(PaddingStory, props)
+ end,
+}
+
+return story
diff --git a/src/Stories/Modifiers/SizeConstraint.story.luau b/src/Stories/Modifiers/SizeConstraint.story.luau
new file mode 100644
index 0000000..197c617
--- /dev/null
+++ b/src/Stories/Modifiers/SizeConstraint.story.luau
@@ -0,0 +1,25 @@
+local React = require("@pkg/react")
+local ReactRoblox = require("@pkg/react-roblox")
+local UILabs = require("@dev-pkg/ui-labs")
+
+local BRC = require("@root/init")
+
+return {
+ react = React,
+ reactRoblox = ReactRoblox,
+ controls = {
+ MinSizeX = UILabs.Slider(100, 0, 255, 1),
+ MaxSizeX = UILabs.Slider(200, 0, 500, 1),
+ },
+ story = function(props)
+ return React.createElement(BRC.Frame, {
+ AnchorPoint = "m",
+ Position = UDim2.new(0.5, 0, 0.5, 0),
+ Size = UDim2.new(0.5, 0, 0.5, 0),
+ BackgroundColor = "#FFF",
+
+ MinSize = Vector2.new(props.controls.MinSizeX, 0),
+ MaxSize = Vector2.new(props.controls.MaxSizeX, math.huge),
+ })
+ end,
+}
diff --git a/src/Stories/Modifiers/TextSizeConstraint.story.luau b/src/Stories/Modifiers/TextSizeConstraint.story.luau
new file mode 100644
index 0000000..1d85b5d
--- /dev/null
+++ b/src/Stories/Modifiers/TextSizeConstraint.story.luau
@@ -0,0 +1,29 @@
+local React = require("@pkg/react")
+local ReactRoblox = require("@pkg/react-roblox")
+local UILabs = require("@dev-pkg/ui-labs")
+
+local BRC = require("@root/init")
+
+return {
+ react = React,
+ reactRoblox = ReactRoblox,
+ controls = {
+ SizeY = UILabs.Slider(100, 0, 500),
+ MaxTextSize = UILabs.Slider(20, 1, 100),
+ },
+ story = function(props)
+ return React.createElement(BRC.TextLabel, {
+ AnchorPoint = "m",
+ Position = UDim2.new(0.5, 0, 0.5, 0),
+ Size = UDim2.new(0.5, 0, 0, props.controls.SizeY),
+ BackgroundColor = "#FFF",
+ TextColor = "#F00",
+
+ Text = "creepy ahh text",
+ TextSize = "auto",
+ Font = Enum.Font.Creepster,
+
+ MaxTextSize = props.controls.MaxTextSize,
+ })
+ end,
+}
diff --git a/src/Stories/Modifiers/UICorner.story.luau b/src/Stories/Modifiers/UICorner.story.luau
new file mode 100644
index 0000000..4bf16b1
--- /dev/null
+++ b/src/Stories/Modifiers/UICorner.story.luau
@@ -0,0 +1,24 @@
+local React = require("@pkg/react")
+local ReactRoblox = require("@pkg/react-roblox")
+local UILabs = require("@dev-pkg/ui-labs")
+
+local BRC = require("@root/init")
+
+local story = {
+ react = React,
+ reactRoblox = ReactRoblox,
+ controls = {
+ CornerRadius = UILabs.Slider(10, 0, 150, 1),
+ },
+ story = function(props)
+ return React.createElement(BRC.Frame, {
+ AnchorPoint = "m",
+ Position = UDim2.new(0.5, 0, 0.5, 0),
+ Size = UDim2.new(0.5, 0, 0.5, 0),
+ BackgroundColor = "#FFF",
+ CornerRadius = props.controls.CornerRadius, -- UDim | number
+ })
+ end,
+}
+
+return story
diff --git a/src/Stories/Modifiers/UIStroke.story.luau b/src/Stories/Modifiers/UIStroke.story.luau
new file mode 100644
index 0000000..24bc959
--- /dev/null
+++ b/src/Stories/Modifiers/UIStroke.story.luau
@@ -0,0 +1,30 @@
+local React = require("@pkg/react")
+local ReactRoblox = require("@pkg/react-roblox")
+local UILabs = require("@dev-pkg/ui-labs")
+
+local BRC = require("@root/init")
+
+local story = {
+ react = React,
+ reactRoblox = ReactRoblox,
+ controls = {
+ Color = Color3.fromHex("#FF0000"),
+ Thickness = UILabs.Slider(5, 0, 50),
+ },
+ story = function(props)
+ return React.createElement(BRC.Frame, {
+ AnchorPoint = "m",
+ Position = UDim2.new(0.5, 0, 0.5, 0),
+ Size = UDim2.new(0.5, 0, 0.5, 0),
+ BackgroundColor = "#FFF",
+ CornerRadius = 15,
+
+ Border = {
+ Color = props.controls.Color, -- Color3 | number
+ Thickness = props.controls.Thickness, -- number
+ },
+ })
+ end,
+}
+
+return story
diff --git a/src/Typings.luau b/src/Typings.luau
new file mode 100644
index 0000000..fc4c222
--- /dev/null
+++ b/src/Typings.luau
@@ -0,0 +1,218 @@
+local React = require("@pkg/react")
+
+--- User-friendly anchor points
+---
+--- center
+--- left | right
+--- | | |
+--- `tl`, `t`, `tr` – top
+--- `ml`, `m`, `mr` – middle
+--- `bl`, `b`, `br` – bottom
+export type AnchorPoint = "tl" | "t" | "tr" | "ml" | "m" | "mr" | "bl" | "b" | "br" | Vector2
+
+--- Color
+---
+--- It's either `Color3` or hexcode of color (e.g. `#FF0000`)
+export type Color = Color3 | string
+
+--- Text Size
+---
+--- `"auto"` enables `TextScaled` property. Otherwise sets TextSize
+export type TextSize = number | "auto"
+
+--- Font
+export type FontValue = Font | EnumItem
+
+--- UDim
+---
+--- Takes `UDim` or `number` as offset
+export type UDimValue = UDim | number
+
+--- Padding (like in CSS)
+---
+--- There's 4 scenarios
+--- 1. Value is `UDimValue` or `{UDimValue}`: padding `UDimValue` is applied to all sides
+--- 2. Value is `{UDimValue, UDimValue}`: first value is vertical padding, second - horizontal padding
+--- 3. Value is `{UDimValue, UDimValue, UDimValue}`: where each value is {Top, Horizontal, Bottom}
+--- 4. Value is `{UDimValue, UDimValue, UDimValue, UDimValue}`: where each value is {Top, Right, Bottom, Left}
+export type PaddingValue = UDimValue | { UDimValue }
+
+--- Flex alignments (like in CSS)
+export type FlexAlignment =
+ "start"
+ | "end"
+ | "center"
+ | "space-between"
+ | "space-around"
+ | "space-evenly"
+ | "stretch"
+
+--- Type alias for either Binding or Value
+export type BindingOrValue = React.Binding | T
+
+--- Border settings
+---
+--- NOTE: when building, it uses `UIStroke`, instead of `GuiObject`'s border properties
+export type BorderSettings = {
+ Color: Color?,
+ Thickness: number?,
+ LineJoinMode: Enum.LineJoinMode,
+ Transparency: number?,
+}
+
+--- Aspect ratio settings
+-- TODO: support number-only
+export type AspectSettings = {
+ Ratio: number,
+ Type: Enum.AspectType?,
+ DominantAxis: Enum.DominantAxis?,
+}
+
+--- List layout settings
+export type ListLayoutSettings = {
+ Gap: UDimValue,
+
+ Direction: Enum.FillDirection?,
+ Order: Enum.SortOrder?,
+ Wraps: boolean?,
+
+ JustifyContent: FlexAlignment?,
+ AlignItems: FlexAlignment?,
+ -- TODO: ItemLineAlignment
+}
+
+--- Gui Object modifers
+export type GuiObjectModifiers = {
+ Border: BorderSettings?,
+
+ CornerRadius: UDimValue?,
+
+ Aspect: AspectSettings?,
+
+ Padding: PaddingValue?,
+
+ MinSize: Vector2?,
+ MaxSize: Vector2?,
+
+ ListLayout: ListLayoutSettings?,
+ FlexMode: Enum.UIFlexMode?,
+}
+
+export type GuiObjectProps = {
+ Active: boolean,
+
+ AnchorPoint: AnchorPoint?,
+ AutomaticSize: Enum.AutomaticSize?,
+
+ BackgroundColor: Color?,
+ BackgroundTransparency: number?,
+ --- Alias for `BackgroundTransparency = 1`
+ NoBackground: boolean?,
+
+ ClipsDescendants: boolean?,
+ -- TODO: GuiState:Enum.GuiState,
+ Interactable: boolean,
+
+ LayoutOrder: number?,
+
+ -- TODO: NextSelectionDown:GuiObject
+ -- TODO: NextSelectionLeft:GuiObject
+ -- TODO: NextSelectionRight:GuiObject
+ -- TODO: NextSelectionUp:GuiObject
+
+ Position: UDim2?,
+ Rotation: number?,
+
+ Selectable: boolean?,
+ -- TODO: SelectionImageObject:GuiObject,
+ -- TODO: SelectionOrder:number
+
+ Size: UDim2?,
+ SizeConstraint: Enum.SizeConstraint?,
+
+ Transparency: number,
+ Visible: boolean?,
+
+ ZIndex: number?,
+} & GuiObjectModifiers
+
+export type ScreenGuiProps = {
+ Enabled: boolean?,
+ ResetOnSpawn: boolean?,
+ ZIndexBehavior: Enum.ZIndexBehavior?,
+
+ ScreenInsets: Enum.ScreenInsets?,
+ DisplayOrder: number?,
+}
+
+export type TextProps = {
+ Font: FontValue?,
+ LineHeight: number?,
+
+ MaxVisibleGraphemes: number,
+
+ RichText: boolean?,
+ Text: string?,
+
+ TextColor: Color?,
+ TextDirection: Enum.TextDirection?,
+ TextFits: boolean?,
+ TextSize: TextSize?,
+ TextTransparency: number?,
+ TextTruncate: Enum.TextTruncate?,
+ TextWrapped: boolean?,
+
+ TextXAlignment: Enum.TextXAlignment?,
+ TextYAlignment: Enum.TextYAlignment?,
+
+ MinTextSize: number?,
+ MaxTextSize: number?,
+}
+
+export type TextBoxProps = {
+ ClearTextOnFocus: boolean?,
+ CursorPosition: number?,
+ MultiLine: boolean?,
+ PlaceholderColor: Color?,
+ PlaceholderText: string?,
+ SelectionStart: number?,
+ ShowNativeInput: boolean?,
+ TextEditable: boolean?,
+}
+
+export type TextButtonProps = {
+ AutoButtonColor: boolean?,
+ HoverHapticEffect: HapticEffect?,
+ PressHapticEffect: HapticEffect?,
+ Modal: boolean?,
+ Selected: boolean?,
+ Style: Enum.ButtonStyle?,
+}
+
+export type ScrollingFrameScrollBar = {
+ TopImage: string?,
+ MidImage: string?,
+ BottomImage: string?,
+
+ Color: Color,
+ Transparency: number?,
+
+ Thickness: number?,
+ HorizontalInset: Enum.ScrollBarInset?,
+ VerticalInset: Enum.ScrollBarInset?,
+ VerticalPosition: Enum.VerticalScrollBarPosition?,
+}
+
+export type ScrollingFrameProps = {
+ AutomaticCanvasSize: Enum.AutomaticSize?,
+
+ CanvasPosition: Vector2?,
+ CanvasSize: UDim2?,
+
+ ScrollingDirection: Enum.ScrollingDirection?,
+ ScrollingEnabled: boolean?,
+
+ ScrollBar: ScrollingFrameScrollBar?,
+}
+
+return {}
diff --git a/src/Utils.luau b/src/Utils.luau
new file mode 100644
index 0000000..5c21d2f
--- /dev/null
+++ b/src/Utils.luau
@@ -0,0 +1,336 @@
+local React = require("@pkg/react")
+
+local Typings = require("@root/Typings")
+
+local Utils = {}
+
+local AnchorPointMapping = setmetatable({
+ tl = Vector2.new(0, 0),
+ t = Vector2.new(0.5, 0),
+ tr = Vector2.new(1, 0),
+
+ ml = Vector2.new(0, 0.5),
+ m = Vector2.new(0.5, 0.5),
+ mr = Vector2.new(1, 0.5),
+
+ bl = Vector2.new(0, 1),
+ b = Vector2.new(0.5, 1),
+ br = Vector2.new(1, 1),
+}, {
+ __index = function()
+ error("Invalid anchor point.")
+ end,
+})
+
+local function isBinding(value: any): boolean
+ return typeof(value) == "table" and value["getValue"] ~= nil and value["map"] ~= nil
+end
+
+-- TODO: create separate package "better-react-hooks"
+function Utils.mapBinding(
+ bindingOrValue: Typings.BindingOrValue,
+ map: (value: T) -> R
+): React.Binding
+ local binding = if isBinding(bindingOrValue)
+ then bindingOrValue :: React.Binding
+ else React.createBinding(bindingOrValue :: T)
+
+ return binding:map(map)
+end
+
+function Utils.Assign(table1: T1, table2: T2): T1 & T2
+ local result = table1 :: any
+
+ for k, v in pairs(table2 :: any) do
+ result[k] = v
+ end
+
+ return result :: T1 & T2
+end
+
+-- thanks to https://gist.github.com/qizhihere/cb2a14432d9bf65693ad?permalink_comment_id=3389084#gistcomment-3389084
+function Utils.MergeMulti(...): any
+ local tables_to_merge = { ... }
+ assert(#tables_to_merge > 1, "There should be at least two tables to merge them")
+
+ for k, t in ipairs(tables_to_merge) do
+ assert(typeof(t) == "table", string.format("Expected a table as function parameter %d", k))
+ end
+
+ local result = table.clone(tables_to_merge[1])
+
+ for i = 2, #tables_to_merge do
+ local from = tables_to_merge[i]
+ for k, v in pairs(from) do
+ if type(v) == "table" then
+ result[k] = result[k] or {}
+ assert(type(result[k]) == "table", string.format("Expected a table: '%s'", k))
+ result[k] = Utils.MergeMulti(result[k], v)
+ else
+ result[k] = v
+ end
+ end
+ end
+
+ return result
+end
+
+function Utils.CalculateAspectRatio(instance: GuiObject)
+ local AbsoluteSize = instance.AbsoluteSize
+
+ print("===============================")
+ print(("Calculated Aspect Ratio: %.3f"):format(AbsoluteSize.X / AbsoluteSize.Y))
+ print("===============================")
+end
+
+function Utils.ConvertColor(value: Typings.BindingOrValue): React.Binding
+ return Utils.mapBinding(value, function(value)
+ if typeof(value) == "Color3" then
+ return value
+ elseif typeof(value) == "string" then
+ return Color3.fromHex(value)
+ else
+ error(
+ string.format(
+ 'Invalid value in ConvertColor. Expected Color3 or string, found "%s"',
+ typeof(value)
+ )
+ )
+ end
+ end)
+end
+
+function Utils.ConvertAnchorPoint(
+ value: Typings.BindingOrValue
+): React.Binding
+ return Utils.mapBinding(value, function(value)
+ if typeof(value) == "Vector2" then
+ return value
+ elseif typeof(value) == "string" then
+ return AnchorPointMapping[value]
+ else
+ error(
+ string.format(
+ 'Invalid value in ConvertAnchorPoint. Expected Vector2 or string, found "%s"',
+ typeof(value)
+ )
+ )
+ end
+ end)
+end
+
+function Utils.ConvertFont(value: Typings.BindingOrValue): React.Binding
+ return Utils.mapBinding(value, function(value)
+ if typeof(value) == "EnumItem" then
+ return Font.fromEnum(value :: Enum.Font)
+ elseif typeof(value) == "Font" then
+ return value
+ else
+ error(
+ string.format(
+ 'Invalid value in ConvertFont. Expected Font or Enum.Font, found "%s"',
+ typeof(value)
+ )
+ )
+ end
+ end)
+end
+
+local function ConvertUDimInternal(value: Typings.UDimValue): UDim
+ if typeof(value) == "UDim" then
+ return value
+ elseif typeof(value) == "number" then
+ return UDim.new(0, value)
+ else
+ error(
+ string.format(
+ 'Invalid value in ConvertUDim. Expected UDim or number, found "%s"',
+ typeof(value)
+ )
+ )
+ end
+end
+
+function Utils.ConvertUDim(value: Typings.BindingOrValue): React.Binding
+ return Utils.mapBinding(value, ConvertUDimInternal)
+end
+
+export type ConvertPaddingResult = {
+ PaddingBottom: UDim,
+ PaddingLeft: UDim,
+ PaddingRight: UDim,
+ PaddingTop: UDim,
+}
+
+local function ConvertPaddingInternal(value: { UDim }): ConvertPaddingResult
+ if #value == 0 then
+ return { PaddingTop = 0, PaddingRight = 0, PaddingBottom = 0, PaddingLeft = 0 }
+ elseif #value == 1 then
+ return {
+ PaddingTop = value[1],
+ PaddingRight = value[1],
+ PaddingBottom = value[1],
+ PaddingLeft = value[1],
+ }
+ elseif #value == 2 then
+ return {
+ PaddingTop = value[1],
+ PaddingRight = value[2],
+ PaddingBottom = value[1],
+ PaddingLeft = value[2],
+ }
+ elseif #value == 3 then
+ return {
+ PaddingTop = value[1],
+ PaddingRight = value[2],
+ PaddingBottom = value[3],
+ PaddingLeft = value[2],
+ }
+ elseif #value == 4 then
+ return {
+ PaddingTop = value[1],
+ PaddingRight = value[2],
+ PaddingBottom = value[3],
+ PaddingLeft = value[4],
+ }
+ else
+ error(
+ string.format(
+ "Invalid value in ConvertUDim. Expected up to 4 padding values, found %d.",
+ #value
+ )
+ )
+ end
+end
+
+function Utils.ConvertPadding(
+ value: Typings.BindingOrValue
+): React.Binding
+ return Utils.mapBinding(value, function(value)
+ if typeof(value) == "table" then
+ local result: { UDim } = {}
+ for _, v in value do
+ result[#result + 1] = ConvertUDimInternal(v)
+ end
+
+ return ConvertPaddingInternal(result)
+ elseif typeof(value) == "UDim" or typeof(value) == "number" then
+ return ConvertPaddingInternal({ ConvertUDimInternal(value) })
+ else
+ error(
+ string.format(
+ 'Invalid value in ConvertPadding. Expected UDimValue or {UDimValue}, found "%s"',
+ typeof(value)
+ )
+ )
+ end
+ end)
+end
+
+export type ConvertFlexValueResult = {
+ HorizontalAlignment: Enum.HorizontalAlignment?,
+ HorizontalFlex: Enum.UIFlexAlignment?,
+ VerticalAlignment: Enum.VerticalAlignment?,
+ VerticalFlex: Enum.UIFlexAlignment?,
+}
+
+--- Converts CSS like JustifyContent and AlignItems values to Roblox's ListLayout props
+function Utils.ConvertFlexValue(
+ direction: Enum.FillDirection,
+ value: Typings.FlexAlignment
+): ConvertFlexValueResult
+ if value == "start" then
+ if direction == Enum.FillDirection.Horizontal then
+ return {
+ HorizontalAlignment = Enum.HorizontalAlignment.Left,
+ HorizontalFlex = Enum.UIFlexAlignment.None,
+ }
+ else
+ return {
+ VerticalAlignment = Enum.VerticalAlignment.Top,
+ VerticalFlex = Enum.UIFlexAlignment.None,
+ }
+ end
+ elseif value == "end" then
+ if direction == Enum.FillDirection.Horizontal then
+ return {
+ HorizontalAlignment = Enum.HorizontalAlignment.Right,
+ HorizontalFlex = Enum.UIFlexAlignment.None,
+ }
+ else
+ return {
+ VerticalAlignment = Enum.VerticalAlignment.Bottom,
+ VerticalFlex = Enum.UIFlexAlignment.None,
+ }
+ end
+ elseif value == "center" then
+ if direction == Enum.FillDirection.Horizontal then
+ return {
+ HorizontalAlignment = Enum.HorizontalAlignment.Center,
+ HorizontalFlex = Enum.UIFlexAlignment.None,
+ }
+ else
+ return {
+ VerticalAlignment = Enum.VerticalAlignment.Center,
+ VerticalFlex = Enum.UIFlexAlignment.None,
+ }
+ end
+ elseif value == "space-between" then
+ if direction == Enum.FillDirection.Horizontal then
+ return {
+ HorizontalAlignment = Enum.HorizontalAlignment.Center,
+ HorizontalFlex = Enum.UIFlexAlignment.SpaceBetween,
+ }
+ else
+ return {
+ VerticalAlignment = Enum.VerticalAlignment.Center,
+ VerticalFlex = Enum.UIFlexAlignment.SpaceBetween,
+ }
+ end
+ elseif value == "space-around" then
+ if direction == Enum.FillDirection.Horizontal then
+ return {
+ HorizontalAlignment = Enum.HorizontalAlignment.Center,
+ HorizontalFlex = Enum.UIFlexAlignment.SpaceAround,
+ }
+ else
+ return {
+ VerticalAlignment = Enum.VerticalAlignment.Center,
+ VerticalFlex = Enum.UIFlexAlignment.SpaceAround,
+ }
+ end
+ elseif value == "space-evenly" then
+ if direction == Enum.FillDirection.Horizontal then
+ return {
+ HorizontalAlignment = Enum.HorizontalAlignment.Center,
+ HorizontalFlex = Enum.UIFlexAlignment.SpaceEvenly,
+ }
+ else
+ return {
+ VerticalAlignment = Enum.VerticalAlignment.Center,
+ VerticalFlex = Enum.UIFlexAlignment.SpaceEvenly,
+ }
+ end
+ elseif value == "stretch" then
+ if direction == Enum.FillDirection.Horizontal then
+ return {
+ HorizontalAlignment = Enum.HorizontalAlignment.Center,
+ HorizontalFlex = Enum.UIFlexAlignment.Fill,
+ }
+ else
+ return {
+ VerticalAlignment = Enum.VerticalAlignment.Center,
+ VerticalFlex = Enum.UIFlexAlignment.Fill,
+ }
+ end
+ else
+ error(
+ string.format(
+ 'Invalid value in ConvertFlexValue. Expected "start", "end", "center", "space-between", "space-around", "space-evenly" or "stretch", found "%s"',
+ value
+ )
+ )
+ end
+end
+
+return Utils
diff --git a/src/components/Button.ts b/src/components/Button.ts
deleted file mode 100644
index 0d7ca33..0000000
--- a/src/components/Button.ts
+++ /dev/null
@@ -1,14 +0,0 @@
-import TextComponent, { TextComponentProps } from "../helpers/TextComponent";
-
-type ButtonProps = TextComponentProps & {
- autoButtonColor: boolean
-}
-
-export const ButtonComponent = TextComponent
- .expand(
- (props) => ({
- AutoButtonColor: props.autoButtonColor,
- }),
- );
-
-export const Button = ButtonComponent.build("textbutton");
diff --git a/src/components/CanvasGroup.ts b/src/components/CanvasGroup.ts
deleted file mode 100644
index 56146cc..0000000
--- a/src/components/CanvasGroup.ts
+++ /dev/null
@@ -1,30 +0,0 @@
-import BaseComponent from "../helpers/BaseComponent";
-import { ColorOrHex, resolveColor3 } from "../utils";
-
-export interface CanvasGroupProps {
- /**
- * Color tint that applies to all descendants.
- *
- * `GroupColor3` property in Roblox
- */
- tintColor?: ColorOrHex,
-
- /**
- * Transparency that applies to all descendants.
- *
- * `GroupTransparency` property in Roblox
- */
- opacity?: number
-}
-
-/**
- * @see https://create.roblox.com/docs/reference/engine/classes/CanvasGroup
- */
-export const CanvasGroup = BaseComponent
- .expand(
- (props) => ({
- GroupColor3: resolveColor3(props.tintColor),
- GroupTransparency: props.opacity
- }),
- )
- .build("canvasgroup");
diff --git a/src/components/Frame.ts b/src/components/Frame.ts
deleted file mode 100644
index 9b8f740..0000000
--- a/src/components/Frame.ts
+++ /dev/null
@@ -1,3 +0,0 @@
-import BaseComponent from "../helpers/BaseComponent";
-
-export const Frame = BaseComponent.build("frame");
diff --git a/src/components/GridLayout.tsx b/src/components/GridLayout.tsx
deleted file mode 100644
index ee3eda6..0000000
--- a/src/components/GridLayout.tsx
+++ /dev/null
@@ -1,40 +0,0 @@
-import React from "@rbxts/react";
-
-import type { BindingVariants } from "../utils";
-
-export type GridLayoutProps = BindingVariants<{
- cellPadding?: UDim2
- cellSize?: UDim2
- cellAspectRatio?: number
- cellAspectType?: Enum.AspectType
- cellAspectAxis?: Enum.DominantAxis
- startCorner?: Enum.StartCorner
- direction?: Enum.FillDirection
- horizontalAlign?: Enum.HorizontalAlignment
- verticalAlign?: Enum.VerticalAlignment,
- sortOrder?: Enum.SortOrder;
-}>
-
-export function GridLayout(props: GridLayoutProps) {
- return (
-
- {props.cellAspectRatio !== undefined
- ?
- : undefined}
-
- );
-}
diff --git a/src/components/Image.ts b/src/components/Image.ts
deleted file mode 100644
index 82e20e0..0000000
--- a/src/components/Image.ts
+++ /dev/null
@@ -1,28 +0,0 @@
-import { BaseComponent } from "../helpers";
-
-import { ColorOrHex, resolveColor3 } from "../utils";
-import { mapBinding } from "@rbxts/pretty-react-hooks";
-
-export interface ImageRect {
- offset: Vector2;
- size: Vector2;
-}
-
-export interface ImageProps {
- image?: string,
- imageColor?: ColorOrHex,
- imageTransparency?: number,
- imageRect?: ImageRect
-}
-
-export const ImageComponent = BaseComponent
- .expand((props) => ({
- Image: props.image,
- ImageColor3: resolveColor3(props.imageColor),
- ImageTransparency: props.imageTransparency,
-
- ImageRectOffset: mapBinding(props.imageRect, (rect) => rect?.offset ?? new Vector2()),
- ImageRectSize: mapBinding(props.imageRect, (rect) => rect?.size ?? new Vector2()),
- }));
-
-export const Image = ImageComponent.build("imagelabel");
diff --git a/src/components/ListLayout.tsx b/src/components/ListLayout.tsx
deleted file mode 100644
index 77eeb82..0000000
--- a/src/components/ListLayout.tsx
+++ /dev/null
@@ -1,38 +0,0 @@
-import React from "@rbxts/react";
-
-import { BindingVariants, resolveUDim } from "../utils";
-
-export type ListLayoutProps = BindingVariants<{
- padding?: number | UDim
- direction?: Enum.FillDirection
- order?: Enum.SortOrder
- horizontalAlign?: Enum.HorizontalAlignment
- verticalAlign?: Enum.VerticalAlignment
-
-
- flexWrap?: boolean
- flexAlignX?: Enum.UIFlexAlignment
- flexAlignY?: Enum.UIFlexAlignment
- flexAlignItems?: Enum.ItemLineAlignment
-}>
-
-export function ListLayout(props: ListLayoutProps) {
- return (
-
- );
-}
diff --git a/src/components/ScrollingFrame.ts b/src/components/ScrollingFrame.ts
deleted file mode 100644
index e84d9b0..0000000
--- a/src/components/ScrollingFrame.ts
+++ /dev/null
@@ -1,76 +0,0 @@
-import Object from "@rbxts/object-utils";
-import BaseComponent from "../helpers/BaseComponent";
-
-import { resolveColor3 } from "../utils";
-import { getBindingValue, mapBinding } from "@rbxts/pretty-react-hooks";
-
-import type { InstanceAttributes } from "@rbxts/react";
-
-export type ScrollingFrameProps = {
- automaticCanvasSize?: Enum.AutomaticSize
-
- canvasPosition?: Vector2
- canvasSize?: UDim2
-
- direction?: Enum.ScrollingDirection
-
- /* Horizontal Scrollbar inset */
- scrollbarInsetH?: Enum.ScrollBarInset | boolean
-
- /* Vertical Scrollbar inset */
- scrollbarInsetV?: Enum.ScrollBarInset | boolean
-
- scrollbar?: Scrollbar | false
-}
-
-export type Scrollbar = {
- topImage?: string
- midImage?: string
- bottomImage?: string
-
- imageColor?: Color3 | string
- imageTransparency?: number
-}
-
-export const ScrollingFrameComponent = BaseComponent
- .expand(
- (props) => Object.assign(
- {
- AutomaticCanvasSize: props.automaticCanvasSize,
-
- CanvasPosition: props.canvasPosition,
- CanvasSize: props.canvasSize,
-
- HorizontalScrollBarInset: mapBinding(
- props.scrollbarInsetH,
- (scrollbarInsetH) =>
- typeIs(scrollbarInsetH, "boolean") && scrollbarInsetH
- ? Enum.ScrollBarInset.Always
- : (scrollbarInsetH as Enum.ScrollBarInset) || Enum.ScrollBarInset.None,
- ),
- VerticalScrollBarInset: mapBinding(
- props.scrollbarInsetV,
- (scrollbarInsetV) =>
- typeIs(scrollbarInsetV, "boolean") && scrollbarInsetV
- ? Enum.ScrollBarInset.Always
- : (scrollbarInsetV as Enum.ScrollBarInset) || Enum.ScrollBarInset.None,
- ),
-
- ScrollingDirection: props.direction,
- ScrollingEnabled: mapBinding(props.scrollbar, (scrollbar) => !typeIs(scrollbar, "boolean")),
- } as InstanceAttributes,
-
- // scrollbar settings
- {
- TopImage: mapBinding(props.scrollbar, (scrollbar) => scrollbar && scrollbar.topImage),
- MidImage: mapBinding(props.scrollbar, (scrollbar) => scrollbar && scrollbar.midImage),
- BottomImage: mapBinding(props.scrollbar, (scrollbar) => scrollbar && scrollbar.bottomImage),
-
- ScrollBarImageColor3: mapBinding(props.scrollbar, (scrollbar) => scrollbar && getBindingValue(resolveColor3(scrollbar.imageColor))),
- ScrollBarImageTransparency: mapBinding(props.scrollbar, (scrollbar) => scrollbar && scrollbar.imageTransparency),
-
- } as InstanceAttributes,
- ),
- );
-
-export const ScrollingFrame = ScrollingFrameComponent.build("scrollingframe");
diff --git a/src/components/Text.ts b/src/components/Text.ts
deleted file mode 100644
index 77a345d..0000000
--- a/src/components/Text.ts
+++ /dev/null
@@ -1,5 +0,0 @@
-import TextComponent, { TextComponentProps } from "../helpers/TextComponent";
-
-export const Text = TextComponent
- .expand>()
- .build("textlabel");
diff --git a/src/components/TextBox.ts b/src/components/TextBox.ts
deleted file mode 100644
index d54784c..0000000
--- a/src/components/TextBox.ts
+++ /dev/null
@@ -1,24 +0,0 @@
-import TextComponent, { TextComponentProps } from "../helpers/TextComponent";
-
-import { resolveColor3 } from "../utils";
-
-type TextBoxProps = {
- placeholder?: string
- placeholderColor?: Color3 | string,
-
- textEditable?: boolean,
- clearTextOnFocus?: boolean
-}
-
-export const TextBoxComponent = TextComponent
- .expand & TextBoxProps>(
- (props) => ({
- PlaceholderText: props.placeholder,
- PlaceholderColor3: resolveColor3(props.placeholderColor),
-
- TextEditable: props.textEditable,
- ClearTextOnFocus: props.clearTextOnFocus,
- }),
- );
-
-export const TextBox = TextBoxComponent.build("textbox");
\ No newline at end of file
diff --git a/src/helpers/BaseComponent.tsx b/src/helpers/BaseComponent.tsx
deleted file mode 100644
index 818fb50..0000000
--- a/src/helpers/BaseComponent.tsx
+++ /dev/null
@@ -1,152 +0,0 @@
-import React from "@rbxts/react";
-import ExpandableComponent from "./ExpandableComponent";
-
-import { ColorOrHex, ResolvableAnchorPoint, resolveAnchorPoint, resolveUDim } from "../utils";
-import { getBaseColor, Gradient, GradientElement } from "./Gradient";
-
-import type { InstanceProps } from "@rbxts/react";
-
-export type BaseProps = {
- visible?: boolean,
-
- noBackground?: boolean
- background?: Gradient | ColorOrHex
- backgroundTransparency?: number
- gradientRotation: number
-
- border?: ColorOrHex | Gradient
- borderGradientRotation?: number
- borderMode?: Enum.ApplyStrokeMode
- borderSize?: number
- borderLineJoinMode?: Enum.LineJoinMode
- stroke?: InstanceProps
-
- position?: UDim2
- size?: UDim2,
- anchorPoint?: ResolvableAnchorPoint
- automaticSize?: Enum.AutomaticSize
-
- cornerRadius?: number | UDim
-
- aspectRatio?: number
- aspectType?: Enum.AspectType
- aspectAxis?: Enum.DominantAxis
-
- padding?: number | UDim
- paddingLeft?: number | UDim
- paddingRight?: number | UDim
- paddingTop?: number | UDim
- paddingBottom?: number | UDim
-
- minSize?: Vector2
- maxSize?: Vector2
-
- minTextSize?: number
- maxTextSize?: number
-
- scale?: number,
-
- layoutOrder?: number
- zIndex?: number
- sizeConstraint?: Enum.SizeConstraint
-
- // Flex
- flex?: InstanceProps
- flexMode?: Enum.UIFlexMode
-}
-
-export default new ExpandableComponent()
- .expand(
- // most basic props:
- (userProps) => ({
- Visible: userProps.visible,
-
- BackgroundColor3: getBaseColor(userProps.background),
- BackgroundTransparency: userProps.noBackground ? 1 : (userProps.backgroundTransparency ?? 0),
-
- AutomaticSize: userProps.automaticSize,
-
- Position: userProps.position,
- Size: userProps.size,
- AnchorPoint: resolveAnchorPoint(userProps.anchorPoint ?? new Vector2(0, 0)),
-
- BorderSizePixel: 0, // we use UIStroke instead
-
- LayoutOrder: userProps.layoutOrder,
- ZIndex: userProps.zIndex,
- SizeConstraint: userProps.sizeConstraint,
- }),
-
- // most basic modifiers:
- (userProps) => [
- userProps.cornerRadius !== undefined
- ?
- : undefined,
-
- // Aspect Ratio
- userProps.aspectRatio !== undefined
- ?
- : undefined,
-
- // Stroke (border)
- userProps.stroke !== undefined
- || userProps.border !== undefined || userProps.borderSize !== undefined || userProps.borderLineJoinMode !== undefined
- ? (
-
-
-
- )
- : undefined,
-
- // Padding
- userProps.padding !== undefined
- || userProps.paddingLeft !== undefined || userProps.paddingRight !== undefined
- || userProps.paddingTop !== undefined || userProps.paddingBottom !== undefined
- ?
- : undefined,
-
- // Size constraint
- userProps.minSize !== undefined || userProps.maxSize !== undefined
- ?
- : undefined,
-
- // Text Size constraint
- userProps.minTextSize !== undefined || userProps.maxTextSize !== undefined
- ?
- : undefined,
-
- // Scale
- userProps.scale !== undefined
- ?
- : undefined,
-
- // Flex item
- userProps.flex !== undefined || userProps.flexMode !== undefined
- ?
- : undefined,
-
- ,
- ],
- );
diff --git a/src/helpers/CalculateAspectRatio.ts b/src/helpers/CalculateAspectRatio.ts
deleted file mode 100644
index 59f05d0..0000000
--- a/src/helpers/CalculateAspectRatio.ts
+++ /dev/null
@@ -1,26 +0,0 @@
-/**
- * Pass this function to GuiObject as ref to calculate aspect ratio
- *
- * @example
- * ```tsx
- *
- * ...
- *
- *
- * // In Output (some fraction):
- * // [CALC_ASPECT_RATIO](1): .875
- * ```
- */
-export function calculateAspectRatio(element: unknown) {
- if (typeIs(element, "Instance") && element.IsA("GuiObject")) {
- const sizes = (element as GuiObject).AbsoluteSize;
- const name = (element as GuiObject).Name;
-
- print(`[CALC_ASPECT_RATIO](${name}): ${sizes.X / sizes.Y}`);
- } else {
- error(`[CALC_ASPECT_RATIO]: Passed invalid argument. GuiObject expected, got ${typeOf(element)}`)
- }
-}
diff --git a/src/helpers/ExpandableComponent.ts b/src/helpers/ExpandableComponent.ts
deleted file mode 100644
index 0700bac..0000000
--- a/src/helpers/ExpandableComponent.ts
+++ /dev/null
@@ -1,109 +0,0 @@
-import React, { ForwardedRef, InstanceProps, ReactChild, ReactNode } from "@rbxts/react";
-import Object from "@rbxts/object-utils";
-
-import { BindingVariants as BindingVariantsUtils, flat, ReactProps } from "../utils";
-
-/** @deprecated import from `../utils`, not from here */
-export type BindingVariants = BindingVariantsUtils
-
-type PropBuilder = (userProps: BindingVariantsUtils
, context: C) => InstanceProps
-type ChildrenBuilder
= (userProps: BindingVariantsUtils
, context: C) => ReactNode[]
-type ContextBuilder
= (userProps: BindingVariantsUtils
) => C
-
-/**
- * Expandable component
- *
- * Generics:
- * - I: instance
- * - P: user props
- * - C: context (passed in builders)
- */
-export default class ExpandableComponent {
- private propsBuilders: PropBuilder
[];
- private childrenBuilders: ChildrenBuilder
[];
- private contextBuilders: ContextBuilder
[];
-
- constructor(
- propsBuilders: PropBuilder
[] = [],
- childrenBuilders: ChildrenBuilder
[] = [],
- contextBuilders: ContextBuilder
[] = [],
- ) {
- this.propsBuilders = propsBuilders;
- this.childrenBuilders = childrenBuilders;
- this.contextBuilders = contextBuilders;
- }
-
- expandContext(
- builder: ContextBuilder,
- ): ExpandableComponent {
- return new ExpandableComponent(
- this.propsBuilders,
- this.childrenBuilders,
- [...this.contextBuilders, builder] as ContextBuilder
[],
- );
- }
-
- expand(
- propBuilder?: PropBuilder,
- childrenBuilder?: ChildrenBuilder
,
- ): ExpandableComponent {
- return new ExpandableComponent(
- [...this.propsBuilders, propBuilder] as PropBuilder[],
- [...this.childrenBuilders, childrenBuilder] as ChildrenBuilder
[],
- this.contextBuilders,
- );
- }
-
- build(elementType: string) {
- return React.forwardRef((userProps: BindingVariantsUtils
>, ref) => {
- const context = this.buildContext(userProps);
- const props = this.buildProps(userProps, context, ref);
- const children = this.buildChildren(userProps, context);
-
- return React.createElement(elementType, props, ...children);
- });
- }
-
- private buildContext(userProps: BindingVariantsUtils
>): C {
- return Object.assign(
- {},
- ...this.contextBuilders
- .map((build) => build(userProps)),
- );
- }
-
- private buildProps(userProps: BindingVariantsUtils
>, context: C, ref: ForwardedRef): InstanceProps {
- const builtProps: InstanceProps[] = [];
-
- for (const build of this.propsBuilders) {
- const props = build(userProps, context);
- if (props !== undefined) {
- builtProps.push(props);
- }
- }
-
- return Object.assign(
- Object.assign(
- {},
- ...builtProps,
- ),
- {
- Event: userProps.event,
- Change: userProps.change,
- Tag: userProps.tag,
- ref: ref,
- },
- userProps.overrideRoblox as object,
- );
- }
-
- private buildChildren(userProps: BindingVariantsUtils>, context: C): React.ReactNode[] {
- const children = flat(
- this.childrenBuilders
- .map((build) => build(userProps, context).filterUndefined()),
- );
- if (userProps.children !== undefined) children.push(userProps.children as ReactChild);
-
- return children;
- }
-}
diff --git a/src/helpers/Gradient.tsx b/src/helpers/Gradient.tsx
deleted file mode 100644
index 6dd1d7a..0000000
--- a/src/helpers/Gradient.tsx
+++ /dev/null
@@ -1,64 +0,0 @@
-import React, { Binding } from "@rbxts/react";
-
-import { ColorOrHex, resolveColor3 } from "../utils";
-
-import { BindingOrValue, getBindingValue, mapBinding } from "@rbxts/pretty-react-hooks";
-
-export type Gradient = Array | Array | ColorSequence
-
-export function createColorSequence(value: Gradient): ColorSequence {
- if (typeIs(value, "ColorSequence")) {
- return value as ColorSequence;
- } else if (typeIs(value[0], "ColorSequenceKeypoint")) {
- return new ColorSequence(value as Array);
- } else {
- if (value.size() === 1) value = [value[0], value[0]];
-
- const keypointsCount = value.size();
- const step = 1 / (keypointsCount - 1);
-
- const seq: Array = [];
- let j = 0;
- for (let i = 0; i <= 1; i += step) {
- seq.push(new ColorSequenceKeypoint(
- i,
- getBindingValue(resolveColor3(value[j] as ColorOrHex)) ?? new Color3(),
- ));
- j++;
- }
-
- return new ColorSequence(seq);
- }
-}
-
-export type GradientElementProps = {
- color?: BindingOrValue
- rotation?: BindingOrValue
-}
-
-export function getBaseColor(color: BindingOrValue): Binding | undefined {
- if (typeIs(color, "nil")) return;
-
- return mapBinding(
- color,
- (value) =>
- typeIs(value, "string") || typeIs(value, "Color3") || typeIs(value, "nil")
- ? getBindingValue(resolveColor3(value))! // regular color
- : Color3.fromHex("#FFFFFF"), // gradient
- );
-}
-
-export function GradientElement(props: GradientElementProps) {
- const color = getBindingValue(props.color);
-
- return (
- !typeIs(color, "string") && !typeIs(color, "Color3") && !typeIs(color, "nil")
- // if not single color, make gradient
- ? , createColorSequence)}
- Rotation={props.rotation}
- />
- : <>>
- );
-}
diff --git a/src/helpers/TextComponent.ts b/src/helpers/TextComponent.ts
deleted file mode 100644
index 9dfefb0..0000000
--- a/src/helpers/TextComponent.ts
+++ /dev/null
@@ -1,62 +0,0 @@
-import BaseComponent from "./BaseComponent";
-
-import { ColorOrHex, resolveColor3 } from "../utils";
-import { mapBinding } from "@rbxts/pretty-react-hooks";
-
-import type { InferEnumNames, InstanceEvent } from "@rbxts/react";
-
-export type TextComponentInstance = TextLabel | TextButton | TextBox
-export type TextComponentProps = {
- text?: string,
- textColor?: ColorOrHex
-
- textSize?: number | "AUTO"
-
- font?: Enum.Font | Font
- richText?: boolean
-
- textAlign?: Enum.TextXAlignment
- verticalTextAlign?: Enum.TextYAlignment
-
- /** @deprecated use `textAlign` (WILL BE REMOVED IN NEXT MAJOR VERSION!) */
- align?: Enum.TextXAlignment
- /** @deprecated use `verticalTextAlign` (WILL BE REMOVED IN NEXT MAJOR VERSION!) */
- verticalAlign?: Enum.TextYAlignment
-
- event?: InstanceEvent
-}
-
-export default BaseComponent
- .expand(
- (props) => ({
- Text: props.text,
- TextSize: mapBinding(
- props.textSize,
- (size) =>
- typeIs(size, "number")
- ? size
- : 0,
- ),
- TextScaled: mapBinding(
- props.textSize,
- (size) => size === "AUTO",
- ),
- TextColor3: resolveColor3(props.textColor),
-
- FontFace: mapBinding(
- props.font,
- (font) =>
- typeIs(font, "nil")
- ? Font.fromEnum(Enum.Font.Legacy)
- : typeIs(font, "Font")
- ? font // font
- : typeIs(font, "string")
- ? Font.fromEnum(Enum.Font[font as InferEnumNames]) // font enum key
- : Font.fromEnum(font), // font enum
- ),
- RichText: props.richText,
-
- TextXAlignment: props.textAlign ?? props.align,
- TextYAlignment: props.verticalTextAlign ?? props.verticalAlign,
- }),
- );
diff --git a/src/helpers/index.ts b/src/helpers/index.ts
deleted file mode 100644
index 2ef6369..0000000
--- a/src/helpers/index.ts
+++ /dev/null
@@ -1,10 +0,0 @@
-import { calculateAspectRatio } from "./CalculateAspectRatio";
-
-import ExpandableComponent from "./ExpandableComponent";
-
-import BaseComponent from "./BaseComponent";
-import TextComponent from "./TextComponent";
-
-export { calculateAspectRatio }
-export { ExpandableComponent, BaseComponent, TextComponent };
-export * as Gradient from "./Gradient";
diff --git a/src/index.ts b/src/index.ts
deleted file mode 100644
index 5aa37d8..0000000
--- a/src/index.ts
+++ /dev/null
@@ -1,13 +0,0 @@
-export { Button, ButtonComponent } from "./components/Button";
-export { CanvasGroup } from "./components/CanvasGroup";
-export { Frame } from "./components/Frame";
-export { ScrollingFrame } from "./components/ScrollingFrame";
-export { Image, ImageRect, ImageProps, ImageComponent } from "./components/Image";
-export { Text } from "./components/Text";
-export { TextBox, TextBoxComponent } from "./components/TextBox";
-
-export { GridLayout } from "./components/GridLayout";
-export { ListLayout } from "./components/ListLayout";
-
-export * from "./utils";
-export * as Helpers from "./helpers";
diff --git a/src/init.luau b/src/init.luau
new file mode 100644
index 0000000..107387e
--- /dev/null
+++ b/src/init.luau
@@ -0,0 +1,37 @@
+local Typings = require("@root/Typings")
+
+-- reexports of typings:
+export type AnchorPoint = Typings.AnchorPoint
+export type Color = Typings.Color
+export type TextSize = Typings.TextSize
+export type FontValue = Typings.FontValue
+export type UDimValue = Typings.UDimValue
+export type PaddingValue = Typings.PaddingValue
+export type FlexAlignment = Typings.FlexAlignment
+export type BindingOrValue = Typings.BindingOrValue
+
+export type BorderSettings = Typings.BorderSettings
+export type AspectSettings = Typings.AspectSettings
+export type ListLayoutSettings = Typings.ListLayoutSettings
+export type GuiObjectModifiers = Typings.GuiObjectModifiers
+
+export type GuiObjectProps = Typings.GuiObjectProps
+export type ScreenGuiProps = Typings.ScreenGuiProps
+export type TextProps = Typings.TextProps
+export type TextBoxProps = Typings.TextBoxProps
+export type TextButtonProps = Typings.TextButtonProps
+export type ScrollingFrameScrollBar = Typings.ScrollingFrameScrollBar
+export type ScrollingFrameProps = Typings.ScrollingFrameProps
+
+local BetterReactComponents = {
+ Utils = require("@root/Utils"),
+ Builder = require("@root/Builder"),
+}
+
+local Components = require("@root/Components")
+local Builders = require("@root/Builders")
+
+return BetterReactComponents.Utils.Assign(
+ BetterReactComponents.Utils.Assign(BetterReactComponents, Components),
+ { Builders = require("@root/Builders") }
+)
diff --git a/src/stories/App.ts b/src/stories/App.ts
deleted file mode 100644
index 5cdd42c..0000000
--- a/src/stories/App.ts
+++ /dev/null
@@ -1,14 +0,0 @@
-import React from "@rbxts/react";
-
-import { Frame } from ".."; // @rbxts/better-react-components
-
-declare const _G: Record;
-_G.__DEV__ = true;
-
-export function App(props: { children: React.ReactNode }) {
- return React.createElement(
- Frame,
- { size: UDim2.fromScale(1, 1), noBackground: true },
- props.children,
- );
-}
diff --git a/src/stories/basics/1_helloworld.story.tsx b/src/stories/basics/1_helloworld.story.tsx
deleted file mode 100644
index 0da1176..0000000
--- a/src/stories/basics/1_helloworld.story.tsx
+++ /dev/null
@@ -1,26 +0,0 @@
-import React from "@rbxts/react";
-
-import { Text } from "../.."; // @rbxts/better-react-components
-import { App } from "../App";
-
-import { hoarcekat } from "@rbxts/pretty-react-hooks";
-import { CatppuccinLatte } from "../utils";
-
-export = hoarcekat(() => {
- return (
-
-
-
- );
-});
diff --git a/src/stories/basics/2_padding.story.tsx b/src/stories/basics/2_padding.story.tsx
deleted file mode 100644
index 7e209ae..0000000
--- a/src/stories/basics/2_padding.story.tsx
+++ /dev/null
@@ -1,47 +0,0 @@
-import React from "@rbxts/react";
-
-import { AnchorPoints, Frame, Text } from "../.."; // @rbxts/better-react-components
-
-import { hoarcekat } from "@rbxts/pretty-react-hooks";
-import { App } from "../App";
-
-export = hoarcekat(() => {
- return (
-
-
-
-
-
-
-
- );
-});
diff --git a/src/stories/basics/3_cornerRadius.story.tsx b/src/stories/basics/3_cornerRadius.story.tsx
deleted file mode 100644
index 25ff5f1..0000000
--- a/src/stories/basics/3_cornerRadius.story.tsx
+++ /dev/null
@@ -1,20 +0,0 @@
-import React from "@rbxts/react";
-
-import { AnchorPoints, Frame } from "../.."; // @rbxts/better-react-components
-import { hoarcekat } from "@rbxts/pretty-react-hooks";
-import { App } from "../App";
-
-export = hoarcekat(() => {
- return (
-
-
-
- );
-});
diff --git a/src/stories/basics/5_flexItem.story.tsx b/src/stories/basics/5_flexItem.story.tsx
deleted file mode 100644
index b371551..0000000
--- a/src/stories/basics/5_flexItem.story.tsx
+++ /dev/null
@@ -1,41 +0,0 @@
-import React, { useBinding } from "@rbxts/react";
-
-import { Frame, Helpers, ListLayout } from "../../index"; // @rbxts/better-react-components
-import { hoarcekat } from "@rbxts/pretty-react-hooks";
-import { App } from "../App";
-
-// const { calculateAspectRatio } = Helpers;
-
-export = hoarcekat(() => {
- return (
-
-
-
-
-
-
-
-
- );
-})
diff --git a/src/stories/examples/sidebar.story.tsx b/src/stories/examples/sidebar.story.tsx
deleted file mode 100644
index d749c09..0000000
--- a/src/stories/examples/sidebar.story.tsx
+++ /dev/null
@@ -1,67 +0,0 @@
-import React from "@rbxts/react";
-
-import { AnchorPoints, Button, Frame, ListLayout } from "../.."; // @rbxts/better-react-components
-import { App } from "../App";
-
-import { hoarcekat } from "@rbxts/pretty-react-hooks";
-import { CatppuccinLatte } from "../utils";
-
-function CoolButton(props: { text: string, onClick: () => void }) {
- return (
-
- );
-}
-
-export = hoarcekat(() => {
- return (
-
-
-
-
- print("Clicked on button #1")}
- />
-
- print("Clicked on button #2")}
- />
-
- print("Clicked on button #3")}
- />
-
-
- );
-});
diff --git a/src/stories/examples/textBox.story.tsx b/src/stories/examples/textBox.story.tsx
deleted file mode 100644
index d1ba617..0000000
--- a/src/stories/examples/textBox.story.tsx
+++ /dev/null
@@ -1,36 +0,0 @@
-import React from "@rbxts/react";
-
-import { hoarcekat } from "@rbxts/pretty-react-hooks";
-
-import { App } from "../App";
-import { AnchorPoints, TextBox } from "../.."; // @rbxts/better-react-components
-import { CatppuccinLatte } from "../utils";
-
-
-export = hoarcekat(() => {
- return (
-
-
-
- );
-});
diff --git a/src/stories/tests/bindings.story.tsx b/src/stories/tests/bindings.story.tsx
deleted file mode 100644
index ec5ce2f..0000000
--- a/src/stories/tests/bindings.story.tsx
+++ /dev/null
@@ -1,62 +0,0 @@
-import React, { useBinding } from "@rbxts/react";
-
-import { Button, Frame, GridLayout, Text } from "../.."; // @rbxts/better-react-components
-
-import { hoarcekat } from "@rbxts/pretty-react-hooks";
-import { CatppuccinLatte } from "../utils";
-
-export = hoarcekat(() => {
- const [count, setCount] = useBinding(0);
-
- return (
-
-
-
-