Add bunext playground for local component development

Runs at http://localhost:7010 via bun run dev. The playground imports the library from src through a committed symlink and tsconfig path alias, so component edits hot-reload without a build step. It has its own install root and is excluded from the published package.
This commit is contained in:
2026-10-19 04:23:07 +01:00
parent 46049c5e15
commit 73a25f54c2
18 changed files with 1348 additions and 5 deletions
+3
View File
@@ -33,3 +33,6 @@ report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json
.DS_Store .DS_Store
# build output # build output
dist dist
# dev playground artifacts (bunext)
.bunext
+4
View File
@@ -15,6 +15,9 @@
| `bun run typecheck` | `tsc --noEmit` - must be clean before finishing any change | | `bun run typecheck` | `tsc --noEmit` - must be clean before finishing any change |
| `bun test src` | Unit tests (`src/**/*.test.ts`) | | `bun test src` | Unit tests (`src/**/*.test.ts`) |
| `bun run compile` | Clean build into `dist/` + post-compile step | | `bun run compile` | Clean build into `dist/` + post-compile step |
| `bun run dev` | Start the playground in `dev/` at http://localhost:7010 |
| `bun run dev:types` | `tsc --watch`, for consumers working against `dist/` |
| `bun run dev:typecheck` | Typecheck the playground (`dev/tsconfig.json`) |
| `./publish.sh "msg"`| Typecheck → test → compile → `git commit/push` → `bun publish` | | `./publish.sh "msg"`| Typecheck → test → compile → `git commit/push` → `bun publish` |
`publish.sh` pushes and publishes - only run it when explicitly asked. Bump `version` in `package.json` before publishing. `publish.sh` pushes and publishes - only run it when explicitly asked. Bump `version` in `package.json` before publishing.
@@ -37,6 +40,7 @@
| `src/(partials)/`, `src/(functions)/`, `src/(utils)/` | Internal helpers, not meant for direct import | | `src/(partials)/`, `src/(functions)/`, `src/(utils)/` | Internal helpers, not meant for direct import |
| `src/types.ts` | Shared types/constants | | `src/types.ts` | Shared types/constants |
| `scripts/post-compile.ts` | Copies `base.css`, adds `.js` to relative imports, prepends `"use client"` | | `scripts/post-compile.ts` | Copies `base.css`, adds `.js` to relative imports, prepends `"use client"` |
| `dev/` | Local bunext playground, own `node_modules`/lockfile; never published. `dev/twui -> ../src` drives HMR for library edits |
## Public API Rules (MANDATORY) ## Public API Rules (MANDATORY)
+20
View File
@@ -78,6 +78,26 @@ bun test src
bun run compile # tsc → dist/, then scripts/post-compile.ts bun run compile # tsc → dist/, then scripts/post-compile.ts
``` ```
### Playground
`dev/` is a local [bunext](https://git.tben.me/Moduletrace/bunext) app that imports the library straight from `src/` (through the committed `dev/twui` symlink and a tsconfig path alias), so component edits hot-reload without running `tsc`. It has its own `node_modules` and is excluded from the published package by the `files` whitelist.
```sh
cd dev
bun install
cd ..
bun run dev # http://localhost:7010
```
Install the library dependencies at the repo root first: the symlinked sources resolve `react`, `tailwind-merge`, `lucide-react` and the optional peers from the root `node_modules`.
Other useful scripts:
```sh
bun run dev:types # tsc --watch, for consumers working against dist/
bun run dev:typecheck # typecheck the playground pages and the sources they import
```
### Publishing ### Publishing
```sh ```sh
+1
View File
@@ -0,0 +1 @@
@moduletrace:registry=https://git.tben.me/api/packages/moduletrace/npm/
+277
View File
@@ -0,0 +1,277 @@
{
"lockfileVersion": 2,
"configVersion": 1,
"workspaces": {
"": {
"name": "@moduletrace/twui-dev",
"dependencies": {
"@moduletrace/bunext": "^1.3.4",
"lucide-react": "^1.59.0",
},
"devDependencies": {
"typescript": "^5.9.3",
},
},
},
"packages": {
"@alloc/quick-lru": ["@alloc/[email protected]", "", {}, "sha512-U4+70Pc5ZS9osnCBCE5Jha/ciHM+Yp+CNMNC/7HvYbNRk1Ldd+f7qO65W5qfhu/TCv+/ozljlXXe9Nj8419DMA=="],
"@esbuild/aix-ppc64": ["@esbuild/[email protected]", "", { "os": "aix", "cpu": "ppc64" }, "sha512-XExcO+dvLKvVtNTibSTBej1NCAbaGhWn9Ww1ZPx80qsahhPFe/8jgWP0IchNe0F3HwkU7n8ejhH8bjonqht8mQ=="],
"@esbuild/android-arm": ["@esbuild/[email protected]", "", { "os": "android", "cpu": "arm" }, "sha512-kXXoiPVVGQcnIYGOeaovwOURpniDBpSq4A03qkQ+BMQqtGG6HYap3xne9C1O1yo4TR3qxlCX5IqqmX6fFo2Lqg=="],
"@esbuild/android-arm64": ["@esbuild/[email protected]", "", { "os": "android", "cpu": "arm64" }, "sha512-5YfKeeI8qWfBZIX+u2xZC3Zlb3Os/gLS2sbEKM+I4ZOcsWmHS2WLysCcQZDAFRslDUU5Oiq44gf6PYN1vGwG5A=="],
"@esbuild/android-x64": ["@esbuild/[email protected]", "", { "os": "android", "cpu": "x64" }, "sha512-O387ite7SzUyCcy3JQX4P4bLtEA7bLLkx+esve5JHnyYfNTxcVpXZo9jhdB0lTKN44gztELTdU7nS8Nr16Fs1Q=="],
"@esbuild/darwin-arm64": ["@esbuild/[email protected]", "", { "os": "darwin", "cpu": "arm64" }, "sha512-n4KqkOQrraxHJcgjM1RvwbigfQKIKJVpM7xp+KsxiyUSrRdIXnt73VhrPAx0fV44hgfmIVKjxMN9J1t5jySVkw=="],
"@esbuild/darwin-x64": ["@esbuild/[email protected]", "", { "os": "darwin", "cpu": "x64" }, "sha512-uq6suIWYP37qzGddBKPw5QEQPi6HiLGsO7UmkpfyaYNQ3D+rN6w6WfwH+nuqcGXWvawGwxOEroO4YGnFh95azw=="],
"@esbuild/freebsd-arm64": ["@esbuild/[email protected]", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-n+I0BTSRIoy+d6RPKnEVwql5UwBJolytvY4mAOIEJorKlqgPII8ix6slVVrfZ5Tnj7glIZvloylbB/EJPMWEXw=="],
"@esbuild/freebsd-x64": ["@esbuild/[email protected]", "", { "os": "freebsd", "cpu": "x64" }, "sha512-78XJTJkvPs0kz2w61301PJjXl4g7q3JqiYMZ/M/yVI73EHBrCRTgkhu9oqG7vPqq+a/yadEW8aD+agKlk5xrmg=="],
"@esbuild/linux-arm": ["@esbuild/[email protected]", "", { "os": "linux", "cpu": "arm" }, "sha512-XlDnu2q5yoqems+xay6wSAcg9DDD7K9RLKZEBOMZm3ckNpJBvOX20tSfby8KfrrhINDyv9V2YVZKY/SpoGJI8w=="],
"@esbuild/linux-arm64": ["@esbuild/[email protected]", "", { "os": "linux", "cpu": "arm64" }, "sha512-pW4AC0P3it8c7do9MVM4p51FzHzdM/TZrerurgRcHJ2WTa1VQ1CIq18xncfpBJw4ojkiZZrKW2yIBWBP92j6Ug=="],
"@esbuild/linux-ia32": ["@esbuild/[email protected]", "", { "os": "linux", "cpu": "ia32" }, "sha512-CYbnj78HsIeA+DhgUKgFCfvNsTHFhMMrinUrMZpDXJXKN8T3XViTZ/+wtHeVxEWY8ewSzTFN+nRmSwO2tZaLUQ=="],
"@esbuild/linux-loong64": ["@esbuild/[email protected]", "", { "os": "linux", "cpu": "none" }, "sha512-buwkd8nsph4R+ajRvw0qM5Hja/TXQow3ptzWO2EbG/cqcIkHloRrdlBtQlshyYGTNFvfkfJ5tpPLVkY4DtsPfQ=="],
"@esbuild/linux-mips64el": ["@esbuild/[email protected]", "", { "os": "linux", "cpu": "none" }, "sha512-ZVykbDyk7519VwiNb9Lcj9m8XM6v5V9uKPvrEMkkEedVewf+0itkhahp4HDpgERXhwLRpWFypsGbG/J8s0QjJA=="],
"@esbuild/linux-ppc64": ["@esbuild/[email protected]", "", { "os": "linux", "cpu": "ppc64" }, "sha512-CAXl+Dtd9UUuJd8pKKdwh6MLm3MUMiqMPmhZ3tTSXPqfyQ3vDl6R5hZdZ/kYojK4ofXtdfSv1tFq8XzWx3heNQ=="],
"@esbuild/linux-riscv64": ["@esbuild/[email protected]", "", { "os": "linux", "cpu": "none" }, "sha512-GeXCej4IQtU1B+QlDV8W/RRvbzI3O/Stss+/bCXv4lZls5WGRtu2a+3JkA3i4qIUlMXpcHebWpF8AkJhATowuA=="],
"@esbuild/linux-s390x": ["@esbuild/[email protected]", "", { "os": "linux", "cpu": "s390x" }, "sha512-3H1weTYZPxt/WOhByszQZybS9w5lKzUn1FDMsgEChbHWQwHYQQRfBxgCcZvPhjHfKyJjIievvMmEUawJrdY9Dg=="],
"@esbuild/linux-x64": ["@esbuild/[email protected]", "", { "os": "linux", "cpu": "x64" }, "sha512-4xTZr1FUmSoQW4XIWmit3tzQrUTZM+N3P0XV8xROKYF50XfI7xeO90+1bZvNwxIufQ9hDQVRJH5YhgPVF8A/HQ=="],
"@esbuild/netbsd-arm64": ["@esbuild/[email protected]", "", { "os": "none", "cpu": "arm64" }, "sha512-sSATRjPeDBg3pdgHoQfoYBob11Kk1FGa9lui5RIHZCoCkJa9QKlvl3/vKz2usCmYYjs7ymJR/2Nnsqe+Hjt5nw=="],
"@esbuild/netbsd-x64": ["@esbuild/[email protected]", "", { "os": "none", "cpu": "x64" }, "sha512-lqnzCV+mM0gIADaKihiCg6ifgfU2L3h5E33rNQBN1Y4MaVGnzryzmvvf7UHxprpQdE8hpqLolJ9Rl+SkIRDpyw=="],
"@esbuild/openbsd-arm64": ["@esbuild/[email protected]", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-AL2qJILH7lNjrDmCQDvdxMfAUIv8KMNZOvrwAQ8i8//ntL9FflhOyMJ8OZSMBb8/AWXe3/5v5S20y3zCoZWKoQ=="],
"@esbuild/openbsd-x64": ["@esbuild/[email protected]", "", { "os": "openbsd", "cpu": "x64" }, "sha512-QtiuPytchRyC4rwUKhexJdQKvDuZ6hWloi3igqPQNUJCS1/v9EiO3UTOXR6A3FoMo4fnAKbWJdqaIwhOzh8qEw=="],
"@esbuild/openharmony-arm64": ["@esbuild/[email protected]", "", { "os": "none", "cpu": "arm64" }, "sha512-WkhYDmpTjLvGlScA1rwjRUmhl4k8oXR3cIbtqWmELgU/dFeHHlEllxDvdWcNJV9rbzCexB5vz8gtNewWLgCT7Q=="],
"@esbuild/sunos-x64": ["@esbuild/[email protected]", "", { "os": "sunos", "cpu": "x64" }, "sha512-GPMSkTOtMnv2U2F8gxe4Io6qmVs+YKyp832Etqqxr0hFngmXQ3rzwytelm3GIn7T4VviRUlf3sOgBOiTdvaf7g=="],
"@esbuild/win32-arm64": ["@esbuild/[email protected]", "", { "os": "win32", "cpu": "arm64" }, "sha512-PIhhEkE9uPBleRBrQEJpUn7MBnibZzbGzYWPmY3x+YoVg/95zbjB4CxPPOQ8l5tYYM4mMaCthF8/1DIfBQQyWQ=="],
"@esbuild/win32-ia32": ["@esbuild/[email protected]", "", { "os": "win32", "cpu": "ia32" }, "sha512-YmJbfTlvU7Sdn9BB+4PRES4oB6pxgS37MAONj+hBr/cpXS1aBPKXxNnDbu+QCWPj0o9dgyxeq79g6c5P8KeuYA=="],
"@esbuild/win32-x64": ["@esbuild/[email protected]", "", { "os": "win32", "cpu": "x64" }, "sha512-5ebpxr3nWMzrL/rnUI755Jkuee0bHL/Gq0WTF9lvcpv73wAp5eu8MfBUgWK9bhWvZjj7yX8etf/8tI8Ney695g=="],
"@jridgewell/gen-mapping": ["@jridgewell/[email protected]", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA=="],
"@jridgewell/remapping": ["@jridgewell/[email protected]", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ=="],
"@jridgewell/resolve-uri": ["@jridgewell/[email protected]", "", {}, "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw=="],
"@jridgewell/sourcemap-codec": ["@jridgewell/[email protected]", "", {}, "sha512-T7jf+5zgsZHwNJ4lvQ7/aezbyk0nNX+zJVWpmHA7VYsEx7a7qr5Rg5IbtJFqkgze5Y2sruq1RUY8Q837Od7iFw=="],
"@jridgewell/trace-mapping": ["@jridgewell/[email protected]", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="],
"@moduletrace/bunext": ["@moduletrace/[email protected]", "https://git.tben.me/api/packages/Moduletrace/npm/%40moduletrace%2Fbunext/-/1.3.4/bunext-1.3.4.tgz", { "dependencies": { "@tailwindcss/postcss": "^4.3.3", "@types/bun": "1.3.14", "@types/node": "^24.10.0", "@types/react": "^19.3.0", "@types/react-dom": "^19.3.0", "bun-plugin-tailwind": "^0.1.2", "chalk": "^5.6.2", "chokidar": "^5.0.0", "commander": "^14.0.2", "esbuild": "^0.28.2", "lightningcss-wasm": "^1.32.0", "lodash": "^4.18.1", "ora": "^9.0.0", "postcss": "^8.5.30", "react": "^19.3.0", "react-dom": "^19.3.0", "tailwindcss": "^4.3.3", "typescript": "^5.0.0", "uuid": "^14.0.2" }, "bin": { "bunext": "dist/commands/index.js" } }, "sha512-+WERTs024+7c2RiynwxngPhShPOexnA2qriVe38JTDAEzl2bqHq97WBecJhD6z7R+VTMibfx+ODikHBXcWEj8Q=="],
"@oven/bun-darwin-aarch64": ["@oven/[email protected]", "", { "os": "darwin", "cpu": "arm64" }, "sha512-u2wAM9RzqyF4baO5oFVhcFUVWO1CCMLG6yC5/aPWCYGZPfL5ULMbeHt+0/3I0h3yaPx1m+PKSANO/61jPtDZcQ=="],
"@oven/bun-darwin-x64": ["@oven/[email protected]", "", { "os": "darwin", "cpu": "x64" }, "sha512-wRGeciWOyKyG/BfTHq4CAyqoOfuzj5UJkKA8FlJETW2yhtDh+Ba3EgPnsVclFyzIw7E0HJxpsonoj17g3HPK/Q=="],
"@oven/bun-freebsd-aarch64": ["@oven/[email protected]", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-z4LiDRPTJMKA8JvtP+Z+I5m1syJr8HGYFainIJoCAdkWaHMyQMaEEYr0N+FzTK4pECUHUmscCTNfC9XJwTe4dQ=="],
"@oven/bun-freebsd-x64": ["@oven/[email protected]", "", { "os": "freebsd", "cpu": "x64" }, "sha512-YpwGnfvtSMRMnnViJI5wCGmS5HpCBAl0wLhBflJfmZYkgOHfplzeKUkUVDW2bNOZyrym3YN+EDaLWooaJpX8Eg=="],
"@oven/bun-linux-aarch64": ["@oven/[email protected]", "", { "os": "linux", "cpu": "arm64" }, "sha512-ylogDFxDD+aRKO0TJauwDzCWM+rULvc85gRNZQ9CnY6rm2BU4GC2XUDi20bh67CIuB4Wt/0PL1xpsJ2hqplMww=="],
"@oven/bun-linux-aarch64-android": ["@oven/[email protected]", "", { "os": "android", "cpu": "arm64" }, "sha512-KCsYD+HHawd6xht4BOffTnLZXNf+xzq8l52MsIF2yRtIfUvGTCumdnJPXxuo969iXukb7cWtz+WMkpYlKMG+sg=="],
"@oven/bun-linux-aarch64-musl": ["@oven/[email protected]", "", { "os": "linux", "cpu": "arm64" }, "sha512-5OJIbcVspZxIN/zQtrFYwLuJgaOlciGcynmwCtwKyg0rmXHJoNG0QyspiiLnDUDoVTSGY/nrn0QEbWKhu0QbEQ=="],
"@oven/bun-linux-x64": ["@oven/[email protected]", "", { "os": "linux", "cpu": "x64" }, "sha512-RArgnpjeXjaGzjp5h7Rs89cUYJutO8IyqR3itE8OBaBzEt8KNm2jp/YozxBHq4eFovwrQLryeSwTAcmiT+zcoA=="],
"@oven/bun-linux-x64-android": ["@oven/[email protected]", "", { "os": "android", "cpu": "x64" }, "sha512-5CdcvAc+iTqf/p6S0VkoGF8harKMjTfEkMOXJHslwBzl5GcY5alth1mkhD3T7SqLA5aUal95vzfiNjoAZXkPOw=="],
"@oven/bun-linux-x64-musl": ["@oven/[email protected]", "", { "os": "linux", "cpu": "x64" }, "sha512-L0gTTjTncLqv7VsV/2iLz7Zk4vd969xqRgbtGnskmV3MvL+IJgzTyQr5zm8GGGWSm/SO/ab2vnn4GXgPomoGbg=="],
"@oven/bun-windows-aarch64": ["@oven/[email protected]", "", { "os": "win32", "cpu": "arm64" }, "sha512-a3Vyu9KI0J9vfIS+/LJMmjWI/qsBpph8u0R7d6Tm+YaSOqZeN2LRULfVqz13dgQRTvqg6aWBxOv9jE4Rl0lAAQ=="],
"@oven/bun-windows-x64": ["@oven/[email protected]", "", { "os": "win32", "cpu": "x64" }, "sha512-9pV16qXHTFvZDH61S8hpdpVBbWmvEqekczLF54zTnQHKP28D6yRCNLrD/7jVbXYR61fQYAx4FSBgzCxGCKb3rQ=="],
"@tailwindcss/node": ["@tailwindcss/[email protected]", "", { "dependencies": { "@jridgewell/remapping": "^2.3.5", "enhanced-resolve": "^5.24.1", "jiti": "^2.7.0", "lightningcss": "1.32.0", "magic-string": "^0.30.21", "source-map-js": "^1.2.1", "tailwindcss": "4.3.3" } }, "sha512-/T8IKEsf9VTU6tLjgC7+sv2mOPtQxzE2jMw7u4Tt40Tx+QSZxpzh95/H6cMKoja9XuW7iMdLJYBB0o9G1CaAgg=="],
"@tailwindcss/oxide": ["@tailwindcss/[email protected]", "", { "optionalDependencies": { "@tailwindcss/oxide-android-arm64": "4.3.3", "@tailwindcss/oxide-darwin-arm64": "4.3.3", "@tailwindcss/oxide-darwin-x64": "4.3.3", "@tailwindcss/oxide-freebsd-x64": "4.3.3", "@tailwindcss/oxide-linux-arm-gnueabihf": "4.3.3", "@tailwindcss/oxide-linux-arm64-gnu": "4.3.3", "@tailwindcss/oxide-linux-arm64-musl": "4.3.3", "@tailwindcss/oxide-linux-x64-gnu": "4.3.3", "@tailwindcss/oxide-linux-x64-musl": "4.3.3", "@tailwindcss/oxide-wasm32-wasi": "4.3.3", "@tailwindcss/oxide-win32-arm64-msvc": "4.3.3", "@tailwindcss/oxide-win32-x64-msvc": "4.3.3" } }, "sha512-krXjAikiaFSPaK/FkAQT5UTx3VormQaiZ5hBFlJZ9UFQGB/rwg1MZIhHAG9smMQRTdyJxP6Qt5MwMtdyU5FWrA=="],
"@tailwindcss/oxide-android-arm64": ["@tailwindcss/[email protected]", "", { "os": "android", "cpu": "arm64" }, "sha512-Y85A2gmPSkl5Ve5qR86GL4HT509cFqQh1aes9p3sSkyTPwt0Pppf3GkwGe4JPACcRYjgJIEhQgM6dBClnr0NYw=="],
"@tailwindcss/oxide-darwin-arm64": ["@tailwindcss/[email protected]", "", { "os": "darwin", "cpu": "arm64" }, "sha512-BiaWatpBcERQFDlOjRDpIVXuFK5PJez5SA4JMg6VYZdBYU+qKfV/vqjcIs+IYmtitf1xYQZTwXvU/8y4lfZUGw=="],
"@tailwindcss/oxide-darwin-x64": ["@tailwindcss/[email protected]", "", { "os": "darwin", "cpu": "x64" }, "sha512-fAeUqfV5ndhxRwai8cXGzdLvul9utWOmeTkv69unv4ZXixjn61Z+p9lCWdwOwA3TYboG3BwdVuN/RDjhBRl0mw=="],
"@tailwindcss/oxide-freebsd-x64": ["@tailwindcss/[email protected]", "", { "os": "freebsd", "cpu": "x64" }, "sha512-iyf5bV6+wnAlflVeEy7R25dupxTNECZN5QMI0qNT6eT+EgaGdZcKhGkr5SdoaWiLJ3spLqIY9VCeSGrwmtg4kw=="],
"@tailwindcss/oxide-linux-arm-gnueabihf": ["@tailwindcss/[email protected]", "", { "os": "linux", "cpu": "arm" }, "sha512-aAYUprJAJQWWbRrPvtjdroZ56Md+JM8pMiopS6xGEwDfLhqj+2ver2p4nU4Mb3CRqcMmNBjo8KkUgcxhkzVQGQ=="],
"@tailwindcss/oxide-linux-arm64-gnu": ["@tailwindcss/[email protected]", "", { "os": "linux", "cpu": "arm64" }, "sha512-nDxldcEENOxZRzC2uu9jrutZdAAQtb+8WWDCSnWL1zvBk1+FN+x6MtDViPB5AJMfttVCUhehGWus3XBPgatM/w=="],
"@tailwindcss/oxide-linux-arm64-musl": ["@tailwindcss/[email protected]", "", { "os": "linux", "cpu": "arm64" }, "sha512-Md44bD6veX/PC5iyF8cDVnw4HBIANZepRZZ7a8DQOvkfo5WUBwcp6iAuCUz23u+4SUkhJlD3eL7hNdW8ezd/kA=="],
"@tailwindcss/oxide-linux-x64-gnu": ["@tailwindcss/[email protected]", "", { "os": "linux", "cpu": "x64" }, "sha512-tx7us1muwOKAKWao2v/GaafFeQboE6aj88vC6ziN2NCGcRm8gWUhwjzg+YdVB1e4boAtdtma4L43onunI6NS4w=="],
"@tailwindcss/oxide-linux-x64-musl": ["@tailwindcss/[email protected]", "", { "os": "linux", "cpu": "x64" }, "sha512-SJxX60smvHgasZoBy11dX6YRjXJFovwWBoedhbQPOBzgFWBHGB+TVPWB9BxzR7TTxU8FQZAI2AyiNCMzFm8Img=="],
"@tailwindcss/oxide-wasm32-wasi": ["@tailwindcss/[email protected]", "", { "dependencies": { "@emnapi/core": "^1.11.1", "@emnapi/runtime": "^1.11.1", "@emnapi/wasi-threads": "^1.2.2", "@napi-rs/wasm-runtime": "^1.1.4", "@tybys/wasm-util": "^0.10.2", "tslib": "^2.8.1" }, "cpu": "none" }, "sha512-jx1+rPhY/5Ympkktd656HBWEBLxP7dH06losBLjjf5vgCODXvi9KhtftWcMIwTFIDqBr7cRnQkdLnAG+IOlGvQ=="],
"@tailwindcss/oxide-win32-arm64-msvc": ["@tailwindcss/[email protected]", "", { "os": "win32", "cpu": "arm64" }, "sha512-3rc292Ca2ceK6Ulcc/bAVnTs/3nDtoPhyEKlgPv+yQJQi/JS/AMJlqzxvlDacL1nekbrcf6bTqp/jV4qgnPxNQ=="],
"@tailwindcss/oxide-win32-x64-msvc": ["@tailwindcss/[email protected]", "", { "os": "win32", "cpu": "x64" }, "sha512-yJ0pwIVc/nYeGoV02WtsN8KYyLQv7kyI2wDnkezyJlGGjkd4QLwDGAwl47YpPJeuI0M0ObaXGSPjvWDPeTPggw=="],
"@tailwindcss/postcss": ["@tailwindcss/[email protected]", "", { "dependencies": { "@alloc/quick-lru": "^5.2.0", "@tailwindcss/node": "4.3.3", "@tailwindcss/oxide": "4.3.3", "postcss": "^8.5.16", "tailwindcss": "4.3.3" } }, "sha512-JTSZZGQi1AyKirbLN3azmjVzef92tcX7h+iSqPdaeStyFpGpDlKvvpxeOE8njhbUanbRwr3z8DyzhICWnMtQeg=="],
"@types/bun": ["@types/[email protected]", "", { "dependencies": { "bun-types": "1.3.14" } }, "sha512-h1hFqFVcvAvD9j9K7ZW7vd82aSA+rTdznZa+5bwvCwqSB1jmmfLcbIWhOLx1/+boy/xmjgCs/OMUL8hRJSmnPw=="],
"@types/node": ["@types/[email protected]", "", { "dependencies": { "undici-types": ">=7.24.0 <7.24.7" } }, "sha512-93d49wCWJkCyN87MxLpNN6KXPCwFw7bVUg79mGUV1OS40JGWwVCr8n61DXCSqz7CV6/VfPkb7xec7uP2M+QHWQ=="],
"@types/react": ["@types/[email protected]", "", { "dependencies": { "csstype": "^3.2.2" } }, "sha512-N0rFCuH9YoxG9/m61l9MfpJKfmLOVU0em7ipIz6TRgSSkvReLB9vL85GB+yr8Bs5leqpvg96JSwF4ZS1s4viQg=="],
"@types/react-dom": ["@types/[email protected]", "", { "peerDependencies": { "@types/react": "^19.3.0" } }, "sha512-ZI7bU42mZXXKHn/qNLEw2IrbiINU7X5+vfgdixBHkCNpYWXjKgfQ/P+uyGb5CjOLB9UcnTeg3rylQtV2hym44Q=="],
"ansi-regex": ["[email protected]", "", {}, "sha512-KzTVk2tCWAHtYrvvvaP8bJKJq2pVinhLcGEQdtLIYPbmNGNyYe8QwNaTUYQp2J7/vIsUKt5QCqAfUkYyG9DkOw=="],
"bun": ["[email protected]", "", { "optionalDependencies": { "@oven/bun-darwin-aarch64": "1.4.3", "@oven/bun-darwin-x64": "1.4.3", "@oven/bun-freebsd-aarch64": "1.4.3", "@oven/bun-freebsd-x64": "1.4.3", "@oven/bun-linux-aarch64": "1.4.3", "@oven/bun-linux-aarch64-android": "1.4.3", "@oven/bun-linux-aarch64-musl": "1.4.3", "@oven/bun-linux-x64": "1.4.3", "@oven/bun-linux-x64-android": "1.4.3", "@oven/bun-linux-x64-musl": "1.4.3", "@oven/bun-windows-aarch64": "1.4.3", "@oven/bun-windows-x64": "1.4.3" }, "os": [ "!aix", "!sunos", "!openbsd", ], "cpu": [ "x64", "arm64", ], "bin": { "bun": "bin/bun.exe", "bunx": "bin/bunx.exe" } }, "sha512-EvQQ2dUJlixjKgJjeXZvdsHUWIKnVfYufrQNDHa++0YM4XksQ4GtGofEdRnQSiyNp/yUWiX6SlqS8zugZCo+2A=="],
"bun-plugin-tailwind": ["[email protected]", "", { "peerDependencies": { "bun": ">=1.0.0" } }, "sha512-41jNC1tZRSK3s1o7pTNrLuQG8kL/0vR/JgiTmZAJ1eHwe0w5j6HFPKeqEk0WAD13jfrUC7+ULuewFBBCoADPpg=="],
"bun-types": ["[email protected]", "", { "dependencies": { "@types/node": "*" } }, "sha512-4N0ig0fEomHt5R0KCFWjovxow98rIoRwKolrYdCcknNwMekCXRnWEUvgu5soYV8QXtVsrUD8B95MBOZGPvr6KQ=="],
"chalk": ["[email protected]", "", {}, "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA=="],
"chokidar": ["[email protected]", "", { "dependencies": { "readdirp": "^5.0.0" } }, "sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw=="],
"cli-cursor": ["[email protected]", "", { "dependencies": { "restore-cursor": "^5.0.0" } }, "sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw=="],
"cli-spinners": ["[email protected]", "", {}, "sha512-bXfOC4QcT1tKXGorxL3wbJm6XJPDqEnij2gQ2m7ESQuE+/z9YFIWnl/5RpTiKWbMq3EVKR4fRLJGn6DVfu0mpw=="],
"commander": ["[email protected]", "", {}, "sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw=="],
"csstype": ["[email protected]", "", {}, "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ=="],
"detect-libc": ["[email protected]", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="],
"enhanced-resolve": ["[email protected]", "", { "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.3.3" } }, "sha512-9vhedylFonb2YGogzUKX6+Ja72gOJbN1QHAqdrvqLwhdl/QWbKopzoUC9EbQNVsAns/bx/4uyqalrQAoy1IByw=="],
"esbuild": ["[email protected]", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.28.2", "@esbuild/android-arm": "0.28.2", "@esbuild/android-arm64": "0.28.2", "@esbuild/android-x64": "0.28.2", "@esbuild/darwin-arm64": "0.28.2", "@esbuild/darwin-x64": "0.28.2", "@esbuild/freebsd-arm64": "0.28.2", "@esbuild/freebsd-x64": "0.28.2", "@esbuild/linux-arm": "0.28.2", "@esbuild/linux-arm64": "0.28.2", "@esbuild/linux-ia32": "0.28.2", "@esbuild/linux-loong64": "0.28.2", "@esbuild/linux-mips64el": "0.28.2", "@esbuild/linux-ppc64": "0.28.2", "@esbuild/linux-riscv64": "0.28.2", "@esbuild/linux-s390x": "0.28.2", "@esbuild/linux-x64": "0.28.2", "@esbuild/netbsd-arm64": "0.28.2", "@esbuild/netbsd-x64": "0.28.2", "@esbuild/openbsd-arm64": "0.28.2", "@esbuild/openbsd-x64": "0.28.2", "@esbuild/openharmony-arm64": "0.28.2", "@esbuild/sunos-x64": "0.28.2", "@esbuild/win32-arm64": "0.28.2", "@esbuild/win32-ia32": "0.28.2", "@esbuild/win32-x64": "0.28.2" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-HKVLS8dvII+xoKW9kmqxbRKrnWEXfJJr/FZhhJmiqIB0e053QNYFqOBouTMO/k5sID4MvCiUCvv8b9M4h32wIA=="],
"get-east-asian-width": ["[email protected]", "", {}, "sha512-XjH1AECxf0giL2V1aU8vKyRR2ppRUb5c0EvT7zuJTokQ74bNo52zOtghqdWIqrhUD79fo3x0WfKZdOqxF6LG1Q=="],
"graceful-fs": ["[email protected]", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="],
"is-interactive": ["[email protected]", "", {}, "sha512-qP1vozQRI+BMOPcjFzrjXuQvdak2pHNUMZoeG2eRbiSqyvbEf/wQtEOTOX1guk6E3t36RkaqiSt8A/6YElNxLQ=="],
"is-unicode-supported": ["[email protected]", "", {}, "sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ=="],
"jiti": ["[email protected]", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ=="],
"lightningcss": ["[email protected]", "", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-android-arm64": "1.32.0", "lightningcss-darwin-arm64": "1.32.0", "lightningcss-darwin-x64": "1.32.0", "lightningcss-freebsd-x64": "1.32.0", "lightningcss-linux-arm-gnueabihf": "1.32.0", "lightningcss-linux-arm64-gnu": "1.32.0", "lightningcss-linux-arm64-musl": "1.32.0", "lightningcss-linux-x64-gnu": "1.32.0", "lightningcss-linux-x64-musl": "1.32.0", "lightningcss-win32-arm64-msvc": "1.32.0", "lightningcss-win32-x64-msvc": "1.32.0" } }, "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ=="],
"lightningcss-android-arm64": ["[email protected]", "", { "os": "android", "cpu": "arm64" }, "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg=="],
"lightningcss-darwin-arm64": ["[email protected]", "", { "os": "darwin", "cpu": "arm64" }, "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ=="],
"lightningcss-darwin-x64": ["[email protected]", "", { "os": "darwin", "cpu": "x64" }, "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w=="],
"lightningcss-freebsd-x64": ["[email protected]", "", { "os": "freebsd", "cpu": "x64" }, "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig=="],
"lightningcss-linux-arm-gnueabihf": ["[email protected]", "", { "os": "linux", "cpu": "arm" }, "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw=="],
"lightningcss-linux-arm64-gnu": ["[email protected]", "", { "os": "linux", "cpu": "arm64" }, "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ=="],
"lightningcss-linux-arm64-musl": ["[email protected]", "", { "os": "linux", "cpu": "arm64" }, "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg=="],
"lightningcss-linux-x64-gnu": ["[email protected]", "", { "os": "linux", "cpu": "x64" }, "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA=="],
"lightningcss-linux-x64-musl": ["[email protected]", "", { "os": "linux", "cpu": "x64" }, "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg=="],
"lightningcss-wasm": ["[email protected]", "", { "dependencies": { "napi-wasm": "^1.0.1" } }, "sha512-OLAtqEyInBSVWjPrTjpLzcZUMUHO0q+2PFBXKr86nxZOu0P38givj/ZMtRaZ0d38pMTb9wQx+LtaLtHclv+sEA=="],
"lightningcss-win32-arm64-msvc": ["[email protected]", "", { "os": "win32", "cpu": "arm64" }, "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw=="],
"lightningcss-win32-x64-msvc": ["[email protected]", "", { "os": "win32", "cpu": "x64" }, "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q=="],
"lodash": ["[email protected]", "", {}, "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q=="],
"log-symbols": ["[email protected]", "", { "dependencies": { "is-unicode-supported": "^2.0.0", "yoctocolors": "^2.1.1" } }, "sha512-ja1E3yCr9i/0hmBVaM0bfwDjnGy8I/s6PP4DFp+yP+a+mrHO4Rm7DtmnqROTUkHIkqffC84YY7AeqX6oFk0WFg=="],
"lucide-react": ["[email protected]", "", { "peerDependencies": { "@types/react": "*", "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" }, "optionalPeers": ["@types/react"] }, "sha512-5qgMyyCPU6OvnKpLS2t693+ZtiqA8oaSweWoUows0BLFKFf6V75m4+CptpmohZHpD5IuOVIBXHBBiv2vpLJmtw=="],
"magic-string": ["[email protected]", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="],
"mimic-function": ["[email protected]", "", {}, "sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA=="],
"nanoid": ["[email protected]", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-uKdg2G3GNCKQn9byYOpxbGqrT2fGO5KRt5J/8b3pok8rT6qxGWF6hxMyJiEYtAf+FVyYuD9hRaDqX5uPFYJ4ZQ=="],
"onetime": ["[email protected]", "", { "dependencies": { "mimic-function": "^5.0.0" } }, "sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ=="],
"ora": ["[email protected]", "", { "dependencies": { "chalk": "^5.6.2", "cli-cursor": "^5.0.0", "cli-spinners": "^3.2.0", "is-interactive": "^2.0.0", "is-unicode-supported": "^2.1.0", "log-symbols": "^7.0.1", "stdin-discarder": "^0.3.2", "string-width": "^8.1.0" } }, "sha512-6VlU9MLXbjVQD04AZCMX28hVtA5bUoadvUqO76MUCVA0ilwJbMiHsITRPfyVm6p/BC0Av/BXMujx39WCe1LEqw=="],
"picocolors": ["[email protected]", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="],
"postcss": ["[email protected]", "", { "dependencies": { "nanoid": "^3.3.20", "picocolors": "^1.1.1", "source-map-js": "^1.2.2" } }, "sha512-YUbWYCgErwnMAVkjUnYa+KlMBzGpHwiIt/DE7f0G33AuX2LH9UcTk6KgWxd3zRU+pN3u/C+Xl3EnMLM4Hea7Sg=="],
"react": ["[email protected]", "", {}, "sha512-QNhWdt7nrgVWWQG58QuGsAR3jv3Hbe4mb4vz7w2csnHavciZJgda1CPNaldF6eVxv90ud3jUXpKlHL69gO38yw=="],
"react-dom": ["[email protected]", "", { "dependencies": { "scheduler": "^0.28.0" }, "peerDependencies": { "react": "^19.3.1" } }, "sha512-Y0bARz53lhEC1NMCASs0/j+faB/zqt3R8hpvg1e8+61ZVJ4UL0UdC4cBbXfZMcIuhrfgeILTLe+ZeBCavQPtKQ=="],
"readdirp": ["[email protected]", "", {}, "sha512-Kko+Y5XQ6fM+Ce3dq3m9YGxnacYZYl9cA1wZjaF3Vbry2L3i1qVg8+CAgNPsXRArPMUMCaOR7oa9Nqntc43JKA=="],
"restore-cursor": ["[email protected]", "", { "dependencies": { "onetime": "^7.0.0", "signal-exit": "^4.1.0" } }, "sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA=="],
"scheduler": ["[email protected]", "", {}, "sha512-juorfCmIkIw8tT+p5BXSm6PJjQF/ycEYmKyzURCIt/RaZIhL+PulbQ9Yu2z1HdOJDdqDTlxA1+xKBmHXJsczAw=="],
"signal-exit": ["[email protected]", "", {}, "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw=="],
"source-map-js": ["[email protected]", "", {}, "sha512-KGj/8Y43x35aZVDtt+J4mK1hoLGHULMYfSkODJNQjNDC3oW1PqPoxMwo0pLUsWM/UEGzON/NxeHywEfNXNP3Vw=="],
"stdin-discarder": ["[email protected]", "", {}, "sha512-eCPu1qRxPVkl5605OTWF8Wz40b4Mf45NY5LQmVPQ599knfs5QhASUm9GbJ5BDMDOXgrnh0wyEdvzmL//YMlw0A=="],
"string-width": ["[email protected]", "", { "dependencies": { "get-east-asian-width": "^1.5.0", "strip-ansi": "^7.1.2" } }, "sha512-ZbmZM0JCihQN91dWnxoipT2KOEyHqEyfRXUyjuRhW8b/xnqPDoq4gWEVApTVa9db2wN8mmoikgFBbjh71+cGeQ=="],
"strip-ansi": ["[email protected]", "", { "dependencies": { "ansi-regex": "^6.2.2" } }, "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w=="],
"tailwindcss": ["[email protected]", "", {}, "sha512-gOhV3P7ufE62QDGg1zVaTgCR+EtPv92k2nIhVcVKcLmxT1sUBsQGhnZj175j+MqRt4zLF7ic+sCYjfhxMxj7YQ=="],
"tapable": ["[email protected]", "", {}, "sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A=="],
"typescript": ["[email protected]", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
"undici-types": ["[email protected]", "", {}, "sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg=="],
"uuid": ["[email protected]", "", { "bin": { "uuid": "dist-node/bin/uuid" } }, "sha512-Sc5u1VVln93y1Xnz4R+CunOZc7R9LUcTmhj7Rc7Eg3HVcf7rVn2FOoa812fe0W8p3jD7xeoGSYbfqCGGH2B6xw=="],
"yoctocolors": ["[email protected]", "", {}, "sha512-xYqdZFUK/VYazNl/oCDYN+3WloWQwMfZxBoiNt6qNyk+xfOdi598muWE42rNZFp1kNOiqW936q5RhUdnpqElSg=="],
"@tailwindcss/oxide-wasm32-wasi/@emnapi/core": ["@emnapi/[email protected]", "", { "dependencies": { "@emnapi/wasi-threads": "1.2.3", "tslib": "^2.4.0" }, "bundled": true }, "sha512-zLpS5asjEb7lq8jYLq37N6XKaE41DIexlY1rF/z4/tIl3wo13Sqm28fRyfIsKZD+NZ8mM5RoKkpW/rBcuoSZSg=="],
"@tailwindcss/oxide-wasm32-wasi/@emnapi/runtime": ["@emnapi/[email protected]", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-Xz4Tpyki7XyrpbUK1jR1AhdAdaXyhhY4lZ3neLodmhpuWfy2PAQN5B46sAiU4liOXGLkHypn/qU+jvfWSCYYLA=="],
"@tailwindcss/oxide-wasm32-wasi/@emnapi/wasi-threads": ["@emnapi/[email protected]", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-ELEBe8PsLvvJ6QMr0zLt8ffvOHW/dc1m3CEzNMg7aJUv3bMaoDtw2TXyDAwkYBuroxxuHEwhRTLJSe5sya547g=="],
"@tailwindcss/oxide-wasm32-wasi/@napi-rs/wasm-runtime": ["@napi-rs/[email protected]", "", { "dependencies": { "@tybys/wasm-util": "^0.10.3" }, "peerDependencies": { "@emnapi/core": "^1.7.1 || ^2.0.0-alpha.4", "@emnapi/runtime": "^1.7.1 || ^2.0.0-alpha.4" }, "bundled": true }, "sha512-HshSvXzmBxNzqejd3k/KemgQ3hdREUYFRRrgxOZ+gPljDIsO2SDZrVZMd4Z/HsxudQbE62OVyVFE8z9BCurfjA=="],
"@tailwindcss/oxide-wasm32-wasi/@tybys/wasm-util": ["@tybys/[email protected]", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-W3c4gRigFS0T/Ma4qIYF3GDAc5AQdHb1yL5znJT1Zv1YaD9Kitx656wBjvr19qbiosmZT8lWDM5BEMynUqX65A=="],
"@tailwindcss/oxide-wasm32-wasi/tslib": ["[email protected]", "", { "bundled": true }, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
"bun-types/@types/node": ["@types/[email protected]", "", { "dependencies": { "undici-types": "~8.9.0" } }, "sha512-uMzsv9stDdofIZBx4UGh4Z9ZgXnGC9seRDscilBcTHAeOFk67MZg/k9bWVfO+bk4qZmG+l52hRnLwUiH2LnJTg=="],
"lightningcss-wasm/napi-wasm": ["[email protected]", "", { "bundled": true }, "sha512-h/4nMGsHjZDCYmQVNODIrYACVJ+I9KItbG+0si6W/jSjdA9JbWDoU4LLeMXVcEQGHjttI2tuXqDrbGF7qkUHHg=="],
"bun-types/@types/node/undici-types": ["[email protected]", "", {}, "sha512-KTDyRTYX8sWmKXAikPHHSyc63CRPETMctyjKFupcC6OBLXT3xsN0e9aF7m+mIXutFWpUXuedtowG7iLOzp0kQg=="],
}
}
+7
View File
@@ -0,0 +1,7 @@
import type { BunextConfig } from "@moduletrace/bunext/types";
const config: BunextConfig = {
port: 7010,
};
export default config;
+19
View File
@@ -0,0 +1,19 @@
{
"name": "@moduletrace/twui-dev",
"version": "0.0.0",
"private": true,
"type": "module",
"scripts": {
"dev": "bunx bunext dev",
"build": "bunx bunext build",
"start": "bunx bunext start",
"typecheck": "tsc --noEmit"
},
"dependencies": {
"@moduletrace/bunext": "^1.3.4",
"lucide-react": "^1.59.0"
},
"devDependencies": {
"typescript": "^5.9.3"
}
}
+23
View File
@@ -0,0 +1,23 @@
import type { PropsWithChildren, ReactNode } from "react";
import H2 from "@moduletrace/twui/layout/H2";
import P from "@moduletrace/twui/layout/P";
type Props = PropsWithChildren<{
title: string;
description?: ReactNode;
}>;
/**
* A consistently spaced block for the demo pages.
*/
export default function DemoSection({ title, description, children }: Props) {
return (
<section className="w-full border-b border-slate-200 border-solid pb-10 dark:border-white/10">
<H2>{title}</H2>
{description ? (
<P className="!pt-0 text-sm opacity-60">{description}</P>
) : null}
<div className="mt-4 w-full">{children}</div>
</section>
);
}
+50
View File
@@ -0,0 +1,50 @@
import type { BunextRootComponentProps } from "@moduletrace/bunext/types";
import ColorSchemeSelector from "@moduletrace/twui/elements/ColorSchemeSelector";
import "../styles/global.css";
const NAV = [
{ href: "/", label: "Overview" },
{ href: "/buttons", label: "Buttons" },
{ href: "/forms", label: "Forms" },
{ href: "/elements", label: "Elements" },
];
export default function Root({ children }: BunextRootComponentProps) {
return (
<div className="min-h-screen bg-background-light text-foreground-light dark:bg-background-dark dark:text-foreground-dark">
<header className="sticky top-0 z-40 border-b border-slate-200 border-solid bg-background-light/90 backdrop-blur dark:border-white/10 dark:bg-background-dark/90">
<div className="mx-auto flex w-full max-w-container flex-wrap items-center justify-between gap-4 px-4 py-3 sm:px-10">
<div className="flex flex-wrap items-center gap-6">
<a
href="/"
className="text-lg font-bold no-underline"
>
twui
</a>
<nav className="flex flex-wrap items-center gap-4 text-sm">
{NAV.map((item) => (
<a
key={item.href}
href={item.href}
className="opacity-60 no-underline transition-opacity hover:opacity-100"
>
{item.label}
</a>
))}
</nav>
</div>
<ColorSchemeSelector />
</div>
</header>
<main className="mx-auto w-full max-w-container px-4 py-10 sm:px-10">
{children}
</main>
<footer className="mx-auto w-full max-w-container px-4 pb-10 text-xs opacity-50 sm:px-10">
Local playground for @moduletrace/twui. Edit anything in{" "}
<code>src/</code> and the page updates without a build step.
</footer>
</div>
);
}
+174
View File
@@ -0,0 +1,174 @@
import { useState } from "react";
import { ArrowRight, Download, Plus, Sparkles, Trash2 } from "lucide-react";
import Button, {
TWUIButtonColors,
TWUIButtonSizes,
TWUIButtonVariants,
} from "@moduletrace/twui/layout/Button";
import Divider from "@moduletrace/twui/layout/Divider";
import H1 from "@moduletrace/twui/layout/H1";
import P from "@moduletrace/twui/layout/P";
import Row from "@moduletrace/twui/layout/Row";
import Span from "@moduletrace/twui/layout/Span";
import Stack from "@moduletrace/twui/layout/Stack";
import DemoSection from "../(components)/DemoSection";
export default function ButtonsPage() {
const [saving, setSaving] = useState(false);
function save() {
setSaving(true);
window.setTimeout(() => setSaving(false), 1500);
}
return (
<Stack className="w-full gap-10">
<Stack className="gap-2">
<H1 className="!mb-0">Buttons</H1>
<P className="!pt-0 opacity-70">
Every color is paired with the three variants. Classes are
merged with <code>twMerge</code>, so a consumer{" "}
<code>className</code> always wins.
</P>
</Stack>
{TWUIButtonVariants.map((variant) => (
<DemoSection
key={variant}
title={`${variant} variant`}
description={`variant="${variant}" across TWUIButtonColors.`}
>
<Row className="gap-3">
{TWUIButtonColors.map((color) => (
<Button
key={color}
title={`${color} ${variant} button`}
color={color}
variant={variant}
>
{color}
</Button>
))}
</Row>
</DemoSection>
))}
<DemoSection
title="Sizes"
description="smaller, small, normal, large and larger."
>
<Row className="items-center gap-3">
{TWUIButtonSizes.map((size) => (
<Button
key={size}
title={`${size} button`}
size={size}
>
{size}
</Button>
))}
</Row>
</DemoSection>
<DemoSection
title="Icons and loading"
description="Icons accept a lucide name or a React element. Loading keeps the label in place and swaps in a spinner."
>
<Row className="gap-3">
<Button
title="Create button"
beforeIcon={<Plus size={16} />}
>
Create
</Button>
<Button
title="Sparkle button"
color="secondary"
afterIcon={<Sparkles size={16} />}
>
Enhance
</Button>
<Button
title="Download button"
color="success"
after_icon_name="Download"
>
Download
</Button>
<Button
title="Delete button"
color="error"
variant="outlined"
beforeIcon={<Trash2 size={16} />}
>
Delete
</Button>
<Button
title="Saving button"
color="primary"
loading={saving}
onClick={save}
>
{saving ? "Saving" : "Save changes"}
</Button>
<Button
title="Disabled button"
disabled
color="gray"
variant="outlined"
>
Disabled
</Button>
</Row>
</DemoSection>
<DemoSection
title="Links and arrows"
description="Passing href renders an anchor around the button markup."
>
<Stack className="gap-3">
<Row className="gap-3">
<Button
title="Open buttons page"
href="#"
afterIcon={<ArrowRight size={16} />}
>
Anchor button
</Button>
<Button
title="Download from link"
href="#"
color="secondary"
variant="outlined"
before_icon_name="Download"
>
Outlined link
</Button>
</Row>
<Divider />
<Row className="gap-2">
<Span variant="faded" size="small">
Sizes map to the lucide icon scale too:
</Span>
<Button
title="Smaller download"
size="smaller"
variant="ghost"
beforeIcon={<Download size={14} />}
>
Smaller
</Button>
<Button
title="Larger arrow"
size="larger"
variant="ghost"
afterIcon={<ArrowRight size={20} />}
>
Larger
</Button>
</Row>
</Stack>
</DemoSection>
</Stack>
);
}
+376
View File
@@ -0,0 +1,376 @@
import { useState } from "react";
import {
ArrowUpRight,
Bell,
Box,
CheckCircle2,
ChevronDown,
Inbox,
} from "lucide-react";
import Border from "@moduletrace/twui/elements/Border";
import Breadcrumbs from "@moduletrace/twui/elements/Breadcrumbs";
import Card from "@moduletrace/twui/elements/Card";
import CheckBulletPoints from "@moduletrace/twui/elements/CheckBulletPoints";
import CodeBlock from "@moduletrace/twui/elements/CodeBlock";
import Dropdown from "@moduletrace/twui/elements/Dropdown";
import EmptyContent from "@moduletrace/twui/elements/EmptyContent";
import Loading from "@moduletrace/twui/elements/Loading";
import Modal from "@moduletrace/twui/elements/Modal";
import Pagination from "@moduletrace/twui/elements/Pagination";
import Paper from "@moduletrace/twui/elements/Paper";
import Popover from "@moduletrace/twui/elements/Popover";
import StarRating from "@moduletrace/twui/elements/StarRating";
import Table from "@moduletrace/twui/elements/Table";
import Tabs from "@moduletrace/twui/elements/Tabs";
import Tag from "@moduletrace/twui/elements/Tag";
import Toast from "@moduletrace/twui/elements/Toast";
import Button from "@moduletrace/twui/layout/Button";
import Divider from "@moduletrace/twui/layout/Divider";
import H1 from "@moduletrace/twui/layout/H1";
import P from "@moduletrace/twui/layout/P";
import Row from "@moduletrace/twui/layout/Row";
import Span from "@moduletrace/twui/layout/Span";
import Stack from "@moduletrace/twui/layout/Stack";
import { TWUIButtonColors } from "@moduletrace/twui/layout/Button";
import DemoSection from "../(components)/DemoSection";
const TABLE_DATA = [
{ component: "Button", folder: "layout", status: "stable" },
{ component: "Input", folder: "form", status: "stable" },
{ component: "Tabs", folder: "elements", status: "stable" },
{ component: "AceEditor", folder: "editors", status: "optional peer" },
];
const TAG_COLORS = TWUIButtonColors.filter((color) =>
["primary", "secondary", "success", "error", "warning", "gray"].includes(
color,
),
);
export default function ElementsPage() {
const [page, setPage] = useState(1);
const [toastOpen, setToastOpen] = useState(false);
const [rating, setRating] = useState(3);
return (
<Stack className="w-full gap-10">
<Stack className="gap-2">
<H1 className="!mb-0">Elements</H1>
<P className="!pt-0 opacity-70">
Composite components. Overlays render into portals appended
to <code>document.body</code>, so they escape any stacking
context in the page.
</P>
</Stack>
<DemoSection
title="Cards, tags and surfaces"
description="Card is a link when href is passed. Tag reuses the button color palette."
>
<Stack className="w-full gap-4">
<Row className="items-stretch gap-4">
<Card className="w-full max-w-sm flex-col items-start gap-2">
<Span className="font-semibold">Plain card</Span>
<Span size="small" variant="faded">
A bordered surface with no link behaviour.
</Span>
</Card>
<Card
href="#"
className="w-full max-w-sm flex-col items-start gap-2"
>
<Span className="font-semibold">
Linked card
<ArrowUpRight
size={14}
className="ml-1 inline-block"
/>
</Span>
<Span size="small" variant="faded">
The whole card is clickable.
</Span>
</Card>
<Paper className="w-full max-w-sm">
<Span className="font-semibold">Paper</Span>
<Span size="small" variant="faded">
Used for grouped content and forms.
</Span>
</Paper>
</Row>
<Row className="gap-3">
{TAG_COLORS.map((color) => (
<Tag key={color} color={color}>
{color}
</Tag>
))}
{TAG_COLORS.map((color) => (
<Tag
key={`${color}-outlined`}
color={color}
variant="outlined"
>
{color}
</Tag>
))}
</Row>
<Border className="w-full">
<Row className="w-full justify-between">
<Span size="small">
Border with default spacing
</Span>
<Tag color="gray" variant="ghost">
variant matrix
</Tag>
</Row>
</Border>
</Stack>
</DemoSection>
<DemoSection
title="Tabs"
description="Tab content stays mounted; switching is client-side state."
>
<Tabs
className="w-full"
tabsContentArray={[
{
title: "Overview",
content: (
<P className="!pt-2">
The first tab is active by default. Tab
values are slugified from the title unless
you provide one.
</P>
),
},
{
title: "Details",
content: (
<CheckBulletPoints
className="pt-2"
bulletPoints={[
{
title: "Accepts an array of tabs",
},
{
title: "Supports a default value",
},
{
title: "Can sync to the URL hash",
},
]}
/>
),
},
{
title: "Disabled-looking",
content: (
<P className="!pt-2 opacity-60">
This is just a third tab; there is no
disabled state in the current API.
</P>
),
},
]}
/>
</DemoSection>
<DemoSection
title="Table and pagination"
description="Table derives columns from the keys of the first row."
>
<Stack className="w-full gap-4">
<Border className="w-full overflow-hidden !p-0">
<Table data={TABLE_DATA} />
</Border>
<Pagination
page={page}
setPage={setPage}
count={42}
limit={5}
className="justify-center"
/>
<Span size="small" variant="faded">
Page {page} of {Math.ceil(42 / 5)}
</Span>
</Stack>
</DemoSection>
<DemoSection
title="Overlays"
description="Modal and Popover share the same implementation; Popover renders in place while Modal is a full dialog."
>
<Row className="items-start gap-4">
<OverlayDemo />
</Row>
</DemoSection>
<DemoSection
title="Dropdown"
description="A lightweight positioned panel with click/hover targets."
>
<Dropdown
target={
<Button
title="Open dropdown"
color="gray"
variant="outlined"
afterIcon={<ChevronDown size={16} />}
>
Actions
</Button>
}
contentWrapperProps={{ className: "min-w-48" }}
>
<Stack className="gap-1">
<Span size="small">Edit item</Span>
<Span size="small">Duplicate item</Span>
<Divider />
<Span size="small" className="text-error">
Delete item
</Span>
</Stack>
</Dropdown>
</DemoSection>
<DemoSection
title="Toasts"
description="Toast portals into a body-level root and auto-closes after four seconds."
>
<Row className="gap-3">
<Button
title="Show success toast"
color="success"
onClick={() => setToastOpen(true)}
beforeIcon={<Bell size={16} />}
>
Show toast
</Button>
<Span size="small" variant="faded">
Hovering the toast pauses the timer.
</Span>
</Row>
<Toast
open={toastOpen}
setOpen={setToastOpen}
color="success"
>
Build finished successfully.
</Toast>
</DemoSection>
<DemoSection
title="Status and feedback"
description="Loading, empty states, code blocks and ratings."
>
<div className="grid w-full gap-6 md:grid-cols-2">
<Stack className="w-full gap-3">
<Span className="font-semibold">Loading</Span>
<Row className="gap-4">
<Loading size="smaller" />
<Loading size="small" />
<Loading size="normal" />
<Loading size="large" />
</Row>
<Span className="font-semibold">Star rating</Span>
<StarRating
value={rating}
allowRating
changeHandler={setRating}
/>
<Span size="small" variant="faded">
Current rating: {rating}
</Span>
</Stack>
<EmptyContent
title="No results — EmptyContent takes an optional link and icon."
icon={<Inbox size={24} />}
url="#"
/>
</div>
<Stack className="mt-6 w-full gap-2">
<Span className="font-semibold">Code block</Span>
<CodeBlock data-title="install">
{`bun add @moduletrace/twui`}
</CodeBlock>
</Stack>
</DemoSection>
<DemoSection
title="Breadcrumbs"
description="Infer crumbs from the current path, or pass pageUrl to render deterministically."
>
<Stack className="w-full gap-4">
<Breadcrumbs pageUrl="/elements/overlays/modal" />
<Row className="gap-2">
<CheckCircle2 size={16} className="text-success" />
<Span size="small" variant="faded">
The divider and back button are configurable.
</Span>
</Row>
</Stack>
</DemoSection>
</Stack>
);
}
function OverlayDemo() {
const [modalOpen, setModalOpen] = useState(false);
return (
<>
<Button
title="Open modal"
onClick={() => setModalOpen(true)}
beforeIcon={<Box size={16} />}
>
Open modal
</Button>
<Modal
open={modalOpen}
setOpen={setModalOpen}
target={null}
no_cancel_button
>
<Stack className="w-full gap-3">
<Span className="text-lg font-semibold">Modal content</Span>
<P className="!pt-0">
The modal is controlled by the <code>open</code> and{" "}
<code>setOpen</code> props. Without a target it renders
nothing until you set it.
</P>
<Row className="justify-end">
<Button
title="Close modal"
color="gray"
variant="outlined"
onClick={() => setModalOpen(false)}
>
Close
</Button>
</Row>
</Stack>
</Modal>
<Popover
trigger="hover"
target={
<Tag color="secondary" variant="outlined">
Hover for a popover
</Tag>
}
>
<Stack className="w-56 gap-1">
<Span className="font-semibold">Popover</Span>
<Span size="small" variant="faded">
Same props as Modal, plus hover trigger support.
</Span>
</Stack>
</Popover>
</>
);
}
+243
View File
@@ -0,0 +1,243 @@
import { useState } from "react";
import { AtSign, Info, Mail, Search } from "lucide-react";
import Button from "@moduletrace/twui/layout/Button";
import H1 from "@moduletrace/twui/layout/H1";
import P from "@moduletrace/twui/layout/P";
import Row from "@moduletrace/twui/layout/Row";
import Span from "@moduletrace/twui/layout/Span";
import Stack from "@moduletrace/twui/layout/Stack";
import Checkbox from "@moduletrace/twui/form/Checkbox";
import Form from "@moduletrace/twui/form/Form";
import Input from "@moduletrace/twui/form/Input";
import Radios from "@moduletrace/twui/form/Radios";
import SearchSelect from "@moduletrace/twui/form/SearchSelect";
import Select from "@moduletrace/twui/form/Select";
import Textarea from "@moduletrace/twui/form/Textarea";
import Toggle from "@moduletrace/twui/elements/Toggle";
import DemoSection from "../(components)/DemoSection";
const FRUIT_OPTIONS = [
{ value: "apple", title: "Apple" },
{ value: "banana", title: "Banana" },
{ value: "cherry", title: "Cherry" },
{ value: "dragonfruit", title: "Dragon fruit" },
{ value: "elderberry", title: "Elderberry" },
];
const ROLE_OPTIONS = [
{ value: "viewer", title: "Viewer", default: true },
{ value: "editor", title: "Editor" },
{ value: "admin", title: "Admin" },
];
export default function FormsPage() {
const [notifications, setNotifications] = useState(true);
const [autoSave, setAutoSave] = useState(true);
const [submitted, setSubmitted] = useState<Record<string, unknown> | null>(
null,
);
return (
<Stack className="w-full gap-10">
<Stack className="gap-2">
<H1 className="!mb-0">Forms</H1>
<P className="!pt-0 opacity-70">
The input family shares one shell: label, optional info,
validation state, clear button and prefix/suffix slots.
</P>
</Stack>
<DemoSection
title="Inputs"
description="Variants, labels, icons, prefixes and suffixes."
>
<div className="grid w-full gap-4 md:grid-cols-2">
<Input label="Full name" placeholder="Ada Lovelace" />
<Input
label="Email"
type="email"
placeholder="[email protected]"
prefix={<AtSign size={16} />}
info="We never share your address."
/>
<Input
label="Password"
type="password"
defaultValue="hunter2"
/>
<Input
label="Search"
placeholder="Search components..."
prefix={<Search size={16} />}
/>
<Input
label="Amount"
placeholder="0.00"
prefix="$"
suffix="USD"
inputMode="decimal"
/>
<Input
label="Error state"
variant="error"
defaultValue="not-an-email"
info="This field is marked invalid."
clearInputProps={{ className: "hidden" }}
/>
</div>
</DemoSection>
<DemoSection
title="Textarea and number input"
description="Textarea is Input with istextarea. Number buttons are available through NumberInputButtons."
>
<div className="grid w-full gap-4 md:grid-cols-2">
<Textarea
label="Notes"
placeholder="Anything we should know?"
rows={4}
/>
<Input
label="Quantity"
type="number"
defaultValue={3}
min={0}
max={10}
/>
</div>
</DemoSection>
<DemoSection
title="Selects"
description="Select for short lists; SearchSelect adds filtering and multi-select."
>
<div className="grid w-full gap-4 md:grid-cols-2">
<Select label="Role" options={ROLE_OPTIONS} />
<SearchSelect
label="Favourite fruit"
options={FRUIT_OPTIONS}
placeholder="Pick a fruit"
/>
<SearchSelect
label="Fruits (multiple)"
options={FRUIT_OPTIONS}
defaultValues={["apple", "cherry"]}
multiple
/>
</div>
</DemoSection>
<DemoSection
title="Toggles and choices"
description="Checkbox and Radios render custom markup; Toggle is a pill switch."
>
<div className="grid w-full items-start gap-6 md:grid-cols-2">
<Stack className="gap-4">
<Checkbox
title="Email notifications"
label="Send me an email when a build finishes."
defaultChecked
changeHandler={setNotifications}
/>
<Checkbox
title="Beta features"
label="Opt into experimental components."
/>
<Row className="gap-3">
<Span size="small" variant="faded">
Notifications are {notifications ? "on" : "off"}
</Span>
</Row>
<Row className="gap-3">
<Toggle
default_active={autoSave}
changeHandler={setAutoSave}
/>
<Span size="small" variant="faded">
Auto-save is {autoSave ? "on" : "off"}
</Span>
</Row>
</Stack>
<Radios
name="Plan"
values={[
{ value: "free", title: "Free" },
{ value: "pro", title: "Pro", default: true },
{ value: "team", title: "Team" },
]}
/>
</div>
</DemoSection>
<DemoSection
title="Form wrapper"
description="Form collects FormData on submit. Inputs need a name to appear."
>
<div className="grid w-full items-start gap-6 md:grid-cols-2">
<Form
submitHandler={(_event, data) => {
setSubmitted(data);
}}
changeHandler={() => {
setSubmitted(null);
}}
className="gap-4"
>
<Input
label="Project name"
name="project"
placeholder="my-project"
required
/>
<Input
label="Email"
name="email"
type="email"
placeholder="[email protected]"
prefix={<Mail size={16} />}
required
/>
<Textarea
label="Description"
name="description"
rows={3}
placeholder="What are you building?"
/>
<Row className="gap-3">
<Button title="Submit form" type="submit">
Submit
</Button>
<Button
title="Reset form"
type="reset"
color="gray"
variant="outlined"
>
Reset
</Button>
</Row>
</Form>
<Stack className="w-full gap-2">
<Span size="small" variant="faded">
submitHandler payload
</Span>
<pre className="w-full overflow-x-auto rounded-default border border-slate-200 border-solid bg-gray/40 p-4 text-xs dark:border-white/10 dark:bg-white/5">
{submitted
? JSON.stringify(submitted, null, 2)
: "Submit the form to see the collected data."}
</pre>
<Row className="items-start gap-2">
<Info size={16} className="mt-0.5 shrink-0" />
<Span size="small" variant="faded">
Checkbox and Toggle are div-based and are not
part of FormData. Radios do submit a value.
</Span>
</Row>
</Stack>
</div>
</DemoSection>
</Stack>
);
}
+104
View File
@@ -0,0 +1,104 @@
import Card from "@moduletrace/twui/elements/Card";
import Tag from "@moduletrace/twui/elements/Tag";
import H1 from "@moduletrace/twui/layout/H1";
import P from "@moduletrace/twui/layout/P";
import Row from "@moduletrace/twui/layout/Row";
import Stack from "@moduletrace/twui/layout/Stack";
import Span from "@moduletrace/twui/layout/Span";
const PAGES = [
{
href: "/buttons",
title: "Buttons",
description: "Colors, variants, sizes, icons, loading and links.",
},
{
href: "/forms",
title: "Forms",
description:
"Input, textarea, selects, checkboxes, radios and the Form wrapper.",
},
{
href: "/elements",
title: "Elements",
description:
"Cards, tags, tables, tabs, overlays, toasts and status blocks.",
},
];
const TOKENS = [
{ className: "bg-primary", name: "primary" },
{ className: "bg-secondary", name: "secondary" },
{ className: "bg-accent", name: "accent" },
{ className: "bg-gray", name: "gray" },
{ className: "bg-success", name: "success" },
{ className: "bg-error", name: "error" },
{ className: "bg-warning", name: "warning" },
{ className: "bg-link", name: "link" },
];
export default function OverviewPage() {
return (
<Stack className="w-full gap-10">
<Stack className="gap-3">
<H1 className="!mb-0">twui development playground</H1>
<P className="!pt-0 max-w-3xl opacity-70">
This app imports the library straight from{" "}
<code>../src</code> through a symlinked path alias, so edits
to components hot-reload here without running{" "}
<code>tsc --watch</code>. It is a local development tool and
is not part of the published package.
</P>
<Row className="gap-2">
<Tag color="success" variant="outlined">
bunext
</Tag>
<Tag color="secondary" variant="outlined">
Tailwind v4
</Tag>
<Tag color="gray" variant="outlined">
SSR + HMR
</Tag>
</Row>
</Stack>
<Row className="items-stretch gap-4">
{PAGES.map((page) => (
<Card
key={page.href}
href={page.href}
className="w-full max-w-sm flex-col items-start gap-2 sm:w-72"
>
<Span className="text-lg font-semibold">
{page.title}
</Span>
<Span size="small" variant="faded">
{page.description}
</Span>
</Card>
))}
</Row>
<Stack className="gap-3">
<H1 className="!mb-0 text-xl">Default theme tokens</H1>
<P className="!pt-0 text-sm opacity-60">
Swatches read the values defined in{" "}
<code>src/base.css</code>. Override them in a consumer's{" "}
<code>@theme inline</code> block.
</P>
<Row className="gap-3">
{TOKENS.map((token) => (
<Stack key={token.name} className="items-center gap-1">
<div
className={`h-12 w-16 rounded-default border border-slate-200 border-solid dark:border-white/10 ${token.className}`}
/>
<Span size="smaller" variant="faded">
{token.name}
</Span>
</Stack>
))}
</Row>
</Stack>
</Stack>
);
}
+9
View File
@@ -0,0 +1,9 @@
/*
* Reuse the published base stylesheet so the playground renders with the same
* tokens and preflight consumers get. `twui` is a symlink to the library `src`,
* so `base.css`'s own `@source "./"` scans the real component sources.
*/
@import "../../twui/base.css";
/* Classes used only by the playground pages themselves. */
@source "../";
+23
View File
@@ -0,0 +1,23 @@
{
"compilerOptions": {
"lib": ["ESNext", "DOM", "DOM.Iterable"],
"target": "ESNext",
"module": "ESNext",
"moduleDetection": "force",
"jsx": "react-jsx",
"moduleResolution": "bundler",
"strict": true,
"skipLibCheck": true,
"esModuleInterop": true,
"noFallthroughCasesInSwitch": true,
"resolveJsonModule": true,
"forceConsistentCasingInFileNames": true,
"noEmit": true,
"baseUrl": ".",
"paths": {
"@moduletrace/twui": ["./twui/index.ts"],
"@moduletrace/twui/*": ["./twui/*"]
}
},
"include": ["src", "bunext.config.ts"]
}
Symlink
+1
View File
@@ -0,0 +1 @@
../src
+3 -1
View File
@@ -38,7 +38,9 @@
} }
}, },
"scripts": { "scripts": {
"dev": "tsc --watch", "dev": "bun run --cwd dev dev",
"dev:types": "tsc --watch",
"dev:typecheck": "bun run --cwd dev typecheck",
"compile": "rm -rf dist tsconfig.tsbuildinfo && tsc -p tsconfig.json && bun scripts/post-compile.ts", "compile": "rm -rf dist tsconfig.tsbuildinfo && tsc -p tsconfig.json && bun scripts/post-compile.ts",
"typecheck": "tsc -p tsconfig.json --noEmit", "typecheck": "tsc -p tsconfig.json --noEmit",
"test": "bun test src", "test": "bun test src",
+11 -4
View File
@@ -34,6 +34,11 @@ export type TWUISearchSelectProps<
defaultValues?: KeyType[]; defaultValues?: KeyType[];
changeHandler?: (value: KeyType | KeyType[] | "", data?: T | T[]) => void; changeHandler?: (value: KeyType | KeyType[] | "", data?: T | T[]) => void;
placeholder?: string; placeholder?: string;
/**
* Whether the dropdown should still be open
* after a selection is made
*/
keep_dropdown_open_on_commit?: boolean;
}; };
/** /**
@@ -64,6 +69,7 @@ export default function SearchSelect<
placeholder, placeholder,
multiple = false, multiple = false,
defaultValues, defaultValues,
keep_dropdown_open_on_commit,
...props ...props
}: TWUISearchSelectProps<KeyType, T>) { }: TWUISearchSelectProps<KeyType, T>) {
const rootRef = React.useRef<HTMLDivElement>(null); const rootRef = React.useRef<HTMLDivElement>(null);
@@ -160,17 +166,18 @@ export default function SearchSelect<
setSelected(next); setSelected(next);
notify(next); notify(next);
setQuery(""); setQuery("");
setOpen(true);
setActiveIndex(0);
inputRef.current?.focus(); inputRef.current?.focus();
} else { } else {
const next = [option]; const next = [option];
setSelected(next); setSelected(next);
setQuery(getOptionLabel(option)); setQuery(getOptionLabel(option));
setOpen(true);
setActiveIndex(0);
notify(next); notify(next);
} }
const commit_open_status = keep_dropdown_open_on_commit || false;
setActiveIndex(0);
setOpen(commit_open_status);
}, },
[multiple, notify, selected], [multiple, notify, selected],
); );