From dad7079181f968f39fd21d5baf63813869a85fda Mon Sep 17 00:00:00 2001 From: brxs Date: Sat, 18 Jul 2026 12:12:53 -0700 Subject: [PATCH] Add shared React UI package --- .github/workflows/ci.yml | 1 + .gitignore | 2 +- AGENTS.md | 17 +- README.md | 42 +- package-lock.json | 1184 ++++++++++++++++- package.json | 17 +- packages/ui/LICENSE | 21 + packages/ui/README.md | 94 ++ packages/ui/package.json | 149 +++ packages/ui/src/components/Button.tsx | 84 ++ packages/ui/src/components/Card.tsx | 36 + packages/ui/src/components/Cluster.tsx | 65 + packages/ui/src/components/Divider.tsx | 38 + packages/ui/src/components/Field.tsx | 80 ++ packages/ui/src/components/Grid.tsx | 45 + packages/ui/src/components/Input.tsx | 35 + packages/ui/src/components/List.tsx | 51 + packages/ui/src/components/Note.tsx | 42 + packages/ui/src/components/Quote.tsx | 33 + packages/ui/src/components/Section.tsx | 51 + packages/ui/src/components/SkipLink.tsx | 29 + packages/ui/src/components/Spinner.tsx | 40 + packages/ui/src/components/Stack.tsx | 47 + packages/ui/src/components/Status.tsx | 47 + packages/ui/src/components/Surface.tsx | 32 + packages/ui/src/components/Tag.tsx | 79 ++ packages/ui/src/components/Textarea.tsx | 34 + packages/ui/src/components/VisuallyHidden.tsx | 22 + .../ui/src/components/typography/Body.tsx | 22 + .../ui/src/components/typography/Caption.tsx | 38 + .../ui/src/components/typography/Code.tsx | 22 + .../ui/src/components/typography/Display.tsx | 38 + .../ui/src/components/typography/Heading.tsx | 32 + .../ui/src/components/typography/Lead.tsx | 22 + .../ui/src/components/typography/Signal.tsx | 22 + packages/ui/src/lib/cn.ts | 5 + packages/ui/src/lib/polymorphic.ts | 8 + packages/ui/src/lib/surface.ts | 1 + packages/ui/tailwind.css | 77 ++ packages/ui/test/consumer.css | 3 + packages/ui/test/contracts.test.mjs | 100 ++ packages/ui/test/render.test.mjs | 98 ++ packages/ui/test/tailwind.test.mjs | 32 + packages/ui/tsconfig.json | 17 + 44 files changed, 2941 insertions(+), 13 deletions(-) create mode 100644 packages/ui/LICENSE create mode 100644 packages/ui/README.md create mode 100644 packages/ui/package.json create mode 100644 packages/ui/src/components/Button.tsx create mode 100644 packages/ui/src/components/Card.tsx create mode 100644 packages/ui/src/components/Cluster.tsx create mode 100644 packages/ui/src/components/Divider.tsx create mode 100644 packages/ui/src/components/Field.tsx create mode 100644 packages/ui/src/components/Grid.tsx create mode 100644 packages/ui/src/components/Input.tsx create mode 100644 packages/ui/src/components/List.tsx create mode 100644 packages/ui/src/components/Note.tsx create mode 100644 packages/ui/src/components/Quote.tsx create mode 100644 packages/ui/src/components/Section.tsx create mode 100644 packages/ui/src/components/SkipLink.tsx create mode 100644 packages/ui/src/components/Spinner.tsx create mode 100644 packages/ui/src/components/Stack.tsx create mode 100644 packages/ui/src/components/Status.tsx create mode 100644 packages/ui/src/components/Surface.tsx create mode 100644 packages/ui/src/components/Tag.tsx create mode 100644 packages/ui/src/components/Textarea.tsx create mode 100644 packages/ui/src/components/VisuallyHidden.tsx create mode 100644 packages/ui/src/components/typography/Body.tsx create mode 100644 packages/ui/src/components/typography/Caption.tsx create mode 100644 packages/ui/src/components/typography/Code.tsx create mode 100644 packages/ui/src/components/typography/Display.tsx create mode 100644 packages/ui/src/components/typography/Heading.tsx create mode 100644 packages/ui/src/components/typography/Lead.tsx create mode 100644 packages/ui/src/components/typography/Signal.tsx create mode 100644 packages/ui/src/lib/cn.ts create mode 100644 packages/ui/src/lib/polymorphic.ts create mode 100644 packages/ui/src/lib/surface.ts create mode 100644 packages/ui/tailwind.css create mode 100644 packages/ui/test/consumer.css create mode 100644 packages/ui/test/contracts.test.mjs create mode 100644 packages/ui/test/render.test.mjs create mode 100644 packages/ui/test/tailwind.test.mjs create mode 100644 packages/ui/tsconfig.json diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 811c5f0..0a65624 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -18,3 +18,4 @@ jobs: - run: npm ci - run: npm run check - run: npm pack --dry-run + - run: npm pack --dry-run --workspace @protocol-works/ui diff --git a/.gitignore b/.gitignore index 9dab5bd..b57dfc0 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,4 @@ node_modules/ +packages/*/dist/ *.tgz .DS_Store - diff --git a/AGENTS.md b/AGENTS.md index 0c9fd34..6a3abe8 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -5,12 +5,22 @@ This repository owns the reusable [protocol]works design-system foundation shared by product repositories. The first package is `@protocol-works/design-tokens`. +## Packages + +- `@protocol-works/design-tokens` at the repository root is the CSS-first, platform-neutral + foundation. +- `@protocol-works/ui` under `packages/ui/` owns portable React primitives proven across product + repositories. + ## Boundaries - Keep the token package CSS-first and platform-neutral. - Shared brand primitives, typography, motion, geometry, and theme values belong here. -- Product-specific semantic roles, runtime theme persistence, and application components stay in - their consuming repositories until at least two consumers need the same contract. +- Portable component contracts needed by at least two consumers belong in `@protocol-works/ui`. +- Product-specific semantic roles, runtime theme persistence, framework adapters, and domain + components stay in their consuming repositories. +- Do not add Next.js, Tauri, or another product runtime as a UI package dependency. +- Publish UI components through direct subpath exports; consumers should not need a barrel import. - Do not add a token compiler while CSS remains sufficient for every consumer. - Keep raw values in namespaced `--pw-*` custom properties. Tailwind aliases may be unnamespaced because they intentionally define the shared utility vocabulary. @@ -22,7 +32,8 @@ Run before committing: ```bash npm run check npm pack --dry-run +npm pack --dry-run --workspace @protocol-works/ui ``` Tests must protect the public token names, Tailwind utility generation, light/dark theme values, -and required contrast. +required contrast, UI subpath exports, server rendering, and packaged files. diff --git a/README.md b/README.md index 70200a8..0c9533f 100644 --- a/README.md +++ b/README.md @@ -2,14 +2,19 @@ Shared design foundations for [protocol]works products. -The initial package, `@protocol-works/design-tokens`, is deliberately smaller than a component -library. It owns the brand palette, canonical light and dark theme values, typography, bundled -font dependencies, motion, geometry, and Tailwind CSS v4 aliases. Each product continues to own -its application-specific semantic roles, component APIs, and runtime theme persistence. +The repository publishes two layers: + +- `@protocol-works/design-tokens` owns the brand palette, canonical light and dark theme values, + typography, bundled fonts, motion, geometry, and Tailwind CSS v4 aliases. +- `@protocol-works/ui` owns portable React primitives established across [protocol]works products. + +Products continue to own runtime theme persistence, framework adapters, product-specific semantic +roles, and domain components. ## Status -`0.x` foundation. Published publicly as `@protocol-works/design-tokens`. +`0.x` foundation. `@protocol-works/design-tokens` is published publicly; `@protocol-works/ui` is +prepared independently under `packages/ui/`. ## Install @@ -50,6 +55,32 @@ preference. `border-rule`, `bg-accent-clay`, `font-sans`, `text-body`, `rounded-tight`, `duration-quick`, and `ease-edit`. +## React UI + +Install the token and UI packages together: + +```bash +npm install @protocol-works/design-tokens @protocol-works/ui +``` + +Register the compiled component sources after importing Tailwind and the token foundation: + +```css +@import "tailwindcss"; +@import "@protocol-works/design-tokens"; +@import "@protocol-works/ui/tailwind.css"; +``` + +Import components directly so consumers only load what they use: + +```tsx +import { Button } from "@protocol-works/ui/button"; +import { Stack } from "@protocol-works/ui/stack"; +import { Heading } from "@protocol-works/ui/heading"; +``` + +See [`packages/ui/README.md`](./packages/ui/README.md) for the complete component catalogue. + ## Fonts DM Sans, DM Mono, and DM Serif Display are delivered through Fontsource packages and are bundled @@ -66,4 +97,5 @@ MIT. See [`LICENSE`](./LICENSE). npm install npm run check npm pack --dry-run +npm pack --dry-run --workspace @protocol-works/ui ``` diff --git a/package-lock.json b/package-lock.json index bca95c4..e272fdb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,14 +8,23 @@ "name": "@protocol-works/design-tokens", "version": "0.1.0", "license": "MIT", + "workspaces": [ + "packages/*" + ], "dependencies": { "@fontsource-variable/dm-sans": "5.2.8", "@fontsource/dm-mono": "5.2.7", "@fontsource/dm-serif-display": "5.2.8" }, "devDependencies": { + "@tailwindcss/cli": "^4.2.2", + "@types/react": "19.2.15", + "@types/react-dom": "19.2.3", "prettier": "3.8.3", - "tailwindcss": "4.2.2" + "react": "19.2.4", + "react-dom": "19.2.4", + "tailwindcss": "4.2.2", + "typescript": "5.9.3" }, "engines": { "node": ">=20" @@ -48,6 +57,1101 @@ "url": "https://github.com/sponsors/ayuhito" } }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@parcel/watcher": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher/-/watcher-2.5.6.tgz", + "integrity": "sha512-tmmZ3lQxAe/k/+rNnXQRawJ4NjxO2hqiOLTHvWchtGZULp4RyFeh6aU4XdOYBFe2KE1oShQTv4AblOs2iOrNnQ==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "detect-libc": "^2.0.3", + "is-glob": "^4.0.3", + "node-addon-api": "^7.0.0", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "@parcel/watcher-android-arm64": "2.5.6", + "@parcel/watcher-darwin-arm64": "2.5.6", + "@parcel/watcher-darwin-x64": "2.5.6", + "@parcel/watcher-freebsd-x64": "2.5.6", + "@parcel/watcher-linux-arm-glibc": "2.5.6", + "@parcel/watcher-linux-arm-musl": "2.5.6", + "@parcel/watcher-linux-arm64-glibc": "2.5.6", + "@parcel/watcher-linux-arm64-musl": "2.5.6", + "@parcel/watcher-linux-x64-glibc": "2.5.6", + "@parcel/watcher-linux-x64-musl": "2.5.6", + "@parcel/watcher-win32-arm64": "2.5.6", + "@parcel/watcher-win32-ia32": "2.5.6", + "@parcel/watcher-win32-x64": "2.5.6" + } + }, + "node_modules/@parcel/watcher-android-arm64": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-android-arm64/-/watcher-android-arm64-2.5.6.tgz", + "integrity": "sha512-YQxSS34tPF/6ZG7r/Ih9xy+kP/WwediEUsqmtf0cuCV5TPPKw/PQHRhueUo6JdeFJaqV3pyjm0GdYjZotbRt/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-darwin-arm64": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-arm64/-/watcher-darwin-arm64-2.5.6.tgz", + "integrity": "sha512-Z2ZdrnwyXvvvdtRHLmM4knydIdU9adO3D4n/0cVipF3rRiwP+3/sfzpAwA/qKFL6i1ModaabkU7IbpeMBgiVEA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-darwin-x64": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-x64/-/watcher-darwin-x64-2.5.6.tgz", + "integrity": "sha512-HgvOf3W9dhithcwOWX9uDZyn1lW9R+7tPZ4sug+NGrGIo4Rk1hAXLEbcH1TQSqxts0NYXXlOWqVpvS1SFS4fRg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-freebsd-x64": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-freebsd-x64/-/watcher-freebsd-x64-2.5.6.tgz", + "integrity": "sha512-vJVi8yd/qzJxEKHkeemh7w3YAn6RJCtYlE4HPMoVnCpIXEzSrxErBW5SJBgKLbXU3WdIpkjBTeUNtyBVn8TRng==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm-glibc": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-glibc/-/watcher-linux-arm-glibc-2.5.6.tgz", + "integrity": "sha512-9JiYfB6h6BgV50CCfasfLf/uvOcJskMSwcdH1PHH9rvS1IrNy8zad6IUVPVUfmXr+u+Km9IxcfMLzgdOudz9EQ==", + "cpu": [ + "arm" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm-musl": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-musl/-/watcher-linux-arm-musl-2.5.6.tgz", + "integrity": "sha512-Ve3gUCG57nuUUSyjBq/MAM0CzArtuIOxsBdQ+ftz6ho8n7s1i9E1Nmk/xmP323r2YL0SONs1EuwqBp2u1k5fxg==", + "cpu": [ + "arm" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm64-glibc": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-glibc/-/watcher-linux-arm64-glibc-2.5.6.tgz", + "integrity": "sha512-f2g/DT3NhGPdBmMWYoxixqYr3v/UXcmLOYy16Bx0TM20Tchduwr4EaCbmxh1321TABqPGDpS8D/ggOTaljijOA==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm64-musl": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-musl/-/watcher-linux-arm64-musl-2.5.6.tgz", + "integrity": "sha512-qb6naMDGlbCwdhLj6hgoVKJl2odL34z2sqkC7Z6kzir8b5W65WYDpLB6R06KabvZdgoHI/zxke4b3zR0wAbDTA==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-x64-glibc": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-glibc/-/watcher-linux-x64-glibc-2.5.6.tgz", + "integrity": "sha512-kbT5wvNQlx7NaGjzPFu8nVIW1rWqV780O7ZtkjuWaPUgpv2NMFpjYERVi0UYj1msZNyCzGlaCWEtzc+exjMGbQ==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-x64-musl": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-musl/-/watcher-linux-x64-musl-2.5.6.tgz", + "integrity": "sha512-1JRFeC+h7RdXwldHzTsmdtYR/Ku8SylLgTU/reMuqdVD7CtLwf0VR1FqeprZ0eHQkO0vqsbvFLXUmYm/uNKJBg==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-win32-arm64": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-arm64/-/watcher-win32-arm64-2.5.6.tgz", + "integrity": "sha512-3ukyebjc6eGlw9yRt678DxVF7rjXatWiHvTXqphZLvo7aC5NdEgFufVwjFfY51ijYEWpXbqF5jtrK275z52D4Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-win32-ia32": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-ia32/-/watcher-win32-ia32-2.5.6.tgz", + "integrity": "sha512-k35yLp1ZMwwee3Ez/pxBi5cf4AoBKYXj00CZ80jUz5h8prpiaQsiRPKQMxoLstNuqe2vR4RNPEAEcjEFzhEz/g==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-win32-x64": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-x64/-/watcher-win32-x64-2.5.6.tgz", + "integrity": "sha512-hbQlYcCq5dlAX9Qx+kFb0FHue6vbjlf0FrNzSKdYK2APUf7tGfGxQCk2ihEREmbR6ZMc0MVAD5RIX/41gpUzTw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@protocol-works/design-tokens": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/@protocol-works/design-tokens/-/design-tokens-0.1.0.tgz", + "integrity": "sha512-qKsu1lcgCYU/36txByxw+wMvXJwfyRembtWgrA7P3V2L08tI0bSwLungn3DMK1kAn311H9u0fCaDx5YzXhnViQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "@fontsource-variable/dm-sans": "5.2.8", + "@fontsource/dm-mono": "5.2.7", + "@fontsource/dm-serif-display": "5.2.8" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/@protocol-works/ui": { + "resolved": "packages/ui", + "link": true + }, + "node_modules/@tailwindcss/cli": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/cli/-/cli-4.2.2.tgz", + "integrity": "sha512-iJS+8kAFZ8HPqnh0O5DHCLjo4L6dD97DBQEkrhfSO4V96xeefUus2jqsBs1dUMt3OU9Ks4qIkiY0mpL5UW+4LQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@parcel/watcher": "^2.5.1", + "@tailwindcss/node": "4.2.2", + "@tailwindcss/oxide": "4.2.2", + "enhanced-resolve": "^5.19.0", + "mri": "^1.2.0", + "picocolors": "^1.1.1", + "tailwindcss": "4.2.2" + }, + "bin": { + "tailwindcss": "dist/index.mjs" + } + }, + "node_modules/@tailwindcss/node": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.2.2.tgz", + "integrity": "sha512-pXS+wJ2gZpVXqFaUEjojq7jzMpTGf8rU6ipJz5ovJV6PUGmlJ+jvIwGrzdHdQ80Sg+wmQxUFuoW1UAAwHNEdFA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/remapping": "^2.3.5", + "enhanced-resolve": "^5.19.0", + "jiti": "^2.6.1", + "lightningcss": "1.32.0", + "magic-string": "^0.30.21", + "source-map-js": "^1.2.1", + "tailwindcss": "4.2.2" + } + }, + "node_modules/@tailwindcss/oxide": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.2.2.tgz", + "integrity": "sha512-qEUA07+E5kehxYp9BVMpq9E8vnJuBHfJEC0vPC5e7iL/hw7HR61aDKoVoKzrG+QKp56vhNZe4qwkRmMC0zDLvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 20" + }, + "optionalDependencies": { + "@tailwindcss/oxide-android-arm64": "4.2.2", + "@tailwindcss/oxide-darwin-arm64": "4.2.2", + "@tailwindcss/oxide-darwin-x64": "4.2.2", + "@tailwindcss/oxide-freebsd-x64": "4.2.2", + "@tailwindcss/oxide-linux-arm-gnueabihf": "4.2.2", + "@tailwindcss/oxide-linux-arm64-gnu": "4.2.2", + "@tailwindcss/oxide-linux-arm64-musl": "4.2.2", + "@tailwindcss/oxide-linux-x64-gnu": "4.2.2", + "@tailwindcss/oxide-linux-x64-musl": "4.2.2", + "@tailwindcss/oxide-wasm32-wasi": "4.2.2", + "@tailwindcss/oxide-win32-arm64-msvc": "4.2.2", + "@tailwindcss/oxide-win32-x64-msvc": "4.2.2" + } + }, + "node_modules/@tailwindcss/oxide-android-arm64": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.2.2.tgz", + "integrity": "sha512-dXGR1n+P3B6748jZO/SvHZq7qBOqqzQ+yFrXpoOWWALWndF9MoSKAT3Q0fYgAzYzGhxNYOoysRvYlpixRBBoDg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-darwin-arm64": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.2.2.tgz", + "integrity": "sha512-iq9Qjr6knfMpZHj55/37ouZeykwbDqF21gPFtfnhCCKGDcPI/21FKC9XdMO/XyBM7qKORx6UIhGgg6jLl7BZlg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-darwin-x64": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.2.2.tgz", + "integrity": "sha512-BlR+2c3nzc8f2G639LpL89YY4bdcIdUmiOOkv2GQv4/4M0vJlpXEa0JXNHhCHU7VWOKWT/CjqHdTP8aUuDJkuw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-freebsd-x64": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.2.2.tgz", + "integrity": "sha512-YUqUgrGMSu2CDO82hzlQ5qSb5xmx3RUrke/QgnoEx7KvmRJHQuZHZmZTLSuuHwFf0DJPybFMXMYf+WJdxHy/nQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.2.2.tgz", + "integrity": "sha512-FPdhvsW6g06T9BWT0qTwiVZYE2WIFo2dY5aCSpjG/S/u1tby+wXoslXS0kl3/KXnULlLr1E3NPRRw0g7t2kgaQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-gnu": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.2.2.tgz", + "integrity": "sha512-4og1V+ftEPXGttOO7eCmW7VICmzzJWgMx+QXAJRAhjrSjumCwWqMfkDrNu1LXEQzNAwz28NCUpucgQPrR4S2yw==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-musl": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.2.2.tgz", + "integrity": "sha512-oCfG/mS+/+XRlwNjnsNLVwnMWYH7tn/kYPsNPh+JSOMlnt93mYNCKHYzylRhI51X+TbR+ufNhhKKzm6QkqX8ag==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-gnu": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.2.2.tgz", + "integrity": "sha512-rTAGAkDgqbXHNp/xW0iugLVmX62wOp2PoE39BTCGKjv3Iocf6AFbRP/wZT/kuCxC9QBh9Pu8XPkv/zCZB2mcMg==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-musl": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.2.2.tgz", + "integrity": "sha512-XW3t3qwbIwiSyRCggeO2zxe3KWaEbM0/kW9e8+0XpBgyKU4ATYzcVSMKteZJ1iukJ3HgHBjbg9P5YPRCVUxlnQ==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-wasm32-wasi": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.2.2.tgz", + "integrity": "sha512-eKSztKsmEsn1O5lJ4ZAfyn41NfG7vzCg496YiGtMDV86jz1q/irhms5O0VrY6ZwTUkFy/EKG3RfWgxSI3VbZ8Q==", + "bundleDependencies": [ + "@napi-rs/wasm-runtime", + "@emnapi/core", + "@emnapi/runtime", + "@tybys/wasm-util", + "@emnapi/wasi-threads", + "tslib" + ], + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "^1.8.1", + "@emnapi/runtime": "^1.8.1", + "@emnapi/wasi-threads": "^1.1.0", + "@napi-rs/wasm-runtime": "^1.1.1", + "@tybys/wasm-util": "^0.10.1", + "tslib": "^2.8.1" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@tailwindcss/oxide-win32-arm64-msvc": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.2.2.tgz", + "integrity": "sha512-qPmaQM4iKu5mxpsrWZMOZRgZv1tOZpUm+zdhhQP0VhJfyGGO3aUKdbh3gDZc/dPLQwW4eSqWGrrcWNBZWUWaXQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-win32-x64-msvc": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.2.2.tgz", + "integrity": "sha512-1T/37VvI7WyH66b+vqHj/cLwnCxt7Qt3WFu5Q8hk65aOvlwAhs7rAp1VkulBJw/N4tMirXjVnylTR72uI0HGcA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@types/react": { + "version": "19.2.15", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.15.tgz", + "integrity": "sha512-eRwcGNHve+E8qtEQSSRl6urh+rFop4v8gm6O8rGv25CodbvFdLjA1vVQ1KkiFE0w0UPOnb8tDiFKL5lp0rtY5Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "csstype": "^3.2.2" + } + }, + "node_modules/@types/react-dom": { + "version": "19.2.3", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.3.tgz", + "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "^19.2.0" + } + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/enhanced-resolve": { + "version": "5.24.2", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.24.2.tgz", + "integrity": "sha512-rpsZEGT1jFuve6QlpyRp9ckQ+kN61hvF9BzCPyMdaKTm8UJce96KBn3sorXOFXlzjPrs3Vc4T1NsSroZ3PxlFw==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.4", + "tapable": "^2.3.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/jiti": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.7.0.tgz", + "integrity": "sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==", + "dev": true, + "license": "MIT", + "bin": { + "jiti": "lib/jiti-cli.mjs" + } + }, + "node_modules/lightningcss": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz", + "integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==", + "dev": true, + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "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" + } + }, + "node_modules/lightningcss-android-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.32.0.tgz", + "integrity": "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.32.0.tgz", + "integrity": "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.32.0.tgz", + "integrity": "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.32.0.tgz", + "integrity": "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.32.0.tgz", + "integrity": "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.32.0.tgz", + "integrity": "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.32.0.tgz", + "integrity": "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.32.0.tgz", + "integrity": "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.32.0.tgz", + "integrity": "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.32.0.tgz", + "integrity": "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.32.0.tgz", + "integrity": "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/mri": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/mri/-/mri-1.2.0.tgz", + "integrity": "sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/node-addon-api": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz", + "integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.5.tgz", + "integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, "node_modules/prettier": { "version": "3.8.3", "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.8.3.tgz", @@ -64,12 +1168,90 @@ "url": "https://github.com/prettier/prettier?sponsor=1" } }, + "node_modules/react": { + "version": "19.2.4", + "resolved": "https://registry.npmjs.org/react/-/react-19.2.4.tgz", + "integrity": "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "19.2.4", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.4.tgz", + "integrity": "sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ==", + "license": "MIT", + "dependencies": { + "scheduler": "^0.27.0" + }, + "peerDependencies": { + "react": "^19.2.4" + } + }, + "node_modules/scheduler": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", + "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==", + "license": "MIT" + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/tailwindcss": { "version": "4.2.2", "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.2.2.tgz", "integrity": "sha512-KWBIxs1Xb6NoLdMVqhbhgwZf2PGBpPEiwOqgI4pFIYbNTfBXiKYyWoTsXgBQ9WFg/OlhnvHaY+AEpW7wSmFo2Q==", "dev": true, "license": "MIT" + }, + "node_modules/tapable": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.3.tgz", + "integrity": "sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "packages/ui": { + "name": "@protocol-works/ui", + "version": "0.1.0", + "license": "MIT", + "engines": { + "node": ">=20" + }, + "peerDependencies": { + "@protocol-works/design-tokens": ">=0.1.0 <1", + "react": ">=19 <20", + "react-dom": ">=19 <20" + } } } } diff --git a/package.json b/package.json index 48409a5..55e8aff 100644 --- a/package.json +++ b/package.json @@ -3,6 +3,9 @@ "version": "0.1.0", "description": "Shared [protocol]works brand, theme, typography, motion, and geometry tokens.", "type": "module", + "workspaces": [ + "packages/*" + ], "style": "./index.css", "sideEffects": [ "**/*.css" @@ -25,8 +28,10 @@ "scripts": { "format": "prettier --write .", "format:check": "prettier --check .", - "test": "node --test", - "check": "npm run format:check && npm test" + "test:tokens": "node --test test/*.test.mjs", + "test": "npm run test:tokens && npm test --workspace @protocol-works/ui", + "build": "npm run build --workspace @protocol-works/ui", + "check": "npm run format:check && npm test && npm run build" }, "repository": { "type": "git", @@ -46,7 +51,13 @@ "@fontsource/dm-serif-display": "5.2.8" }, "devDependencies": { + "@tailwindcss/cli": "^4.2.2", + "@types/react": "19.2.15", + "@types/react-dom": "19.2.3", "prettier": "3.8.3", - "tailwindcss": "4.2.2" + "react": "19.2.4", + "react-dom": "19.2.4", + "tailwindcss": "4.2.2", + "typescript": "5.9.3" } } diff --git a/packages/ui/LICENSE b/packages/ui/LICENSE new file mode 100644 index 0000000..4cdc6e3 --- /dev/null +++ b/packages/ui/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 protocol.works + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/packages/ui/README.md b/packages/ui/README.md new file mode 100644 index 0000000..8a0a102 --- /dev/null +++ b/packages/ui/README.md @@ -0,0 +1,94 @@ +# `@protocol-works/ui` + +Portable React primitives for [protocol]works products. The component contracts originate in the +protocol.works website design-system specimens and consume only +`@protocol-works/design-tokens`. + +The package has no Next.js, Tauri, or product-runtime dependency. Framework adapters and domain +components stay in their consuming applications. + +## Install + +```bash +npm install @protocol-works/design-tokens @protocol-works/ui +``` + +Import Tailwind, the token foundation, and the UI package's source registration: + +```css +@import "tailwindcss"; +@import "@protocol-works/design-tokens"; +@import "@protocol-works/ui/tailwind.css"; +``` + +The UI stylesheet registers the package's compiled component files with Tailwind v4 and defines +the four nested surface recipes. + +## Direct imports + +Every component has a direct subpath export: + +```tsx +import { Button } from "@protocol-works/ui/button"; +import { Field } from "@protocol-works/ui/field"; +import { Input } from "@protocol-works/ui/input"; +import { Heading } from "@protocol-works/ui/heading"; +``` + +There is intentionally no root JavaScript barrel. + +## Catalogue + +### Actions and forms + +- `button` — primary, outline, and ghost actions across light, dark, accent, and signal surfaces +- `field` — accessible label, helper, required, and error wiring +- `input` +- `textarea` + +### Layout and surfaces + +- `card` +- `cluster` +- `divider` +- `grid` +- `section` +- `stack` +- `surface` + +### Content and feedback + +- `list` +- `note` +- `quote` +- `skip-link` +- `spinner` +- `status` +- `tag` +- `visually-hidden` + +### Typography + +- `body` +- `caption` +- `code` +- `display` +- `heading` +- `lead` +- `signal` + +## Product adapters + +Next.js navigation, optimized images, theme persistence, Tauri integration, and product-specific +visual recipes should wrap these primitives locally. For example, the website retains its +`next/link` adapter, while Rabbithole retains instrument-specific controls and backend toast +orchestration. + +## Development + +From the repository root: + +```bash +npm run check +npm pack --dry-run --workspace @protocol-works/ui +``` diff --git a/packages/ui/package.json b/packages/ui/package.json new file mode 100644 index 0000000..20d607c --- /dev/null +++ b/packages/ui/package.json @@ -0,0 +1,149 @@ +{ + "name": "@protocol-works/ui", + "version": "0.1.0", + "description": "Portable React primitives for [protocol]works products.", + "type": "module", + "sideEffects": [ + "./tailwind.css" + ], + "files": [ + "dist", + "LICENSE", + "README.md", + "tailwind.css" + ], + "exports": { + "./body": { + "types": "./dist/components/typography/Body.d.ts", + "import": "./dist/components/typography/Body.js" + }, + "./button": { + "types": "./dist/components/Button.d.ts", + "import": "./dist/components/Button.js" + }, + "./caption": { + "types": "./dist/components/typography/Caption.d.ts", + "import": "./dist/components/typography/Caption.js" + }, + "./card": { + "types": "./dist/components/Card.d.ts", + "import": "./dist/components/Card.js" + }, + "./cluster": { + "types": "./dist/components/Cluster.d.ts", + "import": "./dist/components/Cluster.js" + }, + "./code": { + "types": "./dist/components/typography/Code.d.ts", + "import": "./dist/components/typography/Code.js" + }, + "./display": { + "types": "./dist/components/typography/Display.d.ts", + "import": "./dist/components/typography/Display.js" + }, + "./divider": { + "types": "./dist/components/Divider.d.ts", + "import": "./dist/components/Divider.js" + }, + "./field": { + "types": "./dist/components/Field.d.ts", + "import": "./dist/components/Field.js" + }, + "./grid": { + "types": "./dist/components/Grid.d.ts", + "import": "./dist/components/Grid.js" + }, + "./heading": { + "types": "./dist/components/typography/Heading.d.ts", + "import": "./dist/components/typography/Heading.js" + }, + "./input": { + "types": "./dist/components/Input.d.ts", + "import": "./dist/components/Input.js" + }, + "./lead": { + "types": "./dist/components/typography/Lead.d.ts", + "import": "./dist/components/typography/Lead.js" + }, + "./list": { + "types": "./dist/components/List.d.ts", + "import": "./dist/components/List.js" + }, + "./note": { + "types": "./dist/components/Note.d.ts", + "import": "./dist/components/Note.js" + }, + "./quote": { + "types": "./dist/components/Quote.d.ts", + "import": "./dist/components/Quote.js" + }, + "./section": { + "types": "./dist/components/Section.d.ts", + "import": "./dist/components/Section.js" + }, + "./signal": { + "types": "./dist/components/typography/Signal.d.ts", + "import": "./dist/components/typography/Signal.js" + }, + "./skip-link": { + "types": "./dist/components/SkipLink.d.ts", + "import": "./dist/components/SkipLink.js" + }, + "./spinner": { + "types": "./dist/components/Spinner.d.ts", + "import": "./dist/components/Spinner.js" + }, + "./stack": { + "types": "./dist/components/Stack.d.ts", + "import": "./dist/components/Stack.js" + }, + "./status": { + "types": "./dist/components/Status.d.ts", + "import": "./dist/components/Status.js" + }, + "./surface": { + "types": "./dist/components/Surface.d.ts", + "import": "./dist/components/Surface.js" + }, + "./tag": { + "types": "./dist/components/Tag.d.ts", + "import": "./dist/components/Tag.js" + }, + "./textarea": { + "types": "./dist/components/Textarea.d.ts", + "import": "./dist/components/Textarea.js" + }, + "./visually-hidden": { + "types": "./dist/components/VisuallyHidden.d.ts", + "import": "./dist/components/VisuallyHidden.js" + }, + "./tailwind.css": "./tailwind.css", + "./package.json": "./package.json" + }, + "scripts": { + "build": "tsc -p tsconfig.json", + "test": "npm run build && node --test test/*.test.mjs", + "prepack": "npm run test" + }, + "peerDependencies": { + "@protocol-works/design-tokens": ">=0.1.0 <1", + "react": ">=19 <20", + "react-dom": ">=19 <20" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/protocol-works/design-system.git", + "directory": "packages/ui" + }, + "bugs": { + "url": "https://github.com/protocol-works/design-system/issues" + }, + "homepage": "https://github.com/protocol-works/design-system/tree/main/packages/ui#readme", + "license": "MIT", + "publishConfig": { + "access": "public" + }, + "engines": { + "node": ">=20" + } +} diff --git a/packages/ui/src/components/Button.tsx b/packages/ui/src/components/Button.tsx new file mode 100644 index 0000000..7cc95cc --- /dev/null +++ b/packages/ui/src/components/Button.tsx @@ -0,0 +1,84 @@ +import type { ElementType, ReactNode } from "react"; +import { cn } from "../lib/cn.js"; +import type { PolymorphicProps } from "../lib/polymorphic.js"; +import type { Surface } from "../lib/surface.js"; + +export type ButtonVariant = "primary" | "outline" | "ghost"; + +export type ButtonProps = { + variant?: ButtonVariant; + surface?: Surface; + className?: string; + children: ReactNode; +}; + +const variantClass: Record> = { + light: { + primary: + "bg-foreground text-background hover:bg-foreground/85 active:bg-foreground/95", + outline: + "border border-foreground bg-transparent text-foreground hover:bg-foreground/5 active:bg-foreground/10", + ghost: + "bg-transparent text-foreground hover:bg-foreground/5 active:bg-foreground/10", + }, + dark: { + primary: + "bg-[var(--pw-surface-dark-foreground)] text-[var(--pw-surface-dark-background)] hover:bg-[var(--pw-surface-dark-foreground)]/85 active:bg-[var(--pw-surface-dark-foreground)]/95", + outline: + "border border-[var(--pw-surface-dark-foreground)] bg-transparent text-[var(--pw-surface-dark-foreground)] hover:bg-[color-mix(in_srgb,var(--pw-surface-dark-foreground)_10%,transparent)] active:bg-[color-mix(in_srgb,var(--pw-surface-dark-foreground)_15%,transparent)]", + ghost: + "bg-transparent text-[var(--pw-surface-dark-foreground)] hover:bg-[color-mix(in_srgb,var(--pw-surface-dark-foreground)_10%,transparent)] active:bg-[color-mix(in_srgb,var(--pw-surface-dark-foreground)_15%,transparent)]", + }, + accent: { + primary: + "bg-[var(--pw-surface-accent-foreground)] text-[var(--pw-surface-accent-background)] hover:bg-[var(--pw-surface-accent-foreground)]/90 active:bg-[var(--pw-surface-accent-foreground)]/95", + outline: + "border border-[var(--pw-surface-accent-foreground)] bg-transparent text-[var(--pw-surface-accent-foreground)] hover:bg-[color-mix(in_srgb,var(--pw-surface-accent-foreground)_10%,transparent)] active:bg-[color-mix(in_srgb,var(--pw-surface-accent-foreground)_15%,transparent)]", + ghost: + "bg-transparent text-[var(--pw-surface-accent-foreground)] hover:bg-[color-mix(in_srgb,var(--pw-surface-accent-foreground)_10%,transparent)] active:bg-[color-mix(in_srgb,var(--pw-surface-accent-foreground)_15%,transparent)]", + }, + signal: { + primary: + "bg-[var(--pw-surface-signal-foreground)] text-[var(--pw-surface-signal-background)] hover:bg-[var(--pw-surface-signal-foreground)]/85 active:bg-[var(--pw-surface-signal-foreground)]/95", + outline: + "border border-[var(--pw-surface-signal-foreground)] bg-transparent text-[var(--pw-surface-signal-foreground)] hover:bg-[var(--pw-surface-signal-foreground)]/10 active:bg-[var(--pw-surface-signal-foreground)]/15", + ghost: + "bg-transparent text-[var(--pw-surface-signal-foreground)] hover:bg-[var(--pw-surface-signal-foreground)]/10 active:bg-[var(--pw-surface-signal-foreground)]/15", + }, +}; + +export function Button({ + as, + variant = "outline", + surface = "light", + className, + children, + ...rest +}: PolymorphicProps) { + const Tag = (as ?? "button") as ElementType; + const tagSpecificProps = + Tag === "button" + ? { type: (rest as { type?: string }).type ?? "button" } + : {}; + + return ( + + {children} + + ); +} diff --git a/packages/ui/src/components/Card.tsx b/packages/ui/src/components/Card.tsx new file mode 100644 index 0000000..994f3bd --- /dev/null +++ b/packages/ui/src/components/Card.tsx @@ -0,0 +1,36 @@ +import type { ElementType, ReactNode } from "react"; +import { cn } from "../lib/cn.js"; +import type { PolymorphicProps } from "../lib/polymorphic.js"; + +export type CardVariant = "flat" | "raised"; + +export type CardProps = { + variant?: CardVariant; + className?: string; + children: ReactNode; +}; + +const variantClass: Record = { + flat: "border border-foreground/10 bg-background", + raised: "border border-foreground/10 bg-background shadow-edit", +}; + +export function Card({ + variant = "flat", + as, + className, + children, + style, + ...rest +}: PolymorphicProps) { + const Tag = (as ?? "div") as ElementType; + return ( + + {children} + + ); +} diff --git a/packages/ui/src/components/Cluster.tsx b/packages/ui/src/components/Cluster.tsx new file mode 100644 index 0000000..ae2bf44 --- /dev/null +++ b/packages/ui/src/components/Cluster.tsx @@ -0,0 +1,65 @@ +import type { CSSProperties, ElementType, ReactNode } from "react"; +import { cn } from "../lib/cn.js"; +import type { PolymorphicProps } from "../lib/polymorphic.js"; + +export type ClusterGap = "tight" | "default" | "loose"; +export type ClusterAlign = "start" | "center" | "end" | "baseline"; +export type ClusterJustify = "start" | "center" | "end" | "between"; + +export type ClusterProps = { + gap?: ClusterGap; + align?: ClusterAlign; + justify?: ClusterJustify; + wrap?: boolean; + className?: string; + children: ReactNode; +}; + +const gapValue: Record = { + tight: "var(--pw-space-inline)", + default: "1rem", + loose: "var(--pw-space-block)", +}; + +const alignClass: Record = { + start: "items-start", + center: "items-center", + end: "items-end", + baseline: "items-baseline", +}; + +const justifyClass: Record = { + start: "justify-start", + center: "justify-center", + end: "justify-end", + between: "justify-between", +}; + +export function Cluster({ + gap = "default", + align = "center", + justify, + wrap = true, + as, + className, + children, + style, + ...rest +}: PolymorphicProps & { style?: CSSProperties }) { + const Tag = (as ?? "div") as ElementType; + return ( + + {children} + + ); +} diff --git a/packages/ui/src/components/Divider.tsx b/packages/ui/src/components/Divider.tsx new file mode 100644 index 0000000..790402e --- /dev/null +++ b/packages/ui/src/components/Divider.tsx @@ -0,0 +1,38 @@ +import type { HTMLAttributes } from "react"; +import { cn } from "../lib/cn.js"; + +export type DividerProps = { + orientation?: "horizontal" | "vertical"; + weight?: "hairline" | "thick"; +} & HTMLAttributes; + +export function Divider({ + orientation = "horizontal", + weight = "hairline", + className, + style, + ...rest +}: DividerProps) { + const vertical = orientation === "vertical"; + const sizeProperty = vertical ? "borderLeftWidth" : "borderTopWidth"; + + return ( +
+ ); +} diff --git a/packages/ui/src/components/Field.tsx b/packages/ui/src/components/Field.tsx new file mode 100644 index 0000000..2cb4d59 --- /dev/null +++ b/packages/ui/src/components/Field.tsx @@ -0,0 +1,80 @@ +import { + cloneElement, + isValidElement, + useId, + type ReactElement, + type ReactNode, +} from "react"; +import { cn } from "../lib/cn.js"; + +export type FieldControlProps = { + id?: string; + "aria-describedby"?: string; + invalid?: boolean; +}; + +export type FieldProps = { + label: ReactNode; + helper?: ReactNode; + error?: ReactNode; + required?: boolean; + className?: string; + children: ReactElement; +}; + +export function Field({ + label, + helper, + error, + required = false, + className, + children, +}: FieldProps) { + const reactId = useId(); + const inputId = children.props.id ?? `${reactId}-input`; + const helperId = helper ? `${reactId}-helper` : undefined; + const errorId = error ? `${reactId}-error` : undefined; + const describedBy = + [errorId, helperId].filter(Boolean).join(" ") || undefined; + + const enhancedChild = isValidElement(children) + ? cloneElement(children, { + id: inputId, + "aria-describedby": describedBy, + invalid: Boolean(error) || children.props.invalid, + }) + : children; + + return ( +
+ + {enhancedChild} + {helper && !error ? ( +

+ {helper} +

+ ) : null} + {error ? ( +

+ {error} +

+ ) : null} +
+ ); +} diff --git a/packages/ui/src/components/Grid.tsx b/packages/ui/src/components/Grid.tsx new file mode 100644 index 0000000..a3f0ca4 --- /dev/null +++ b/packages/ui/src/components/Grid.tsx @@ -0,0 +1,45 @@ +import type { CSSProperties, ElementType, ReactNode } from "react"; +import { cn } from "../lib/cn.js"; +import type { PolymorphicProps } from "../lib/polymorphic.js"; + +export type GridGap = "tight" | "default" | "loose"; + +export type GridProps = { + minItemWidth?: string; + cols?: number; + gap?: GridGap; + className?: string; + children: ReactNode; +}; + +const gapValue: Record = { + tight: "var(--pw-space-inline)", + default: "1rem", + loose: "var(--pw-space-block)", +}; + +export function Grid({ + minItemWidth = "16rem", + cols, + gap = "default", + as, + className, + children, + style, + ...rest +}: PolymorphicProps & { style?: CSSProperties }) { + const Tag = (as ?? "div") as ElementType; + const gridTemplateColumns = cols + ? `repeat(${cols}, minmax(0, 1fr))` + : `repeat(auto-fit, minmax(${minItemWidth}, 1fr))`; + + return ( + + {children} + + ); +} diff --git a/packages/ui/src/components/Input.tsx b/packages/ui/src/components/Input.tsx new file mode 100644 index 0000000..1df07ae --- /dev/null +++ b/packages/ui/src/components/Input.tsx @@ -0,0 +1,35 @@ +import type { ComponentPropsWithRef } from "react"; +import { cn } from "../lib/cn.js"; + +export type InputProps = { + invalid?: boolean; + className?: string; +} & Omit, "className">; + +export function Input({ + invalid = false, + className, + type = "text", + ...rest +}: InputProps) { + return ( + + ); +} diff --git a/packages/ui/src/components/List.tsx b/packages/ui/src/components/List.tsx new file mode 100644 index 0000000..9420e7d --- /dev/null +++ b/packages/ui/src/components/List.tsx @@ -0,0 +1,51 @@ +import type { ElementType, HTMLAttributes, ReactNode } from "react"; +import { cn } from "../lib/cn.js"; + +export type ListVariant = "unordered" | "ordered"; +export type ListMarker = "default" | "none"; + +export type ListProps = { + variant?: ListVariant; + marker?: ListMarker; + className?: string; + children: ReactNode; +} & Omit, "className">; + +export function List({ + variant = "unordered", + marker = "default", + className, + children, + ...rest +}: ListProps) { + const Tag: ElementType = variant === "ordered" ? "ol" : "ul"; + const markerClass = + marker === "none" + ? "list-none" + : variant === "ordered" + ? "list-decimal marker:text-foreground-muted" + : "list-disc marker:text-foreground-muted"; + + return ( + + {children} + + ); +} + +export type ListItemProps = HTMLAttributes; + +export function ListItem({ className, children, ...rest }: ListItemProps) { + return ( +
  • + {children} +
  • + ); +} diff --git a/packages/ui/src/components/Note.tsx b/packages/ui/src/components/Note.tsx new file mode 100644 index 0000000..b44818f --- /dev/null +++ b/packages/ui/src/components/Note.tsx @@ -0,0 +1,42 @@ +import type { ReactNode } from "react"; +import { cn } from "../lib/cn.js"; + +export type NoteTone = "info" | "success" | "warning" | "error"; + +export type NoteProps = { + tone?: NoteTone; + title?: ReactNode; + className?: string; + children: ReactNode; +}; + +const toneVariable: Record = { + info: "var(--pw-color-info)", + success: "var(--pw-color-success)", + warning: "var(--pw-color-warning)", + error: "var(--pw-color-error)", +}; + +export function Note({ tone = "info", title, className, children }: NoteProps) { + const color = toneVariable[tone]; + return ( + + ); +} diff --git a/packages/ui/src/components/Quote.tsx b/packages/ui/src/components/Quote.tsx new file mode 100644 index 0000000..95e04e5 --- /dev/null +++ b/packages/ui/src/components/Quote.tsx @@ -0,0 +1,33 @@ +import type { HTMLAttributes, ReactNode } from "react"; +import { cn } from "../lib/cn.js"; + +export type QuoteProps = { + attribution?: ReactNode; + children: ReactNode; +} & Omit, "children">; + +export function Quote({ + attribution, + className, + children, + style, + ...rest +}: QuoteProps) { + return ( +
    +
    {children}
    + {attribution ? ( +
    + {attribution} +
    + ) : null} +
    + ); +} diff --git a/packages/ui/src/components/Section.tsx b/packages/ui/src/components/Section.tsx new file mode 100644 index 0000000..6078a8a --- /dev/null +++ b/packages/ui/src/components/Section.tsx @@ -0,0 +1,51 @@ +import type { ElementType, ReactNode } from "react"; +import { cn } from "../lib/cn.js"; +import type { PolymorphicProps } from "../lib/polymorphic.js"; + +export type SectionWidth = "narrow" | "prose" | "wide" | "content" | "page"; +export type SectionSpacing = "tight" | "default" | "loose"; + +export type SectionProps = { + width?: SectionWidth; + spacing?: SectionSpacing; + className?: string; + children: ReactNode; +}; + +const widthClass: Record = { + narrow: "max-w-[var(--pw-width-narrow)]", + prose: "max-w-[var(--pw-width-prose)]", + wide: "max-w-[var(--pw-width-wide)]", + content: "max-w-[var(--pw-width-content)]", + page: "max-w-[var(--pw-width-page)]", +}; + +const spacingClass: Record = { + tight: "py-[var(--pw-space-block)]", + default: "py-[var(--pw-space-section)]", + loose: "py-[calc(var(--pw-space-section)*1.5)]", +}; + +export function Section({ + width = "content", + spacing = "default", + as, + className, + children, + ...rest +}: PolymorphicProps) { + const Tag = (as ?? "section") as ElementType; + return ( + + {children} + + ); +} diff --git a/packages/ui/src/components/SkipLink.tsx b/packages/ui/src/components/SkipLink.tsx new file mode 100644 index 0000000..ee08938 --- /dev/null +++ b/packages/ui/src/components/SkipLink.tsx @@ -0,0 +1,29 @@ +import type { AnchorHTMLAttributes, ReactNode } from "react"; +import { cn } from "../lib/cn.js"; + +export type SkipLinkProps = { + children?: ReactNode; +} & AnchorHTMLAttributes; + +export function SkipLink({ + href = "#main", + className, + children = "Skip to main content", + ...rest +}: SkipLinkProps) { + return ( + + {children} + + ); +} diff --git a/packages/ui/src/components/Spinner.tsx b/packages/ui/src/components/Spinner.tsx new file mode 100644 index 0000000..f6a155c --- /dev/null +++ b/packages/ui/src/components/Spinner.tsx @@ -0,0 +1,40 @@ +import type { HTMLAttributes } from "react"; +import { cn } from "../lib/cn.js"; +import { VisuallyHidden } from "./VisuallyHidden.js"; + +export type SpinnerSize = "sm" | "md" | "lg"; + +export type SpinnerProps = { + size?: SpinnerSize; + label?: string; +} & HTMLAttributes; + +const sizeVariable: Record = { + sm: "var(--pw-icon-sm)", + md: "var(--pw-icon-md)", + lg: "var(--pw-icon-lg)", +}; + +export function Spinner({ + size = "md", + label = "Loading", + className, + ...rest +}: SpinnerProps) { + const dimension = sizeVariable[size]; + return ( + + + ); +} diff --git a/packages/ui/src/components/Stack.tsx b/packages/ui/src/components/Stack.tsx new file mode 100644 index 0000000..7f56207 --- /dev/null +++ b/packages/ui/src/components/Stack.tsx @@ -0,0 +1,47 @@ +import type { CSSProperties, ElementType, ReactNode } from "react"; +import { cn } from "../lib/cn.js"; +import type { PolymorphicProps } from "../lib/polymorphic.js"; + +export type StackGap = "tight" | "default" | "loose"; +export type StackAlign = "start" | "center" | "end" | "stretch"; + +export type StackProps = { + gap?: StackGap; + align?: StackAlign; + className?: string; + children: ReactNode; +}; + +const gapValue: Record = { + tight: "var(--pw-space-inline)", + default: "1rem", + loose: "var(--pw-space-block)", +}; + +const alignClass: Record = { + start: "items-start", + center: "items-center", + end: "items-end", + stretch: "items-stretch", +}; + +export function Stack({ + gap = "default", + align, + as, + className, + children, + style, + ...rest +}: PolymorphicProps & { style?: CSSProperties }) { + const Tag = (as ?? "div") as ElementType; + return ( + + {children} + + ); +} diff --git a/packages/ui/src/components/Status.tsx b/packages/ui/src/components/Status.tsx new file mode 100644 index 0000000..1320b7c --- /dev/null +++ b/packages/ui/src/components/Status.tsx @@ -0,0 +1,47 @@ +import type { HTMLAttributes, ReactNode } from "react"; +import { cn } from "../lib/cn.js"; + +export type StatusTone = "success" | "warning" | "error" | "info" | "muted"; + +export type StatusProps = { + tone?: StatusTone; + pulse?: boolean; + children: ReactNode; +} & Omit, "children">; + +const toneColor: Record = { + success: "var(--pw-color-success)", + warning: "var(--pw-color-warning)", + error: "var(--pw-color-error)", + info: "var(--pw-color-accent)", + muted: "var(--pw-color-foreground-muted)", +}; + +export function Status({ + tone = "info", + pulse = false, + className, + children, + ...rest +}: StatusProps) { + const color = toneColor[tone]; + return ( + + + ); +} diff --git a/packages/ui/src/components/Surface.tsx b/packages/ui/src/components/Surface.tsx new file mode 100644 index 0000000..8a8e158 --- /dev/null +++ b/packages/ui/src/components/Surface.tsx @@ -0,0 +1,32 @@ +import type { ElementType, ReactNode } from "react"; +import { cn } from "../lib/cn.js"; +import type { PolymorphicProps } from "../lib/polymorphic.js"; +import type { Surface as SurfaceVariant } from "../lib/surface.js"; + +export type SurfaceProps = { + variant?: SurfaceVariant; + className?: string; + children: ReactNode; +}; + +const surfaceClass: Record = { + light: "pw-surface-light", + dark: "pw-surface-dark", + accent: "pw-surface-accent", + signal: "pw-surface-signal", +}; + +export function Surface({ + variant = "light", + as, + className, + children, + ...rest +}: PolymorphicProps) { + const Tag = (as ?? "div") as ElementType; + return ( + + {children} + + ); +} diff --git a/packages/ui/src/components/Tag.tsx b/packages/ui/src/components/Tag.tsx new file mode 100644 index 0000000..03bb76d --- /dev/null +++ b/packages/ui/src/components/Tag.tsx @@ -0,0 +1,79 @@ +import type { HTMLAttributes, ReactNode } from "react"; +import { cn } from "../lib/cn.js"; + +export type TagTone = + | "neutral" + | "accent" + | "success" + | "warning" + | "error" + | "muted"; + +export type TagProps = { + tone?: TagTone; + children: ReactNode; +} & Omit, "children">; + +const toneStyle: Record< + TagTone, + { background: string; color: string; border: string } +> = { + neutral: { + background: + "color-mix(in srgb, var(--pw-color-foreground) 6%, transparent)", + color: "var(--pw-color-foreground)", + border: "color-mix(in srgb, var(--pw-color-foreground) 15%, transparent)", + }, + accent: { + background: "color-mix(in srgb, var(--pw-color-accent) 8%, transparent)", + color: "var(--pw-color-accent)", + border: "color-mix(in srgb, var(--pw-color-accent) 25%, transparent)", + }, + success: { + background: "color-mix(in srgb, var(--pw-color-success) 8%, transparent)", + color: "var(--pw-color-success)", + border: "color-mix(in srgb, var(--pw-color-success) 25%, transparent)", + }, + warning: { + background: "color-mix(in srgb, var(--pw-color-warning) 8%, transparent)", + color: "var(--pw-color-warning)", + border: "color-mix(in srgb, var(--pw-color-warning) 25%, transparent)", + }, + error: { + background: "color-mix(in srgb, var(--pw-color-error) 8%, transparent)", + color: "var(--pw-color-error)", + border: "color-mix(in srgb, var(--pw-color-error) 25%, transparent)", + }, + muted: { + background: "transparent", + color: "var(--pw-color-foreground-muted)", + border: "var(--pw-color-rule)", + }, +}; + +export function Tag({ + tone = "neutral", + className, + children, + style, + ...rest +}: TagProps) { + const toneValues = toneStyle[tone]; + return ( + + {children} + + ); +} diff --git a/packages/ui/src/components/Textarea.tsx b/packages/ui/src/components/Textarea.tsx new file mode 100644 index 0000000..85f8dfd --- /dev/null +++ b/packages/ui/src/components/Textarea.tsx @@ -0,0 +1,34 @@ +import type { ComponentPropsWithRef } from "react"; +import { cn } from "../lib/cn.js"; + +export type TextareaProps = { + invalid?: boolean; + className?: string; +} & Omit, "className">; + +export function Textarea({ + invalid = false, + className, + rows = 4, + ...rest +}: TextareaProps) { + return ( +