From 8e525d19bf6272b64dc92ee509199557ac0e8125 Mon Sep 17 00:00:00 2001 From: Antoine Toussaint <312536060+antoinetoussaint-byte@users.noreply.github.com> Date: Sat, 3 Oct 2026 19:18:54 -0400 Subject: [PATCH] feat(ui): status tones, chips, lists and a preview stylesheet, so a solution page is built from the kit (Part of #1001) A consuming solution measured where it reached past @codefly-dev/ui: about a third of one 332-line page was hand-laid markup standing in for missing kit pieces, and the page could not be rendered at all without a running host. This delivers items 1-7 of #1001; items 8 and 9 (Markdown source offsets and link resolution, Board) are #1010. Status tones. `success`, `warning` and `info` join `destructive` as appearance tokens (contract 2.4.0, defaults for both modes, TOKENS.md), each one colour read as text and as a tint of itself, the shape `destructive` already had. `StatusTone` (neutral | success | warning | danger | info) is shared by `Badge tone`, `Chip tone` and `Banner tone`. A tone replaces a variant's colours rather than stacking on them. `Badge` also takes `size` (sm | default | lg) and keeps #998's decorative `dot`, now sized with the badge. New components: `Chip` / `ChipGroup` (link, button or router link; leading icon, trailing meta, a remove control beside the action, never inside it; no overflow clipping, so the focus ring is never cropped), `List` / `ListItem` and `DescriptionList`. `EmptyState reference` is a pointer elsewhere on the page, distinct from the explanation and from the action. Each gets its own type slots, so a skin can restyle them independently. The token layer moves into the kit. The `@theme` mapping, the light/dark binding and the shadcn custom variants lived in the host's globals.css, so a class a kit component wrote meant nothing outside the host. They are now `@codefly-dev/ui/theme.css`, which globals.css imports; the host's compiled stylesheet is byte-identical apart from the new tokens. The projection of a skin onto custom properties moves with it to `@codefly-dev/ui/skin`, and src/lib/appearance.ts re-exports it. `@codefly-dev/ui/preview.css` is the kit's source compiled with that layer and the default skin. It is committed and checked for staleness rather than built at publish: its bytes depend on the contract's defaults and on the Tailwind toolchain in the lockfile, neither inside the kit's folder, so the kit-version gate could not see them move and the publish would refuse the rebuilt bytes under an unchanged version. It is for a solution's local preview, stories and screenshots only: never loaded by a host or bundled into a remote, not evidence of a deployment's look, and it styles kit components only. A host test renders every kit story and fails if any class one of them carries is not defined there, matched as a whole class. The kit eats its own components: DatasourcesPanel and DeclaredSourceCard read their statuses from tones (paused is a warning in both, not destructive), the panel's hand-laid error card becomes a danger Banner, and the card's declaration and ambiguous-match list become DescriptionList and List. Kit 0.11.5 -> 0.12.0 (ui and saas-ui co-versioned; saas-ui now needs ^0.12.0); @codefly/saas-plugin-contract 2.3.0 -> 2.4.0 with every exact pin. Co-Authored-By: Claude Opus 5.5 (1M context) --- .../contracts/reference/frontend/package.json | 2 +- module/services/frontend/AGENTS.md | 13 + .../code/frontend-plugin-public-api.json | 2 +- .../services/frontend/code/package-lock.json | 30 +- module/services/frontend/code/package.json | 6 +- .../code/packages/codefly-ui/CATALOG.md | 8 +- .../code/packages/codefly-ui/README.md | 132 +- .../code/packages/codefly-ui/TOKENS.md | 27 +- .../code/packages/codefly-ui/package.json | 15 +- .../scripts/generate-preview-stylesheet.mjs | 108 + .../__tests__/no-raw-type-primitives.test.ts | 2 +- .../codefly-ui/src/__tests__/theme.test.ts | 55 + .../src/layout/__tests__/card.test.tsx | 32 + .../src/layout/__tests__/chip.test.tsx | 147 + .../src/layout/__tests__/list.test.tsx | 131 + .../src/layout/__tests__/states.test.tsx | 30 + .../src/layout/__tests__/status.test.tsx | 180 + .../packages/codefly-ui/src/layout/badge.tsx | 64 +- .../packages/codefly-ui/src/layout/banner.tsx | 76 +- .../packages/codefly-ui/src/layout/chip.tsx | 156 + .../codefly-ui/src/layout/empty-state.tsx | 38 +- .../packages/codefly-ui/src/layout/index.ts | 17 +- .../packages/codefly-ui/src/layout/list.tsx | 168 + .../codefly-ui/src/skin/appearance.ts | 130 + .../packages/codefly-ui/src/skin/index.ts | 5 + .../codefly-ui/src/skin/preview.generated.css | 6241 +++++++++++++++++ .../packages/codefly-ui/src/skin/theme.css | 355 + .../src/skin/type-slots.generated.css | 58 + .../codefly-ui/stories/primitives.stories.tsx | 173 + .../saas-plugin-contract/package.json | 2 +- .../saas-plugin-contract/src/appearance.ts | 8 + .../saas-plugin-contract/src/contracts.ts | 8 + .../saas-plugin-contract/src/typography.ts | 18 + .../saas-plugin-manifest/package.json | 2 +- .../packages/saas-plugin-react/package.json | 4 +- .../code/packages/saas-ui/package.json | 6 +- .../__tests__/datasources-panel.test.tsx | 22 + .../__tests__/declared-source-card.test.tsx | 41 +- .../src/datasources/datasources-panel.tsx | 56 +- .../src/datasources/declared-source-card.tsx | 83 +- .../code/scripts/test-published-ui.mjs | 20 + .../frontend/code/src/app/globals.css | 230 +- .../services/frontend/code/src/app/shadcn.css | 112 - .../code/src/lib/__tests__/appearance.test.ts | 45 +- .../src/lib/__tests__/design-tokens.test.ts | 2 +- .../lib/__tests__/preview-stylesheet.test.tsx | 170 + .../frontend/code/src/lib/appearance.ts | 130 +- .../services/frontend/code/ui-inventory.json | 60 + 48 files changed, 8746 insertions(+), 674 deletions(-) create mode 100644 module/services/frontend/code/packages/codefly-ui/scripts/generate-preview-stylesheet.mjs create mode 100644 module/services/frontend/code/packages/codefly-ui/src/__tests__/theme.test.ts create mode 100644 module/services/frontend/code/packages/codefly-ui/src/layout/__tests__/chip.test.tsx create mode 100644 module/services/frontend/code/packages/codefly-ui/src/layout/__tests__/list.test.tsx create mode 100644 module/services/frontend/code/packages/codefly-ui/src/layout/__tests__/status.test.tsx create mode 100644 module/services/frontend/code/packages/codefly-ui/src/layout/chip.tsx create mode 100644 module/services/frontend/code/packages/codefly-ui/src/layout/list.tsx create mode 100644 module/services/frontend/code/packages/codefly-ui/src/skin/appearance.ts create mode 100644 module/services/frontend/code/packages/codefly-ui/src/skin/preview.generated.css create mode 100644 module/services/frontend/code/packages/codefly-ui/src/skin/theme.css delete mode 100644 module/services/frontend/code/src/app/shadcn.css create mode 100644 module/services/frontend/code/src/lib/__tests__/preview-stylesheet.test.tsx diff --git a/module/contracts/reference/frontend/package.json b/module/contracts/reference/frontend/package.json index 2bc5a4086..e2786c658 100644 --- a/module/contracts/reference/frontend/package.json +++ b/module/contracts/reference/frontend/package.json @@ -18,7 +18,7 @@ "build": "tsc -p tsconfig.json" }, "dependencies": { - "@codefly/saas-plugin-contract": "2.3.0", + "@codefly/saas-plugin-contract": "2.4.0", "@codefly/saas-plugin-react": "0.4.1" }, "peerDependencies": { diff --git a/module/services/frontend/AGENTS.md b/module/services/frontend/AGENTS.md index 3dabec42a..ad8dede4a 100644 --- a/module/services/frontend/AGENTS.md +++ b/module/services/frontend/AGENTS.md @@ -116,6 +116,19 @@ release. The package's `exports` map may not name a subpath into the generated tree: a consumer imports the SDK's own surface, never a stub path (`publish-frontend-kit.test.mjs` enforces it). See the `cut-a-release` skill. +`@codefly-dev/ui` and `@codefly-dev/saas-ui` publish beside it. The kit owns its +token layer (`packages/codefly-ui/src/skin/theme.css`: token → utility, the +light/dark binding, the custom variants); `src/app/globals.css` only imports it, +so a new appearance token is bound there, not here. The kit also ships +`src/skin/preview.generated.css` (`@codefly-dev/ui/preview.css`) — the kit +compiled with the default skin, so a solution can preview its pages with no +host. It is committed and checked for staleness, because its bytes depend on the +contract and the lockfile, which the kit-version gate does not see: after +touching either, run `npm run generate:preview-stylesheet --workspace +@codefly-dev/ui`. It is for previews only and never loaded by the +host or a remote; what it may and may not be used for is in +`packages/codefly-ui/README.md` § "Previewing a solution without a host". + ## Two rules every admin surface owes its reader Both are product rules the owner has raised repeatedly, and both are kept by a diff --git a/module/services/frontend/code/frontend-plugin-public-api.json b/module/services/frontend/code/frontend-plugin-public-api.json index 8e994e90b..2f7bcd960 100644 --- a/module/services/frontend/code/frontend-plugin-public-api.json +++ b/module/services/frontend/code/frontend-plugin-public-api.json @@ -4,7 +4,7 @@ "packages": { "@codefly/saas-plugin-contract": { "status": "active", - "packageVersion": "2.3.0", + "packageVersion": "2.4.0", "entrypoints": [".", "./capabilities"], "entrypointExports": { ".": [ diff --git a/module/services/frontend/code/package-lock.json b/module/services/frontend/code/package-lock.json index 8930678ff..ee210cd9f 100644 --- a/module/services/frontend/code/package-lock.json +++ b/module/services/frontend/code/package-lock.json @@ -14,9 +14,9 @@ "@base-ui/react": "^1.3.0", "@bufbuild/protobuf": "^2.11.0", "@codefly-dev/saas-sdk": "0.3.15", - "@codefly-dev/saas-ui": "0.11.5", - "@codefly-dev/ui": "0.11.5", - "@codefly/saas-plugin-contract": "2.3.0", + "@codefly-dev/saas-ui": "0.12.0", + "@codefly-dev/ui": "0.12.0", + "@codefly/saas-plugin-contract": "2.4.0", "@codefly/saas-plugin-manifest": "0.1.0", "@codefly/saas-plugin-react": "0.4.1", "@codefly/saas-settings": "0.1.0", @@ -15266,7 +15266,7 @@ }, "packages/codefly-ui": { "name": "@codefly-dev/ui", - "version": "0.11.5", + "version": "0.12.0", "license": "Apache-2.0", "dependencies": { "@base-ui/react": "^1.3.0", @@ -15288,19 +15288,23 @@ "tailwind-merge": "^3.5.0" }, "devDependencies": { - "@codefly/saas-plugin-contract": "2.3.0", + "@codefly/saas-plugin-contract": "2.4.0", "@codefly/saas-plugin-react": "0.4.1", + "@tailwindcss/postcss": "^4", "@testing-library/react": "^16.1.0", "@types/hast": "^3.0.4", "@types/mdast": "^4.0.4", "@types/react": "^19.2.17", "happy-dom": "^20.8.9", + "postcss": "^8.5.26", "react": "^19.2.8", + "tailwindcss": "^4", + "tw-animate-css": "^1.4.0", "typescript": "5.9.3", "vitest": "^3.0.0" }, "peerDependencies": { - "@codefly/saas-plugin-contract": "2.3.0", + "@codefly/saas-plugin-contract": "2.4.0", "@codefly/saas-plugin-react": "0.4.1", "react": ">=19.2 <20" }, @@ -15339,7 +15343,7 @@ }, "packages/saas-plugin-contract": { "name": "@codefly/saas-plugin-contract", - "version": "2.3.0", + "version": "2.4.0", "license": "Apache-2.0", "dependencies": { "@bufbuild/protobuf": "^2.11.0" @@ -15354,7 +15358,7 @@ "version": "0.1.0", "license": "Apache-2.0", "dependencies": { - "@codefly/saas-plugin-contract": "2.3.0" + "@codefly/saas-plugin-contract": "2.4.0" }, "devDependencies": { "@types/js-yaml": "^4.0.9", @@ -15369,7 +15373,7 @@ "version": "0.4.1", "license": "Apache-2.0", "devDependencies": { - "@codefly/saas-plugin-contract": "2.3.0", + "@codefly/saas-plugin-contract": "2.4.0", "@testing-library/react": "^16.1.0", "@types/react": "19.2.17", "happy-dom": "^20.8.9", @@ -15378,7 +15382,7 @@ "vitest": "^3.0.0" }, "peerDependencies": { - "@codefly/saas-plugin-contract": "2.3.0", + "@codefly/saas-plugin-contract": "2.4.0", "react": ">=19.2 <20" } }, @@ -15418,12 +15422,12 @@ }, "packages/saas-ui": { "name": "@codefly-dev/saas-ui", - "version": "0.11.5", + "version": "0.12.0", "license": "Apache-2.0", "devDependencies": { "@bufbuild/protobuf": "^2.11.0", "@codefly-dev/saas-sdk": "^0.3.9", - "@codefly-dev/ui": "^0.11.0", + "@codefly-dev/ui": "^0.12.0", "@connectrpc/connect": "^2.1.1", "@connectrpc/connect-web": "^2.1.1", "@hookform/resolvers": "^3.9.0", @@ -15440,7 +15444,7 @@ "peerDependencies": { "@bufbuild/protobuf": ">=2", "@codefly-dev/saas-sdk": "^0.3.9", - "@codefly-dev/ui": "^0.11.0", + "@codefly-dev/ui": "^0.12.0", "@connectrpc/connect": ">=2", "@connectrpc/connect-web": ">=2", "@hookform/resolvers": ">=3", diff --git a/module/services/frontend/code/package.json b/module/services/frontend/code/package.json index 675b2aa80..46019785b 100644 --- a/module/services/frontend/code/package.json +++ b/module/services/frontend/code/package.json @@ -37,9 +37,9 @@ "@base-ui/react": "^1.3.0", "@bufbuild/protobuf": "^2.11.0", "@codefly-dev/saas-sdk": "0.3.15", - "@codefly-dev/saas-ui": "0.11.5", - "@codefly-dev/ui": "0.11.5", - "@codefly/saas-plugin-contract": "2.3.0", + "@codefly-dev/saas-ui": "0.12.0", + "@codefly-dev/ui": "0.12.0", + "@codefly/saas-plugin-contract": "2.4.0", "@codefly/saas-plugin-manifest": "0.1.0", "@codefly/saas-plugin-react": "0.4.1", "@codefly/saas-settings": "0.1.0", diff --git a/module/services/frontend/code/packages/codefly-ui/CATALOG.md b/module/services/frontend/code/packages/codefly-ui/CATALOG.md index 8f9e5042c..19614ae75 100644 --- a/module/services/frontend/code/packages/codefly-ui/CATALOG.md +++ b/module/services/frontend/code/packages/codefly-ui/CATALOG.md @@ -9,7 +9,7 @@ not evidence that all primitives, stories or production callers had migrated. | Tier | Contract | | --- | --- | -| `layout` (rank 1) | Native and Base UI controls, compound CardRoot/TabsRoot, data-in Card/Tabs, SegmentedControl, Pagination, Field, table toolbar and empty state, page layout, feedback, responsive Sidebar and Toaster | +| `layout` (rank 1) | Native and Base UI controls, compound CardRoot/TabsRoot, data-in Card/Tabs, SegmentedControl, Pagination, Field, table toolbar and empty state, page layout, status tones (Badge, Chip/ChipGroup, Banner), List/ListItem and DescriptionList, feedback, responsive Sidebar and Toaster | | `content` (rank 2) | Markdown (GFM, no raw HTML, safe links, images opt-in), bounded collapsible JSON tree, code with lazily-loaded highlighting, whitespace-preserving text, and the `Content` switch over them with `auto` detection and an `inline` one-line variant | | `dashboard` (rank 3) | Declarative dashboard, point charts, multi-series metric charts, tiles, provenance and sparklines | | `chat` (rank 3) | Resolved messages and injected send action | @@ -17,7 +17,11 @@ not evidence that all primitives, stories or production callers had migrated. Composite tiers compose layout; they do not import sibling composites. No tier fetches data, reads host authentication or imports private host aliases. Host CSS -is authoritative; owner packages supply class names, not a second stylesheet. +is authoritative: the kit supplies class names and the Tailwind source that +defines them (`theme.css`, `type-slots.css`), which the host compiles into its one +stylesheet. The kit's compiled `preview.css` is for a preview with no host and +is never loaded beside the host's +([README](./README.md#previewing-a-solution-without-a-host)). ## Stories and coverage diff --git a/module/services/frontend/code/packages/codefly-ui/README.md b/module/services/frontend/code/packages/codefly-ui/README.md index f7c35e303..62f8c1cc0 100644 --- a/module/services/frontend/code/packages/codefly-ui/README.md +++ b/module/services/frontend/code/packages/codefly-ui/README.md @@ -31,8 +31,9 @@ and the v1 subset — is [CATALOG.md](./CATALOG.md). data-in presentation. `layout` carries the page containers (`Card`, `Section`, `Tabs`) and the shadcn primitives promoted into the kit as their single sealed home (issue #451) — actions (`Button`), forms (`Input`, `Textarea`, `Label`, - `Checkbox`, `Switch`, `Select`), data display (`Badge`, `Avatar`, `Table`, - `Skeleton`, `Separator`) and overlays (`Dialog`, `AlertDialog`, `Notice`, `Tooltip`, + `Checkbox`, `Switch`, `Select`), data display (`Badge`, `Chip`, `List`, + `DescriptionList`, `Avatar`, `Table`, `Skeleton`, `Separator`), feedback + (`Banner`, `EmptyState`, `ErrorState`) and overlays (`Dialog`, `AlertDialog`, `Notice`, `Tooltip`, `DropdownMenu`); `dashboard` is ``, charts, `fromDashboardData`; `chat` is ``. React only: no plugin runtime, no host context. This is the surface a solution fe-remote consumes. `` is fed by @@ -51,32 +52,42 @@ and the v1 subset — is [CATALOG.md](./CATALOG.md). `allowImages`, and no `innerHTML` anywhere; colour only through the host's token utilities. -### Status, headers and empty states - -Three props exist because a consuming solution reported reaching past the kit -for them (the same report that asked for a standalone stylesheet, a `Chip` and -list primitives — those are **not** here yet): - -- `` prefixes a small filled circle in the variant's own colour. - It is `aria-hidden` and decorative: the badge's text is what a reader and a - screen reader get, so the dot adds a glanceable mark without making colour - the only channel carrying the meaning. Use it when several badges sit in one - row and have to be told apart at a glance. **There is no `success` or - `warning` variant**, because the appearance layer defines no such token — - `destructive` is the only semantic colour the skin ladder carries. A healthy - state therefore uses `default` (solid) against `outline` for "not started"; - giving status its own tones means adding appearance tokens, which is a design - decision for the skin's owner, not something a component may invent. -- `` puts a line under the title, matching `Section`'s prop - of the same name. Without it a caller needing one either mixes `CardRoot` + - `CardDescription` into a page of plain `Card`s, which is how a page stops - looking like one page, or drops the sentence into the body where it reads as - content and loses the `card-description` slot. -- `` takes nodes, not just a string, so an empty state - whose explanation lives elsewhere on the page can **point at it** — "the card - above says where this stands", with a link if there is one. `children` is the - action slot; a cross-reference is not an action, so prose there (or - hand-written outside the block) loses the slot and its width bound. +### Status, card headers, lists and empty states + +A solution page is built from kit components, never from hand-laid utilities: +the kit is what makes such a page small. These exist because a consuming +solution measured where it had to reach past the kit. + +- **Status tones.** `StatusTone` is `neutral | success | warning | danger | + info`, shared by `Badge`, `Chip` and `Banner` so a page's statuses agree. + `success`, `warning` and `info` are appearance tokens a skin may override + ([TOKENS.md](./TOKENS.md)); `danger` reads `destructive`. A tone is a tint of + its colour under text in it, and replaces a `variant`'s colours rather than + stacking on them. Set up / Connected / Error is `neutral` / `success` / + `danger`. +- **``.** `dot` prefixes a decorative, `aria-hidden` circle + in the badge's own colour, so a row of statuses is told apart by shape and + not by colour alone; the text is still what a screen reader gets. `size` is + `sm | default | lg`. +- **`` / ``.** A badge a person can act on: `href` makes it a + link, `onClick` a button, `render` swaps in a router link. `icon` leads, + `meta` trails after a separator (`meta="unread"` reads "· unread"), + `onRemove` + `removeLabel` add a remove control. The remove control sits + beside the link, never inside it: a button in a link is two targets announced + as one. `ChipGroup` is a named, wrapping list of them. +- **``** puts the card's one-line purpose under its title in + the `card-description` slot, matching `Section`'s prop — not a muted + paragraph in the body, and not `CardRoot` mixed into a page of `Card`s. +- **`` / `` and ``** for the places a `Table` is + too heavy: a few rows with an icon, a description, quiet `meta` and + `actions` (`variant="divided"` rules lines between them), and term/value + pairs, `inline` in two aligned columns or `stacked`. +- **``.** `neutral` (the default) is the product speaking and keeps + the original look; the status tones add a glyph each. `danger` is + `role="alert"`, the rest `role="status"`. +- **``.** "The source card above says where this stands" + is a pointer, not an explanation (`description`) and not something to press + (`children`). ## Entry points @@ -92,6 +103,8 @@ list primitives — those are **not** here yet): | `@codefly-dev/ui/chat` | `Chat` (React-only) | | `@codefly-dev/ui/content` | `Content`, `Markdown`, `JsonView`, `CodeBlock`, `TextBlock` (React-only) | | `@codefly-dev/ui/type-slots.css` | The generated type-slot and control-rung utilities | +| `@codefly-dev/ui/theme.css` | The token layer: token → utility, light/dark binding, custom variants (Tailwind source) | +| `@codefly-dev/ui/preview.css` | The kit compiled with the default skin, for previews only — see below | `react`, `@codefly/saas-plugin-react`, and `@codefly/saas-plugin-contract` are **peer** dependencies — the host provides them so it and its Module-Federation @@ -127,10 +140,12 @@ plugin peers are optional, the solution only needs an `.npmrc` pointing the **Styling.** The kit's components name their type slots and control rungs as classes (`type-card-title`, `control-sm`) that the kit defines, not Tailwind. A consumer that compiles the kit's source with its own Tailwind build imports the -generated stylesheet into its entry alongside its `@source` for the kit: +token layer and the generated utilities into its entry alongside its `@source` +for the kit, as the host's own `globals.css` does: ```css @import "tailwindcss"; +@import "@codefly-dev/ui/theme.css"; @import "@codefly-dev/ui/type-slots.css"; @source "../node_modules/@codefly-dev/ui/src"; ``` @@ -201,8 +216,63 @@ const skin = await resolveSkin({ The first source returning a valid descriptor wins; an invalid one is logged and skipped so the compiled default always renders. -`Banner` from `@codefly-dev/ui/layout` renders persistent polite feedback with -optional actions and dismissal. The caller owns data, authorization and read state. +`Banner` from `@codefly-dev/ui/layout` renders persistent feedback with optional +actions and dismissal. The caller owns data, authorization and read state. + +## Previewing a solution without a host + +Every class a kit component writes is defined only where something compiled the +kit with its token layer, and every variable those classes read is set only +where a host projected a skin onto ``. A solution authors no CSS, so on +its own it could not render its page at all. `@codefly-dev/ui/preview.css` is +that compile done once, in this package: plain CSS, the kit's own source only, +with the default skin's values at `:root`. + +It is generated (`npm run generate:preview-stylesheet`) and committed, not built +at publish. Its bytes depend on inputs outside this folder — the contract's +default skin and the Tailwind, Lightning CSS and tw-animate-css versions in the +lockfile — which the kit-version gate cannot see. Committed, a change to any of +them shows up as a changed file here, and the host's `preview-stylesheet` test +fails until it is regenerated. + +```ts +// The preview harness's entry — never a module the remote exposes. +import "@codefly-dev/ui/preview.css"; +``` + +**A preview build may use it** to render, review and screenshot a solution's +pages and stories locally, and to run visual tests against them. Dark mode is +`class="dark"` on an ancestor, as in the host. + +**A preview build may not use it for:** + +- **Anything a host loads.** Import it only from the preview harness's own + entry, never from a module the remote exposes or anything that module + imports. A remote renders inside the host's document, under the host's one + compiled stylesheet and the deployment's skin; bundled, this would ship a + second Tailwind preflight and the default skin's values into that document. +- **Evidence of how a deployment looks.** It is the default skin. A + deployment's skin changes colour, type, radius and density, so a preview + screenshot shows structure and states, not the product's appearance. +- **Styling anything but kit components.** Only classes the kit's own source + writes are compiled in. A utility a solution writes itself renders unstyled + in preview, and that is deliberate: it is the hand-laid markup the kit exists + to replace. `@codefly-dev/saas-ui`'s components are not covered either; they + render inside the host's stylesheet. + +The host's `src/lib/__tests__/preview-stylesheet.test.tsx` renders every story +here and fails if any class one of them carries is missing from the file, so +"built from kit components" and "painted in preview" stay the same claim. + +**Structure travels inline when the host does not compile you.** The host's +stylesheet is compiled over this kit and `@codefly-dev/saas-ui` only (its +`@source` lines); it compiles nothing a solution remote or another module's kit +ships. A utility that exists only because one file wrote it — an arbitrary value +such as `grid-cols-[…]` or `h-[var(--x)]` — is therefore defined for this kit +and missing everywhere else. `DescriptionList`'s +`grid-cols-[minmax(0,max-content)_minmax(0,1fr)]` is fine here for that reason. A +kit element meant to render from a remote carries its structure inline (a +`style` for layout) and takes only colour and type from the tokens. ## A loading indicator never flashes diff --git a/module/services/frontend/code/packages/codefly-ui/TOKENS.md b/module/services/frontend/code/packages/codefly-ui/TOKENS.md index cb72a116c..5415f2833 100644 --- a/module/services/frontend/code/packages/codefly-ui/TOKENS.md +++ b/module/services/frontend/code/packages/codefly-ui/TOKENS.md @@ -31,11 +31,16 @@ The chain is one direction, top to bottom — a skin is *data*, never CSS: descriptor onto that default. Only the tokens a skin declares are overridden; the rest inherit the default. 3. **Custom properties** — the host projects the resolved appearance onto - `` as `--appearance-{light,dark}-` properties, and - `src/app/globals.css` binds the active mode's set to the public shadcn - variables (`--muted-foreground`, `--border`, …). -4. **Utilities** — Tailwind maps those variables to utility classes - (`bg-background`, `text-muted-foreground`, `border-border`, …). + `` as `--appearance-{light,dark}-` properties + (`appearanceStyleProperties`, `@codefly-dev/ui/skin`), and the kit's + `theme.css` binds the active mode's set to the public shadcn variables + (`--muted-foreground`, `--border`, …). +4. **Utilities** — `theme.css` maps those variables to Tailwind utility classes + (`bg-background`, `text-muted-foreground`, `border-border`, …). It is + Tailwind source the host imports into its one compiled stylesheet; + `preview.css` is the same layer compiled over the kit alone with the + default values, for a preview with no host + ([README](./README.md#previewing-a-solution-without-a-host)). 5. **Components** — kit components, host pages, and solution remotes reference only those utilities / variables. None of them names a raw color. @@ -67,6 +72,9 @@ variables read — it does not layer one over the other. | `accent` | `--accent` | Hover / accent surface | `oklch(0.97 0 0)` | `oklch(0.269 0 0)` | | `accentForeground` | `--accent-foreground` | Text on the accent surface | `oklch(0.205 0 0)` | `oklch(0.985 0 0)` | | `destructive` | `--destructive` | Destructive action / error | `oklch(0.577 0.245 27.325)` | `oklch(0.704 0.191 22.216)` | +| `success` | `--success` | Status: healthy, done. Read as text and as a tint of itself | `oklch(0.527 0.154 150.069)` | `oklch(0.792 0.209 151.711)` | +| `warning` | `--warning` | Status: needs attention, not failed. Read as text and as a tint of itself | `oklch(0.555 0.163 48.998)` | `oklch(0.828 0.189 84.429)` | +| `info` | `--info` | Status: informational. Read as text and as a tint of itself | `oklch(0.546 0.245 262.881)` | `oklch(0.707 0.165 254.624)` | | `border` | `--border` | Default hairline border | `oklch(0.922 0 0)` | `oklch(1 0 0 / 10%)` | | `input` | `--input` | Input control border | `oklch(0.922 0 0)` | `oklch(1 0 0 / 15%)` | | `ring` | `--ring` | Focus ring | `oklch(0.708 0 0)` | `oklch(0.556 0 0)` | @@ -223,18 +231,19 @@ other are two slots, or two rungs that both set a height. | `surface-title-tight` | `dialog-title` | | `surface-title-compact` | `card-title-sm` | | `surface-title-plain` | `card-heading` | -| `surface-description` | `card-description`, `dialog-description`, `sheet-description`, `alert-dialog-description`, `field-description`, `field-error` | +| `surface-description` | `card-description`, `dialog-description`, `sheet-description`, `alert-dialog-description`, `list-item-description`, `field-description`, `field-error`, `empty-state-reference` | | `plain` | `metric-delta-label` | | `page-title` | `page-title` | | `section-title` | `section-title`, `empty-state-title-illustrated` | -| `body` | `body`, `card`, `dialog-content`, `sheet-content`, `section-description`, `input`, `textarea`, `select-trigger`, `command-input`, `command-empty`, `banner`, `table`, `pagination-ellipsis`, `table-toolbar`, `table-empty-state`, `card-metadata`, `table-caption`, `tabs-content`, `avatar-fallback`, `avatar-group-count`, `sidebar-group-content`, `sidebar-menu-button`, `sidebar-menu-button-lg`, `sidebar-menu-sub-button`, `error-state`, `input-group-text`, `input-group-control`, `empty-state-description`, `metric-label`, `chat-message` | +| `body` | `body`, `card`, `dialog-content`, `sheet-content`, `section-description`, `input`, `textarea`, `select-trigger`, `command-input`, `command-empty`, `banner`, `list-item`, `description-details`, `table`, `pagination-ellipsis`, `table-toolbar`, `table-empty-state`, `card-metadata`, `table-caption`, `tabs-content`, `avatar-fallback`, `avatar-group-count`, `sidebar-group-content`, `sidebar-menu-button`, `sidebar-menu-button-lg`, `sidebar-menu-sub-button`, `error-state`, `input-group-text`, `input-group-control`, `empty-state-description`, `metric-label`, `chat-message` | | `emphasis` | `emphasis`, `banner-title`, `table-head`, `table-footer`, `sidebar-menu-button-active`, `error-state-title` | | `control-label` | `label` | | `menu-item` | `select-item`, `dropdown-menu-item`, `dropdown-menu-checkbox-item`, `dropdown-menu-radio-item`, `dropdown-menu-sub-trigger`, `command-item` | | `group-label` | `dropdown-menu-label`, `command-group`, `badge`, `card-eyebrow`, `sidebar-group-label`, `sidebar-menu-badge` | -| `group-label-plain` | `caption-plain`, `select-label`, `tooltip-content`, `avatar-fallback-sm`, `sidebar-menu-button-sm`, `sidebar-menu-sub-button-sm` | +| `group-label-plain` | `caption-plain`, `select-label`, `chip-meta`, `description-term`, `tooltip-content`, `avatar-fallback-sm`, `sidebar-menu-button-sm`, `sidebar-menu-sub-button-sm` | | `shortcut` | `dropdown-menu-shortcut`, `command-shortcut` | -| `control` | `segmented-control-segment`, `tabs-trigger`, `input-group-addon`, `empty-state-title`, `metric-heading` | +| `control` | `badge-lg`, `segmented-control-segment`, `tabs-trigger`, `input-group-addon`, `empty-state-title`, `metric-heading` | +| `control-sm` | `chip` | | `control-touch` | `input-touch`, `textarea-touch` | | `metric-value` | `metric-value` | | `metric-value-lg` | `metric-value-lg` | diff --git a/module/services/frontend/code/packages/codefly-ui/package.json b/module/services/frontend/code/packages/codefly-ui/package.json index 759371743..4be7fa5fd 100644 --- a/module/services/frontend/code/packages/codefly-ui/package.json +++ b/module/services/frontend/code/packages/codefly-ui/package.json @@ -1,6 +1,6 @@ { "name": "@codefly-dev/ui", - "version": "0.11.5", + "version": "0.12.0", "description": "The shared, versioned Codefly SaaS frontend kit: the plugin host and the tokens-as-data skin mechanism, standalone so the host and Module-Federation remotes import and dedupe one copy.", "repository": { "type": "git", @@ -43,6 +43,8 @@ "default": "./dist/skin/index.js" }, "./type-slots.css": "./src/skin/type-slots.generated.css", + "./theme.css": "./src/skin/theme.css", + "./preview.css": "./src/skin/preview.generated.css", "./dashboard": { "types": "./dist/dashboard/index.d.ts", "import": "./dist/dashboard/index.js", @@ -72,7 +74,8 @@ "scripts": { "build": "tsc -p tsconfig.json", "test": "vitest run --config vitest.config.ts", - "generate:type-utilities": "node scripts/generate-type-utilities.mjs" + "generate:type-utilities": "node scripts/generate-type-utilities.mjs", + "generate:preview-stylesheet": "node scripts/generate-preview-stylesheet.mjs" }, "dependencies": { "@base-ui/react": "^1.3.0", @@ -94,7 +97,7 @@ "react-markdown": "^10.1.0" }, "peerDependencies": { - "@codefly/saas-plugin-contract": "2.3.0", + "@codefly/saas-plugin-contract": "2.4.0", "@codefly/saas-plugin-react": "0.4.1", "react": ">=19.2 <20" }, @@ -107,11 +110,15 @@ } }, "devDependencies": { - "@codefly/saas-plugin-contract": "2.3.0", + "@codefly/saas-plugin-contract": "2.4.0", "@codefly/saas-plugin-react": "0.4.1", "@testing-library/react": "^16.1.0", + "@tailwindcss/postcss": "^4", "@types/react": "^19.2.17", "happy-dom": "^20.8.9", + "postcss": "^8.5.26", + "tailwindcss": "^4", + "tw-animate-css": "^1.4.0", "react": "^19.2.8", "typescript": "5.9.3", "vitest": "^3.0.0", diff --git a/module/services/frontend/code/packages/codefly-ui/scripts/generate-preview-stylesheet.mjs b/module/services/frontend/code/packages/codefly-ui/scripts/generate-preview-stylesheet.mjs new file mode 100644 index 000000000..fe6ea8135 --- /dev/null +++ b/module/services/frontend/code/packages/codefly-ui/scripts/generate-preview-stylesheet.mjs @@ -0,0 +1,108 @@ +#!/usr/bin/env node +// Compile the kit, alone, into one plain stylesheet painted with the default +// skin: `@codefly-dev/ui/preview.css`. +// +// Every class a kit component writes is defined only where something compiled +// the kit's source with its token layer, and every variable those classes read +// is set only where a host projected a skin onto . A solution authors no +// CSS, so without this it cannot render its own page anywhere but inside a +// running host. This is that compile, done once here, so a preview needs no +// Tailwind of its own. +// +// node scripts/generate-preview-stylesheet.mjs # write (from packages/codefly-ui) +// node scripts/generate-preview-stylesheet.mjs --check # fail if stale +// +// Reads the BUILT projection (`dist/skin/appearance.js`), so build the kit +// first; `prepare:frontend` does. +// +// Committed rather than built at publish: its bytes depend on more than this +// package — the contract's default skin and the Tailwind, Lightning CSS and +// tw-animate-css versions in the lockfile — and the kit-version gate judges only +// what is in this folder. A dist build could change under an unchanged version +// and be refused at publish as "already published with different contents". +// Committed, the bytes are what the gate diffs, and the --check here fails the +// moment any of those inputs moves without a regeneration. +import { readFileSync, writeFileSync } from "node:fs"; +import { createRequire } from "node:module"; +import { dirname, join, resolve } from "node:path"; +import { fileURLToPath } from "node:url"; + +import { DEFAULT_FRONTEND_APPEARANCE } from "@codefly/saas-plugin-contract"; + +import { appearanceStyleProperties } from "../dist/skin/appearance.js"; + +const PACKAGE_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), ".."); +const SKIN = join(PACKAGE_ROOT, "src/skin"); +// Overridable only so the host's test can prove `--check` refuses a stale copy +// without corrupting the tracked file other tests read. +const OUTPUT = + process.env.PREVIEW_STYLESHEET_PATH ?? join(SKIN, "preview.generated.css"); +const require = createRequire(join(PACKAGE_ROOT, "package.json")); +const postcss = require("postcss"); +const tailwind = require("@tailwindcss/postcss"); + +const HEADER = `/* Generated by @codefly-dev/ui scripts/generate-preview-stylesheet.mjs — do not edit. + * + * The kit's components compiled with its token layer (theme.css) and type + * utilities, painted with the DEFAULT skin. Import it once at the root of a + * preview so kit components render without a running host. + * + * May be used for: a solution's local preview, its component stories, and the + * screenshots or visual tests taken from them. + * + * May not be used for: + * - Anything a host loads. A solution's remote renders inside the host's + * document under the host's one compiled stylesheet and the deployment's + * skin. Bundled into a remote, this ships a second Tailwind preflight and the + * default skin's values into that document. + * - Evidence of how a deployment looks. It is the default skin; a deployment's + * skin changes colour, type, radius and density. + * - Styling anything but kit components. Only classes the kit's own source + * writes are here, so a hand-laid utility in a solution renders unstyled in + * preview — the kit is what a solution page is built from. + * + * Dark mode: put class="dark" on an ancestor, as the host does. + */ +`; + +// `source(none)`: only the kit's own source, never whatever happens to sit +// beside it, and never this output, which names every class it defines. +const ENTRY = `@import "tailwindcss" source(none); +@import "tw-animate-css"; +@import "./theme.css"; +@import "./type-slots.generated.css"; +@source "../**/*.{ts,tsx}"; +@source not "../**/__tests__"; +`; + +function defaults() { + const properties = appearanceStyleProperties(DEFAULT_FRONTEND_APPEARANCE); + const lines = Object.entries(properties) + .map(([name, value]) => `\t${name}: ${value};`) + .join("\n"); + // What the host writes as an inline style on , here as the root rule. + return `:root {\n${lines}\n}\n`; +} + +async function render() { + const result = await postcss([ + tailwind({ base: PACKAGE_ROOT, optimize: { minify: false } }), + ]).process(ENTRY + defaults(), { + from: join(SKIN, "preview.entry.css"), + }); + return `${HEADER}\n${result.css.trimEnd()}\n`; +} + +const rendered = await render(); +if (process.argv.includes("--check")) { + if (readFileSync(OUTPUT, "utf8") !== rendered) { + console.error( + `preview stylesheet: ${OUTPUT} is stale — run node scripts/generate-preview-stylesheet.mjs`, + ); + process.exit(1); + } + console.log("preview stylesheet OK"); +} else { + writeFileSync(OUTPUT, rendered); + console.log(`preview stylesheet written: ${rendered.length} bytes`); +} diff --git a/module/services/frontend/code/packages/codefly-ui/src/__tests__/no-raw-type-primitives.test.ts b/module/services/frontend/code/packages/codefly-ui/src/__tests__/no-raw-type-primitives.test.ts index e03087adc..73c38f3b7 100644 --- a/module/services/frontend/code/packages/codefly-ui/src/__tests__/no-raw-type-primitives.test.ts +++ b/module/services/frontend/code/packages/codefly-ui/src/__tests__/no-raw-type-primitives.test.ts @@ -79,7 +79,7 @@ const SAAS_UI_BASELINE: Record = { // `datasources/credential-mode.tsx`, which carries none: the shared copy // names its slots, so the debt left behind shrank rather than moving. "datasources/connect-github-form.tsx": 11, - "datasources/datasources-panel.tsx": 17, + "datasources/datasources-panel.tsx": 15, }; const saasUiDir = resolve(srcDir, "../../saas-ui/src"); const saasUiFiles = existsSync(saasUiDir) ? sourceFiles(saasUiDir) : []; diff --git a/module/services/frontend/code/packages/codefly-ui/src/__tests__/theme.test.ts b/module/services/frontend/code/packages/codefly-ui/src/__tests__/theme.test.ts new file mode 100644 index 000000000..1dfbb77a4 --- /dev/null +++ b/module/services/frontend/code/packages/codefly-ui/src/__tests__/theme.test.ts @@ -0,0 +1,55 @@ +import { existsSync, readFileSync } from "node:fs"; +import { FRONTEND_APPEARANCE_TOKEN_NAMES } from "@codefly/saas-plugin-contract"; +import { describe, expect, it } from "vitest"; + +// The contract owns the token names; theme.css is where each becomes a variable +// and a utility. A token missing from either half fails nowhere at runtime: a +// component writing `text-success` just renders in the inherited colour, which +// is exactly how a status tone went unstyled before this file carried one. + +// Two Vitest projects with different cwds, as in token-contract.test.ts. +function themePath(): string { + for (const path of [ + "src/skin/theme.css", + "packages/codefly-ui/src/skin/theme.css", + ]) { + if (existsSync(path)) return path; + } + throw new Error("could not locate the @codefly-dev/ui theme.css"); +} + +// `chart1` → `--chart-1`, `cardForeground` → `--card-foreground`. +function cssVariable(token: string): string { + return `--${token + .replace(/([a-z])([0-9])/g, "$1-$2") + .replace(/[A-Z]/g, (letter) => `-${letter.toLowerCase()}`)}`; +} + +// The flat rule block for a selector (these blocks nest nothing). The last +// one: the first `@theme inline` holds the shadcn keyframes, not the colours. +function cssBlock(source: string, selector: string): string { + const open = source.lastIndexOf(`\n${selector} {`); + if (open < 0) throw new Error(`theme.css has no ${selector} block`); + const close = source.indexOf("\n}", open + 1); + if (close < 0) throw new Error(`theme.css ${selector} block is unterminated`); + return source.slice(open, close); +} + +const theme = readFileSync(themePath(), "utf8"); +const utilities = cssBlock(theme, "@theme inline"); +const light = cssBlock(theme, ":root"); +const dark = cssBlock(theme, ".dark"); + +describe("theme.css carries every contract token from skin to utility", () => { + for (const token of FRONTEND_APPEARANCE_TOKEN_NAMES) { + const variable = cssVariable(token); + it(`binds ${variable} in both modes and maps it to a colour utility`, () => { + expect(light, `:root must define ${variable}`).toContain(`${variable}:`); + expect(dark, `.dark must define ${variable}`).toContain(`${variable}:`); + expect( + utilities, + `@theme must map --color${variable.slice(1)} so bg-/text- utilities exist`, + ).toContain(`--color${variable.slice(1)}: var(${variable})`); + }); + } +}); diff --git a/module/services/frontend/code/packages/codefly-ui/src/layout/__tests__/card.test.tsx b/module/services/frontend/code/packages/codefly-ui/src/layout/__tests__/card.test.tsx index fdb272803..5dc45abac 100644 --- a/module/services/frontend/code/packages/codefly-ui/src/layout/__tests__/card.test.tsx +++ b/module/services/frontend/code/packages/codefly-ui/src/layout/__tests__/card.test.tsx @@ -28,6 +28,38 @@ describe("Card", () => { expect(screen.queryByRole("heading")).toBeNull(); expect(screen.getByText("only body")).toBeTruthy(); }); + + it("says what the card is under its title, in the card-description slot", () => { + const { container } = render( + +

body

+
, + ); + const description = container.querySelector( + "[data-slot=card-description]", + ); + expect(description?.textContent).toBe( + "Declared by this solution, not chosen here.", + ); + expect(description?.className).toContain("type-card-description"); + }); + + // Two lines of heading would float centred actions against their middle. + it("top-aligns its actions against a heading with a description", () => { + const { container } = render( + Add} + />, + ); + const header = container.querySelector("[data-slot=card-heading]") + ?.parentElement?.parentElement; + expect(header?.className).toContain("items-start"); + }); }); describe("Section", () => { diff --git a/module/services/frontend/code/packages/codefly-ui/src/layout/__tests__/chip.test.tsx b/module/services/frontend/code/packages/codefly-ui/src/layout/__tests__/chip.test.tsx new file mode 100644 index 000000000..9d3d9da05 --- /dev/null +++ b/module/services/frontend/code/packages/codefly-ui/src/layout/__tests__/chip.test.tsx @@ -0,0 +1,147 @@ +// @vitest-environment happy-dom +import { cleanup, fireEvent, render, screen } from "@testing-library/react"; +import { afterEach, describe, expect, it, vi } from "vitest"; +import { Chip, ChipGroup } from "../chip.js"; + +afterEach(cleanup); + +describe("Chip", () => { + it("is a link when given an href", () => { + render(Pricing change); + const link = screen.getByRole("link", { name: "Pricing change" }); + expect(link.getAttribute("href")).toBe("/proposals/p-1"); + }); + + it("is a button when given onClick, and says it is one", () => { + const onClick = vi.fn(); + render(Filter: open); + const button = screen.getByRole("button", { name: "Filter: open" }); + expect(button.getAttribute("type")).toBe("button"); + fireEvent.click(button); + expect(onClick).toHaveBeenCalledOnce(); + }); + + it("is plain text when neither is given", () => { + render(Read only); + expect(screen.queryByRole("link")).toBeNull(); + expect(screen.queryByRole("button")).toBeNull(); + expect(screen.getByText("Read only")).toBeTruthy(); + }); + + it("renders a router link in place of the anchor", () => { + render( + }>Routed, + ); + const link = screen.getByRole("link", { name: "Routed" }); + expect(link.getAttribute("data-router")).toBe("yes"); + expect(link.getAttribute("data-slot")).toBe("chip-action"); + }); + + it("puts its meta after a separator a screen reader skips", () => { + const { container } = render( + + Pricing change + , + ); + const meta = container.querySelector("[data-slot=chip-meta]"); + expect(meta?.textContent).toBe("· unread"); + expect(meta?.querySelector("[aria-hidden]")?.textContent).toBe("· "); + expect(screen.getByRole("link").textContent).toContain("unread"); + }); + + it("keeps its leading icon out of the accessible name", () => { + const { container } = render( + }>Owner, + ); + expect( + container.querySelector("[data-slot=chip-icon]")?.getAttribute( + "aria-hidden", + ), + ).toBe("true"); + }); + + // A button inside a link is two targets announced as one. + it("places remove beside the link, never inside it", () => { + const onRemove = vi.fn(); + render( + + Pricing change + , + ); + const remove = screen.getByRole("button", { + name: "Remove Pricing change", + }); + expect(screen.getByRole("link").contains(remove)).toBe(false); + fireEvent.click(remove); + expect(onRemove).toHaveBeenCalledOnce(); + }); + + // WCAG 2.4.7: the ring is drawn outside the control, so anything between + // the control and the chip's edge that clips overflow hides it. + it("draws a keyboard focus ring nothing in the chip crops", () => { + const { container } = render( + {}} removeLabel="Remove Owner"> + Owner + , + ); + const chip = container.querySelector("[data-slot=chip]") as HTMLElement; + for (const control of [ + screen.getByRole("link"), + screen.getByRole("button", { name: "Remove Owner" }), + ]) { + expect(control.className).toContain("focus-visible:ring-[3px]"); + for ( + let element: HTMLElement | null = control; + element && chip.contains(element); + element = element.parentElement + ) + expect(element.className).not.toMatch(/overflow-(hidden|clip)/); + } + }); + + it("has no remove control unless asked", () => { + render(Pricing change); + expect(screen.queryByRole("button")).toBeNull(); + }); + + it.each([ + ["neutral", "border-border"], + ["success", "text-success"], + ["warning", "text-warning"], + ["danger", "text-destructive"], + ["info", "text-info"], + ] as const)("carries the %s tone", (tone, cls) => { + const { container } = render(Status); + const chip = container.querySelector("[data-slot=chip]"); + expect(chip?.getAttribute("data-tone")).toBe(tone); + expect(chip?.className).toContain(cls); + }); + + it("only shows a hover affordance when there is something to press", () => { + const { container } = render( + <> + Static + Linked + , + ); + const [still, linked] = container.querySelectorAll( + "[data-slot=chip-action]", + ); + expect(still.className).not.toContain("cursor-pointer"); + expect(linked.className).toContain("cursor-pointer"); + }); +}); + +describe("ChipGroup", () => { + it("is a named list with one item per chip", () => { + render( + + Billing + Onboarding + {null} + , + ); + const list = screen.getByRole("list", { name: "Affects" }); + expect(list.querySelectorAll(":scope > li")).toHaveLength(2); + }); +}); diff --git a/module/services/frontend/code/packages/codefly-ui/src/layout/__tests__/list.test.tsx b/module/services/frontend/code/packages/codefly-ui/src/layout/__tests__/list.test.tsx new file mode 100644 index 000000000..b1349c0f7 --- /dev/null +++ b/module/services/frontend/code/packages/codefly-ui/src/layout/__tests__/list.test.tsx @@ -0,0 +1,131 @@ +// @vitest-environment happy-dom +import { cleanup, render, screen, within } from "@testing-library/react"; +import { afterEach, describe, expect, it } from "vitest"; +import { DescriptionList, List, ListItem } from "../list.js"; + +afterEach(cleanup); + +describe("List", () => { + it("is a list of its items", () => { + render( + + Front matter has no owner + Two headings share a level + , + ); + const list = screen.getByRole("list", { name: "Parsing notes" }); + expect(within(list).getAllByRole("listitem")).toHaveLength(2); + }); + + it("rules lines only when divided", () => { + const { container, rerender } = render( + + One + , + ); + const list = () => container.querySelector("[data-slot=list]"); + expect(list()?.className).not.toContain("divide-y"); + rerender( + + One + , + ); + expect(list()?.getAttribute("data-variant")).toBe("divided"); + expect(list()?.className).toContain("divide-y"); + }); +}); + +describe("ListItem", () => { + it("renders every slot it is given, in reading order", () => { + const { container } = render( + + } + description="Line 12 of the source" + meta="2 min ago" + actions={} + > + Heading level skipped + + , + ); + const slots = [ + ...container.querySelectorAll("[data-slot^=list-item-]"), + ].map((element) => element.getAttribute("data-slot")); + expect(slots).toEqual([ + "list-item-icon", + "list-item-title", + "list-item-description", + "list-item-meta", + "list-item-actions", + ]); + expect( + container + .querySelector("[data-slot=list-item-icon]") + ?.getAttribute("aria-hidden"), + ).toBe("true"); + expect(screen.getByRole("button", { name: "Open" })).toBeTruthy(); + }); + + it("renders only the title when nothing else is given", () => { + const { container } = render( + + Just a row + , + ); + expect( + [...container.querySelectorAll("[data-slot^=list-item-]")].map((e) => + e.getAttribute("data-slot"), + ), + ).toEqual(["list-item-title"]); + }); +}); + +describe("DescriptionList", () => { + const items = [ + { term: "Status", value: "Draft" }, + { term: "Owner", value: "Jane Doe" }, + ]; + + it("pairs each term with its value", () => { + const { container } = render(); + const terms = [...container.querySelectorAll("dt")].map( + (e) => e.textContent, + ); + const values = [...container.querySelectorAll("dd")].map( + (e) => e.textContent, + ); + expect(terms).toEqual(["Status", "Owner"]); + expect(values).toEqual(["Draft", "Jane Doe"]); + // Each dd follows its own dt inside one group. + for (const group of container.querySelectorAll( + "[data-slot=description-item]", + )) + expect( + [...group.children].map((child) => child.tagName.toLowerCase()), + ).toEqual(["dt", "dd"]); + }); + + it("aligns pairs in two columns inline and stacks them otherwise", () => { + const { container, rerender } = render(); + const dl = () => container.querySelector("dl"); + const group = () => + container.querySelector("[data-slot=description-item]"); + expect(dl()?.getAttribute("data-layout")).toBe("inline"); + expect(group()?.className).toContain("contents"); + rerender(); + expect(dl()?.getAttribute("data-layout")).toBe("stacked"); + expect(group()?.className).not.toContain("contents"); + }); + + it("takes nodes as values and an explicit key for a non-string term", () => { + render( + Source, value: s }, + ]} + />, + ); + expect(screen.getByRole("link", { name: "s" })).toBeTruthy(); + }); +}); diff --git a/module/services/frontend/code/packages/codefly-ui/src/layout/__tests__/states.test.tsx b/module/services/frontend/code/packages/codefly-ui/src/layout/__tests__/states.test.tsx index 6a6ea725e..1449baad0 100644 --- a/module/services/frontend/code/packages/codefly-ui/src/layout/__tests__/states.test.tsx +++ b/module/services/frontend/code/packages/codefly-ui/src/layout/__tests__/states.test.tsx @@ -32,6 +32,36 @@ describe("EmptyState", () => { expect(screen.getByTestId("glyph")).toBeTruthy(); }); + // "The card above says where this stands" points elsewhere: it is neither + // the explanation (description) nor something to press (children). + it("renders a reference after the description and before the action", () => { + const { container } = render( + + The source card above says where this + stands. + + } + > + + , + ); + const order = [ + ...container.querySelectorAll( + "[data-slot=empty-state-description], [data-slot=empty-state-reference], button", + ), + ].map((element) => element.getAttribute("data-slot") ?? "action"); + expect(order).toEqual([ + "empty-state-description", + "empty-state-reference", + "action", + ]); + expect(screen.getByRole("link", { name: "source card above" })).toBeTruthy(); + }); + it("omits the description paragraph when none is given", () => { const { container } = render(); expect(container.querySelector("p")).toBeNull(); diff --git a/module/services/frontend/code/packages/codefly-ui/src/layout/__tests__/status.test.tsx b/module/services/frontend/code/packages/codefly-ui/src/layout/__tests__/status.test.tsx new file mode 100644 index 000000000..347fafc4f --- /dev/null +++ b/module/services/frontend/code/packages/codefly-ui/src/layout/__tests__/status.test.tsx @@ -0,0 +1,180 @@ +// @vitest-environment happy-dom +import { cleanup, render, screen } from "@testing-library/react"; +import { afterEach, describe, expect, it } from "vitest"; +import { Badge, type StatusTone } from "../badge.js"; +import { Banner } from "../banner.js"; + +afterEach(cleanup); + +const TONES: StatusTone[] = ["neutral", "success", "warning", "danger", "info"]; + +function classesOf(text: string): string[] { + return (screen.getByText(text).closest("[data-slot=badge]")?.className ?? "") + .split(/\s+/) + .filter(Boolean); +} + +describe("Badge tone", () => { + // The bug the tone exists to fix: Set up, Connected and Error rendered as + // outline, secondary and destructive, and secondary read as "nothing here". + it("paints three statuses three ways", () => { + render( + <> + Set up + Connected + Error + , + ); + expect(classesOf("Set up")).toContain("text-muted-foreground"); + expect(classesOf("Connected")).toContain("text-success"); + expect(classesOf("Error")).toContain("text-destructive"); + }); + + it.each([ + ["success", "bg-success/10"], + ["warning", "bg-warning/10"], + ["danger", "bg-destructive/10"], + ["info", "bg-info/10"], + ["neutral", "bg-muted"], + ] as const)("tints %s with its own token", (tone, tint) => { + render(Status); + expect(classesOf("Status")).toContain(tint); + }); + + // Two fills on one badge would resolve by class order, not by intent. + it("replaces the variant's colours rather than stacking over them", () => { + render( + + Healthy + , + ); + const classes = classesOf("Healthy"); + expect(classes).not.toContain("bg-primary"); + expect(classes).not.toContain("text-primary-foreground"); + }); + + it("keeps the variant when no tone is given", () => { + render(Pending); + expect(classesOf("Pending")).toContain("border-border"); + }); + + it("exposes the tone and size for a skin rule or a test to read", () => { + render( + + Paused + , + ); + const badge = screen.getByText("Paused"); + expect(badge.getAttribute("data-tone")).toBe("warning"); + expect(badge.getAttribute("data-size")).toBe("lg"); + }); +}); + +describe("Badge dot", () => { + it("is decorative, so the status is the text and not the colour", () => { + const { container } = render( + + Connected + , + ); + const dot = container.querySelector("[data-slot=badge-dot]"); + expect(dot?.getAttribute("aria-hidden")).toBe("true"); + expect(dot?.className).toContain("bg-current"); + expect(screen.getByText("Connected").textContent).toBe("Connected"); + }); + + it("is absent unless asked for", () => { + const { container } = render(Connected); + expect(container.querySelector("[data-slot=badge-dot]")).toBeNull(); + }); + + it.each([ + ["sm", "size-1"], + ["default", "size-1.5"], + ["lg", "size-2"], + ] as const)("scales with a %s badge", (size, dotClass) => { + const { container } = render( + + Live + , + ); + expect( + container.querySelector("[data-slot=badge-dot]")?.className, + ).toContain(dotClass); + }); +}); + +describe("Badge size", () => { + it.each([ + ["sm", "h-4"], + ["default", "h-5"], + ["lg", "h-6"], + ] as const)("%s sets the badge's height", (size, height) => { + render(Label); + expect(classesOf("Label")).toContain(height); + }); + + it("takes the larger type slot only at lg", () => { + render( + <> + Large + Regular + , + ); + expect(classesOf("Large")).toContain("type-badge-lg"); + expect(classesOf("Large")).not.toContain("type-badge"); + expect(classesOf("Regular")).toContain("type-badge"); + }); +}); + +describe("Banner tone", () => { + it("keeps the original look and no glyph when no tone is given", () => { + const { container } = render(); + const banner = container.querySelector("[data-slot=banner]"); + expect(banner?.getAttribute("data-tone")).toBe("neutral"); + expect(banner?.className).toContain("bg-primary/5"); + expect(container.querySelector("[data-slot=banner-icon]")).toBeNull(); + }); + + it.each( + TONES.filter((tone) => tone !== "neutral"), + )("gives %s a glyph, so it is told apart by shape", (tone) => { + const { container } = render(); + const icon = container.querySelector("[data-slot=banner-icon]"); + expect(icon?.querySelector("svg")).toBeTruthy(); + expect(icon?.getAttribute("aria-hidden")).toBe("true"); + }); + + it("gives each status tone a different glyph", () => { + const glyphs = TONES.filter((tone) => tone !== "neutral").map((tone) => { + const { container, unmount } = render(); + const svg = container.querySelector("[data-slot=banner-icon] svg"); + const shape = svg?.getAttribute("class") ?? ""; + unmount(); + return shape; + }); + expect(new Set(glyphs).size).toBe(glyphs.length); + }); + + it("interrupts for a fault and waits for anything else", () => { + render( + <> + + + , + ); + expect(screen.getByRole("alert").textContent).toContain( + "Nothing reached the host", + ); + expect(screen.getByRole("status").textContent).toContain("Local preview"); + }); + + it("lets a caller replace or remove the glyph", () => { + const { container, rerender } = render( + } title="x" />, + ); + expect(screen.getByTestId("own")).toBeTruthy(); + rerender(); + expect(container.querySelector("[data-slot=banner-icon]")).toBeNull(); + }); +}); diff --git a/module/services/frontend/code/packages/codefly-ui/src/layout/badge.tsx b/module/services/frontend/code/packages/codefly-ui/src/layout/badge.tsx index 65d9febb6..86bb72180 100644 --- a/module/services/frontend/code/packages/codefly-ui/src/layout/badge.tsx +++ b/module/services/frontend/code/packages/codefly-ui/src/layout/badge.tsx @@ -4,6 +4,14 @@ import { cva, type VariantProps } from "class-variance-authority"; import { cn } from "./cn.js"; +/** + * What a status says, as opposed to how loudly a badge says it (`variant`). + * `danger` reads the `destructive` token; the others read their own. Shared by + * every kit component that carries a status, so a page's Connected badge, its + * owner chip and its warning banner agree on what green means. + */ +export type StatusTone = "neutral" | "success" | "warning" | "danger" | "info"; + const badgeVariants = cva( "group/badge inline-flex h-5 w-fit shrink-0 items-center justify-center gap-1 overflow-hidden rounded-4xl border border-transparent px-2 py-0.5 type-badge whitespace-nowrap transition-all focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 [&>svg]:pointer-events-none [&>svg]:size-3!", { @@ -20,42 +28,72 @@ const badgeVariants = cva( "hover:bg-muted hover:text-muted-foreground dark:hover:bg-muted/50", link: "text-primary underline-offset-4 hover:underline", }, + // A tint of the tone's own colour under text in it, the shape + // `destructive` already had, so every tone reads at the same weight + // and none of them is mistaken for the primary action. + tone: { + neutral: "bg-muted text-muted-foreground [a]:hover:bg-muted/80", + success: + "bg-success/10 text-success dark:bg-success/20 [a]:hover:bg-success/20", + warning: + "bg-warning/10 text-warning dark:bg-warning/20 [a]:hover:bg-warning/20", + danger: + "bg-destructive/10 text-destructive dark:bg-destructive/20 [a]:hover:bg-destructive/20", + info: "bg-info/10 text-info dark:bg-info/20 [a]:hover:bg-info/20", + }, + size: { + sm: "h-4 px-1.5 py-0", + default: "", + lg: "h-6 gap-1.5 px-2.5 type-badge-lg [&>svg]:size-3.5!", + }, }, defaultVariants: { variant: "default", + size: "default", }, }, ); -/** - * `dot` prefixes a small filled circle that inherits the variant's text colour. - * - * It is decorative and `aria-hidden`: the badge's own text is what a reader and - * a screen reader get, so the dot adds a glanceable mark without adding a - * second, colour-only channel that carries meaning of its own. That is the - * point of keeping it here rather than letting each caller prepend its own — - * a hand-rolled dot is where a status ends up encoded in colour alone. - */ +const dotSize = { sm: "size-1", default: "size-1.5", lg: "size-2" } as const; + function Badge({ className, variant = "default", + tone, + size = "default", dot = false, render, children, ...props }: useRender.ComponentProps<"span"> & VariantProps & { dot?: boolean }) { + // A tone replaces the variant's colours rather than layering over them: two + // fills on one badge resolve by class order, which is not a decision. + const resolvedSize = size ?? "default"; return useRender({ defaultTagName: "span", props: mergeProps<"span">( { - className: cn(badgeVariants({ variant }), className), + className: cn( + badgeVariants({ + variant: tone ? null : variant, + tone, + size: resolvedSize, + }), + className, + ), + // Decorative: the text is what a reader and a screen reader get. The + // dot gives a row of badges a shape to tell apart, so the status is + // never carried by colour alone. children: dot ? ( <> {children} @@ -68,7 +106,9 @@ function Badge({ render, state: { slot: "badge", - variant, + variant: tone ? undefined : variant, + tone, + size: resolvedSize, }, }); } diff --git a/module/services/frontend/code/packages/codefly-ui/src/layout/banner.tsx b/module/services/frontend/code/packages/codefly-ui/src/layout/banner.tsx index 335d0878d..1084489ee 100644 --- a/module/services/frontend/code/packages/codefly-ui/src/layout/banner.tsx +++ b/module/services/frontend/code/packages/codefly-ui/src/layout/banner.tsx @@ -1,7 +1,14 @@ "use client"; -import { X } from "lucide-react"; +import { + CircleCheckIcon, + InfoIcon, + OctagonAlertIcon, + TriangleAlertIcon, + X, +} from "lucide-react"; import type { ReactNode } from "react"; +import type { StatusTone } from "./badge.js"; import { Button } from "./button.js"; import { cn } from "./cn.js"; @@ -9,40 +16,85 @@ export interface BannerProps { title: ReactNode; children?: ReactNode; actions?: ReactNode; + /** + * What kind of news this is. `neutral` is the product speaking and keeps + * the banner's original look; the others are statuses, each with its own + * glyph so the kind is told by shape and not by colour alone. + */ + tone?: StatusTone; + /** Replaces the tone's glyph; `null` for none. */ + icon?: ReactNode; onDismiss?: () => void; dismissLabel?: string; dismissDisabled?: boolean; className?: string; } -/** Persistent, polite feedback. Callers own fetching, authorization and read state. */ +const toneClasses: Record = { + neutral: "border-primary/30 bg-primary/5", + info: "border-info/30 bg-info/5 [&_[data-slot=banner-icon]]:text-info", + success: + "border-success/30 bg-success/5 [&_[data-slot=banner-icon]]:text-success", + warning: + "border-warning/40 bg-warning/10 [&_[data-slot=banner-icon]]:text-warning", + danger: + "border-destructive/40 bg-destructive/10 [&_[data-slot=banner-icon]]:text-destructive", +}; + +const toneIcons: Record = { + neutral: null, + info: , + success: , + warning: , + danger: , +}; + +/** Persistent feedback. Callers own fetching, authorization and read state. */ export function Banner({ title, children, actions, + tone = "neutral", + icon, onDismiss, dismissLabel = "Dismiss notification", dismissDisabled, className, }: BannerProps) { + const glyph = icon === undefined ? toneIcons[tone] : icon; + // A fault interrupts; anything else waits for a pause in what is being read. + const assertive = tone === "danger"; return (
-
-
- {title} +
+ {glyph && ( + + {glyph} + + )} +
+
+ {title} +
+ {children &&
{children}
}
- {children &&
{children}
}
{(actions || onDismiss) && (
diff --git a/module/services/frontend/code/packages/codefly-ui/src/layout/chip.tsx b/module/services/frontend/code/packages/codefly-ui/src/layout/chip.tsx new file mode 100644 index 000000000..990884f33 --- /dev/null +++ b/module/services/frontend/code/packages/codefly-ui/src/layout/chip.tsx @@ -0,0 +1,156 @@ +import { useRender } from "@base-ui/react/use-render"; +import { XIcon } from "lucide-react"; +import { + Children, + isValidElement, + type MouseEventHandler, + type ReactElement, + type ReactNode, +} from "react"; + +import type { StatusTone } from "./badge.js"; +import { cn } from "./cn.js"; + +const toneClasses: Record = { + neutral: "border-border bg-background text-foreground", + success: "border-transparent bg-success/10 text-success dark:bg-success/20", + warning: "border-transparent bg-warning/10 text-warning dark:bg-warning/20", + danger: + "border-transparent bg-destructive/10 text-destructive dark:bg-destructive/20", + info: "border-transparent bg-info/10 text-info dark:bg-info/20", +}; + +type ChipRemove = + | { onRemove?: undefined; removeLabel?: undefined } + // The remove control has no text of its own, and "Remove" alone does not + // say which of a row of chips it removes. + | { onRemove: () => void; removeLabel: string }; + +export type ChipProps = { + /** The label. */ + children: ReactNode; + /** Renders the chip as a link. */ + href?: string; + /** Renders the chip as a button. */ + onClick?: MouseEventHandler; + /** Replaces the link or button, e.g. with a router link. */ + render?: ReactElement; + icon?: ReactNode; + /** Trailing, quieter text after a separator: "unread", a count, an owner. */ + meta?: ReactNode; + tone?: StatusTone; + className?: string; +} & ChipRemove; + +/** + * A badge a person can act on: follow, press, or remove. The remove control is + * a sibling of the action rather than inside it, because a button inside a + * link is two targets that assistive technology announces as one. + */ +export function Chip({ + children, + href, + onClick, + render, + icon, + meta, + tone = "neutral", + className, + onRemove, + removeLabel, +}: ChipProps) { + const interactive = render !== undefined || href !== undefined || !!onClick; + const action = useRender({ + defaultTagName: href !== undefined ? "a" : onClick ? "button" : "span", + render, + props: { + href, + onClick, + ...(href === undefined && onClick && !render + ? { type: "button" as const } + : {}), + "data-slot": "chip-action", + className: cn( + "inline-flex h-full min-w-0 items-center gap-1 rounded-4xl px-2.5 outline-none [&_svg]:size-3.5 [&_svg]:shrink-0", + interactive && + "cursor-pointer transition-colors hover:bg-foreground/5 focus-visible:ring-[3px] focus-visible:ring-ring/50", + onRemove && "pr-1", + ), + children: ( + <> + {icon && ( + + {icon} + + )} + {children} + {meta != null && ( + + · + {meta} + + )} + + ), + }, + }); + return ( + + {action} + {onRemove && ( + + )} + + ); +} + +export interface ChipGroupProps { + children: ReactNode; + /** Names the set ("Affects", "Owners") for a screen reader. */ + label?: string; + className?: string; +} + +/** A wrapping row of chips, announced as a list of them. */ +export function ChipGroup({ children, label, className }: ChipGroupProps) { + return ( + // `role="list"`: Safari drops list semantics from an unstyled `ul`. +
    + {Children.toArray(children).map((child, index) => ( +
  • + {child} +
  • + ))} +
+ ); +} diff --git a/module/services/frontend/code/packages/codefly-ui/src/layout/empty-state.tsx b/module/services/frontend/code/packages/codefly-ui/src/layout/empty-state.tsx index d89afbd87..5b85ee239 100644 --- a/module/services/frontend/code/packages/codefly-ui/src/layout/empty-state.tsx +++ b/module/services/frontend/code/packages/codefly-ui/src/layout/empty-state.tsx @@ -1,7 +1,8 @@ // A pure, tokens-driven empty-state block for solution pages and list surfaces. // It replaces the heading + one-line-of-guidance that pages compose inline with a // single consistent shape: a centered icon, heading, and description, with an -// optional trailing slot (children) for a call to action. No host context, no SDK, +// optional pointer elsewhere on the page (reference) and a trailing slot +// (children) for a call to action. No host context, no SDK, // no data fetching — so the host app and a solution's Module-Federation remote // render one shared instance. // @@ -17,20 +18,16 @@ export interface EmptyStateProps { /** Leading glyph; defaults to a generic inbox so the block always has one. */ icon?: ReactNode; heading: ReactNode; - /** - * The line under the heading. Takes nodes, not just a string, so an empty - * state whose explanation lives elsewhere on the page can **point at it** - * — "the source card above says where this stands", with a link if there is - * one to give. That is what this prop is for; `children` below is the - * action slot, and a cross-reference is not an action, so putting a - * sentence there (or hand-writing one outside the block) loses the - * `empty-state-description` slot and its width bound. - */ + /** The line under the heading: why there is nothing here. */ description?: ReactNode; /** - * Optional trailing content: a call to action, i.e. something to press. - * Prose belongs in `description`. + * Where the answer lives instead: "the source card above says where this + * stands", with a link if there is one. A pointer, not an explanation and + * not an action, so it is neither `description` nor `children`; written by + * hand outside the block it loses the slot and its width bound. */ + reference?: ReactNode; + /** A call to action: something to press. */ children?: ReactNode; className?: string; variant?: "default" | "illustrated"; @@ -41,6 +38,7 @@ export function EmptyState({ icon = , heading, description, + reference, children, className, variant = "default", @@ -80,7 +78,21 @@ export function EmptyState({ {heading} {description && ( -

{description}

+

+ {description} +

+ )} + {reference != null && ( +

+ {reference} +

)} {children}
diff --git a/module/services/frontend/code/packages/codefly-ui/src/layout/index.ts b/module/services/frontend/code/packages/codefly-ui/src/layout/index.ts index c8e110dc6..ab00891eb 100644 --- a/module/services/frontend/code/packages/codefly-ui/src/layout/index.ts +++ b/module/services/frontend/code/packages/codefly-ui/src/layout/index.ts @@ -28,8 +28,14 @@ export { AvatarImage, } from "./avatar.js"; // Data display -export { Badge, badgeVariants } from "./badge.js"; +export { Badge, badgeVariants, type StatusTone } from "./badge.js"; export { Banner, type BannerProps } from "./banner.js"; +export { + Chip, + ChipGroup, + type ChipGroupProps, + type ChipProps, +} from "./chip.js"; // Actions export { Button, buttonVariants } from "./button.js"; @@ -126,6 +132,15 @@ export { InputGroupTextarea, } from "./input-group.js"; export { Label } from "./label.js"; +export { + DescriptionList, + type DescriptionListItem, + type DescriptionListProps, + List, + ListItem, + type ListItemProps, + type ListProps, +} from "./list.js"; // A floating, non-modal notice that asks for a decision. Its `escape` is // required, so it cannot trap the user behind it. export { diff --git a/module/services/frontend/code/packages/codefly-ui/src/layout/list.tsx b/module/services/frontend/code/packages/codefly-ui/src/layout/list.tsx new file mode 100644 index 000000000..2eacbc163 --- /dev/null +++ b/module/services/frontend/code/packages/codefly-ui/src/layout/list.tsx @@ -0,0 +1,168 @@ +// Lists for the places a `Table` is too heavy: a handful of rows with one or +// two things to say each (diagnostics, a relation list), and term/value pairs +// (a front-matter card). Pure and server-safe, like Card and Section. + +import type { ReactNode } from "react"; +import { cn } from "./cn.js"; + +export interface ListProps { + children: ReactNode; + /** `divided` rules a line between rows, for rows that each carry a description. */ + variant?: "plain" | "divided"; + /** Names the list for a screen reader when no heading above it does. */ + label?: string; + className?: string; +} + +export function List({ + children, + variant = "plain", + label, + className, +}: ListProps) { + return ( + // `role="list"`: Safari drops list semantics from an unstyled `ul`. +
    li]:py-2 [&>li:first-child]:pt-0 [&>li:last-child]:pb-0", + className, + )} + > + {children} +
+ ); +} + +export interface ListItemProps { + /** The row's primary text. */ + children: ReactNode; + icon?: ReactNode; + description?: ReactNode; + /** Trailing, quieter text: a time, a count, a badge. */ + meta?: ReactNode; + /** Trailing controls, after `meta`. */ + actions?: ReactNode; + className?: string; +} + +export function ListItem({ + children, + icon, + description, + meta, + actions, + className, +}: ListItemProps) { + return ( +
  • + {icon && ( + + {icon} + + )} +
    +
    + {children} +
    + {description != null && ( +
    + {description} +
    + )} +
    + {meta != null && ( +
    + {meta} +
    + )} + {actions != null && ( +
    + {actions} +
    + )} +
  • + ); +} + +export interface DescriptionListItem { + term: ReactNode; + value: ReactNode; + /** Needed only when the list reorders; two rows may share a term. */ + key?: string; +} + +export interface DescriptionListProps { + items: readonly DescriptionListItem[]; + /** + * `inline` puts each term beside its value in two aligned columns, for a + * card of short facts; `stacked` puts the term above, for values that wrap. + */ + layout?: "inline" | "stacked"; + className?: string; +} + +export function DescriptionList({ + items, + layout = "inline", + className, +}: DescriptionListProps) { + return ( +
    + {items.map((item, index) => ( + // A `div` around each pair is valid inside `dl`; `contents` lets the + // inline layout's two columns align across every pair. +
    +
    + {item.term} +
    +
    + {item.value} +
    +
    + ))} +
    + ); +} diff --git a/module/services/frontend/code/packages/codefly-ui/src/skin/appearance.ts b/module/services/frontend/code/packages/codefly-ui/src/skin/appearance.ts new file mode 100644 index 000000000..0dbfb0447 --- /dev/null +++ b/module/services/frontend/code/packages/codefly-ui/src/skin/appearance.ts @@ -0,0 +1,130 @@ +// The projection of a resolved skin onto custom properties: what the host puts +// on at SSR and what `preview.css` bakes in for the default skin. Here, +// beside the token layer (theme.css) and the generated type utilities, because +// the three only mean something together — a variable the projection names and +// the stylesheet never reads, or the reverse, styles nothing and fails nowhere. +import { + FRONTEND_APPEARANCE_TOKEN_NAMES, + FRONTEND_CONTROL_SIZE_NAMES, + FRONTEND_TYPE_SLOT_NAMES, + type FrontendAppearance, + type FrontendAppearanceTokenName, + type ResolvedTypeSlot, + resolveTypeRole, + resolveTypeSlot, +} from "@codefly/saas-plugin-contract"; +import type { CSSProperties } from "react"; + +/** + * The custom properties a resolved skin puts on ``: the appearance tokens, + * plus the flattened type slots and control rungs the generated utilities read. + * A property a slot's role does not decide is absent rather than empty, so every + * key is optional. + */ +export type AppearanceStyleProperties = CSSProperties & + Partial< + Record< + `--appearance-${string}` | `--type-${string}` | `--control-${string}`, + string + > + >; + +export function appearanceVariableName( + mode: "light" | "dark", + token: FrontendAppearanceTokenName, +): `--appearance-${"light" | "dark"}-${string}` { + return `--appearance-${mode}-${token.replace(/[A-Z]/g, (letter) => `-${letter.toLowerCase()}`)}`; +} + +/** + * Produces the SSR-safe custom properties consumed by theme.css. Keeping + * both palettes on lets the client theme provider switch one class + * without recomputing the tenant appearance tokens. + */ +export function appearanceStyleProperties( + appearance: FrontendAppearance, +): AppearanceStyleProperties { + const properties: Record = { + // A behaviour, not a token: DateField reads it to choose its shape. It + // rides the same channel as the tokens so the explorer and SSR agree. + "--appearance-date-pattern": appearance.datePattern, + "--appearance-radius": appearance.radius, + "--appearance-font-sans": appearance.fontSans, + "--appearance-font-heading": appearance.fontHeading, + "--appearance-font-mono": appearance.fontMono, + "--appearance-spacing": appearance.spacing, + "--appearance-font-size-base": appearance.fontSizeBase, + "--appearance-sidebar-width": appearance.sidebarWidth, + "--appearance-sidebar-width-icon": appearance.sidebarWidthIcon, + "--appearance-border-width": appearance.borderWidth, + "--appearance-shadow-strength": appearance.shadowStrength, + }; + // Optional values are projected only when the skin decided them; the + // stylesheet's `var(--…, fallback)` derives the rest. `--appearance-…-disabled + // -opacity` is the presence signal for the disabled fill: a decided fill + // renders flat at full opacity, an absent one keeps the faded variant. + if (appearance.buttonRadius !== undefined) + properties["--appearance-button-radius"] = appearance.buttonRadius; + for (const mode of ["light", "dark"] as const) { + for (const token of FRONTEND_APPEARANCE_TOKEN_NAMES) { + const value = appearance[mode][token]; + if (value !== undefined) + properties[appearanceVariableName(mode, token)] = value; + } + if (appearance[mode].disabled !== undefined) + properties[`--appearance-${mode}-disabled-opacity`] = "1"; + } + // Layers 1 to 3, flattened. The kit's generated stylesheet declares one + // utility per slot reading exactly the variables the slot's role decides, and + // this projects exactly those: both derive the set from the same contract, + // which refuses a skin that would change it. A property the role does not + // decide is therefore absent on both sides — never declared, never projected + // — so the element keeps whatever else styles it, as it did before slots. + for (const slot of FRONTEND_TYPE_SLOT_NAMES) + writeTypeProperties( + properties, + `--type-${slot}`, + resolveTypeSlot(appearance, slot), + ); + for (const size of FRONTEND_CONTROL_SIZE_NAMES) { + const rung = appearance.controlSizes[size]; + // Geometry is stored as `--spacing` multiples, not lengths, so a skin that + // changes density moves control geometry with it instead of pinning it. + properties[`--control-${size}-height`] = spacingUnits(rung.height); + properties[`--control-${size}-padding-x`] = spacingUnits(rung.paddingX); + properties[`--control-${size}-icon`] = spacingUnits(rung.icon); + writeTypeProperties( + properties, + `--control-${size}-text`, + resolveTypeRole(appearance, rung.text), + ); + } + return properties as AppearanceStyleProperties; +} + +/** `8` becomes `calc(var(--spacing) * 8)`, matching Tailwind's own sizing utilities. */ +function spacingUnits(multiple: string): string { + return `calc(var(--spacing) * ${multiple})`; +} + +function writeTypeProperties( + properties: Record, + prefix: string, + resolved: ResolvedTypeSlot, +): void { + if (resolved.fontSize !== undefined) + properties[`${prefix}-size`] = resolved.fontSize; + if (resolved.fontWeight !== undefined) + properties[`${prefix}-weight`] = resolved.fontWeight; + if (resolved.lineHeight !== undefined) + properties[`${prefix}-leading`] = resolved.lineHeight; + if (resolved.letterSpacing !== undefined) + properties[`${prefix}-tracking`] = resolved.letterSpacing; + if (resolved.fontFamily !== undefined) + properties[`${prefix}-family`] = + resolved.fontFamily === "heading" + ? "var(--font-heading)" + : resolved.fontFamily === "mono" + ? "var(--font-mono)" + : "var(--font-sans)"; +} diff --git a/module/services/frontend/code/packages/codefly-ui/src/skin/index.ts b/module/services/frontend/code/packages/codefly-ui/src/skin/index.ts index eb7d29989..24d868e79 100644 --- a/module/services/frontend/code/packages/codefly-ui/src/skin/index.ts +++ b/module/services/frontend/code/packages/codefly-ui/src/skin/index.ts @@ -1,3 +1,8 @@ +export { + type AppearanceStyleProperties, + appearanceStyleProperties, + appearanceVariableName, +} from "./appearance.js"; export { CACHE_MAX_ENTRIES, clearSkinCache, diff --git a/module/services/frontend/code/packages/codefly-ui/src/skin/preview.generated.css b/module/services/frontend/code/packages/codefly-ui/src/skin/preview.generated.css new file mode 100644 index 000000000..296a447f5 --- /dev/null +++ b/module/services/frontend/code/packages/codefly-ui/src/skin/preview.generated.css @@ -0,0 +1,6241 @@ +/* Generated by @codefly-dev/ui scripts/generate-preview-stylesheet.mjs — do not edit. + * + * The kit's components compiled with its token layer (theme.css) and type + * utilities, painted with the DEFAULT skin. Import it once at the root of a + * preview so kit components render without a running host. + * + * May be used for: a solution's local preview, its component stories, and the + * screenshots or visual tests taken from them. + * + * May not be used for: + * - Anything a host loads. A solution's remote renders inside the host's + * document under the host's one compiled stylesheet and the deployment's + * skin. Bundled into a remote, this ships a second Tailwind preflight and the + * default skin's values into that document. + * - Evidence of how a deployment looks. It is the default skin; a deployment's + * skin changes colour, type, radius and density. + * - Styling anything but kit components. Only classes the kit's own source + * writes are here, so a hand-laid utility in a solution renders unstyled in + * preview — the kit is what a solution page is built from. + * + * Dark mode: put class="dark" on an ancestor, as the host does. + */ + +/*! tailwindcss v4.3.3 | MIT License | https://tailwindcss.com */ +@layer properties { + @supports (((-webkit-hyphens: none)) and (not (margin-trim: inline))) or ((-moz-orient: inline) and (not (color: rgb(from red r g b)))) { + *, :before, :after, ::backdrop { + --tw-translate-x: 0; + --tw-translate-y: 0; + --tw-translate-z: 0; + --tw-rotate-x: initial; + --tw-rotate-y: initial; + --tw-rotate-z: initial; + --tw-skew-x: initial; + --tw-skew-y: initial; + --tw-space-y-reverse: 0; + --tw-space-x-reverse: 0; + --tw-divide-y-reverse: 0; + --tw-border-style: solid; + --tw-gradient-position: initial; + --tw-gradient-from: #0000; + --tw-gradient-via: #0000; + --tw-gradient-to: #0000; + --tw-gradient-stops: initial; + --tw-gradient-via-stops: initial; + --tw-gradient-from-position: 0%; + --tw-gradient-via-position: 50%; + --tw-gradient-to-position: 100%; + --tw-ordinal: initial; + --tw-slashed-zero: initial; + --tw-numeric-figure: initial; + --tw-numeric-spacing: initial; + --tw-numeric-fraction: initial; + --tw-shadow: 0 0 #0000; + --tw-shadow-color: initial; + --tw-shadow-alpha: 100%; + --tw-inset-shadow: 0 0 #0000; + --tw-inset-shadow-color: initial; + --tw-inset-shadow-alpha: 100%; + --tw-ring-color: initial; + --tw-ring-shadow: 0 0 #0000; + --tw-inset-ring-color: initial; + --tw-inset-ring-shadow: 0 0 #0000; + --tw-ring-inset: initial; + --tw-ring-offset-width: 0px; + --tw-ring-offset-color: #fff; + --tw-ring-offset-shadow: 0 0 #0000; + --tw-outline-style: solid; + --tw-blur: initial; + --tw-brightness: initial; + --tw-contrast: initial; + --tw-grayscale: initial; + --tw-hue-rotate: initial; + --tw-invert: initial; + --tw-opacity: initial; + --tw-saturate: initial; + --tw-sepia: initial; + --tw-drop-shadow: initial; + --tw-drop-shadow-color: initial; + --tw-drop-shadow-alpha: 100%; + --tw-drop-shadow-size: initial; + --tw-duration: initial; + --tw-ease: initial; + --tw-content: ""; + --tw-backdrop-blur: initial; + --tw-backdrop-brightness: initial; + --tw-backdrop-contrast: initial; + --tw-backdrop-grayscale: initial; + --tw-backdrop-hue-rotate: initial; + --tw-backdrop-invert: initial; + --tw-backdrop-opacity: initial; + --tw-backdrop-saturate: initial; + --tw-backdrop-sepia: initial; + --tw-animation-delay: 0s; + --tw-animation-direction: normal; + --tw-animation-duration: initial; + --tw-animation-fill-mode: none; + --tw-animation-iteration-count: 1; + --tw-enter-blur: 0; + --tw-enter-opacity: 1; + --tw-enter-rotate: 0; + --tw-enter-scale: 1; + --tw-enter-translate-x: 0; + --tw-enter-translate-y: 0; + --tw-exit-blur: 0; + --tw-exit-opacity: 1; + --tw-exit-rotate: 0; + --tw-exit-scale: 1; + --tw-exit-translate-x: 0; + --tw-exit-translate-y: 0; + } + } +} + +@layer theme { + :root, :host { + --font-sans: var(--appearance-font-sans); + --font-mono: var(--appearance-font-mono); + --color-amber-400: oklch(82.8% .189 84.429); + --color-amber-500: oklch(76.9% .188 70.08); + --color-black: #000; + --spacing: var(--appearance-spacing); + --container-xs: 20rem; + --container-sm: 24rem; + --container-lg: 32rem; + --container-7xl: 80rem; + --text-lg: 1.125rem; + --text-lg--line-height: calc(1.75 / 1.125); + --radius-md: calc(var(--radius) * .8); + --radius-lg: var(--radius); + --drop-shadow-lg: 0 4px 4px #00000026; + --ease-out: cubic-bezier(0, 0, .2, 1); + --ease-in-out: cubic-bezier(.4, 0, .2, 1); + --animate-spin: spin 1s linear infinite; + --animate-pulse: pulse 2s cubic-bezier(.4, 0, .6, 1) infinite; + --blur-xs: 4px; + --blur-2xl: 40px; + --default-transition-duration: .15s; + --default-transition-timing-function: cubic-bezier(.4, 0, .2, 1); + --default-font-family: var(--appearance-font-sans); + --default-mono-font-family: var(--appearance-font-mono); + --font-heading: var(--appearance-font-heading); + } +} + +@layer base { + *, :after, :before, ::backdrop { + box-sizing: border-box; + border: 0 solid; + margin: 0; + padding: 0; + } + + ::file-selector-button { + box-sizing: border-box; + border: 0 solid; + margin: 0; + padding: 0; + } + + html, :host { + -webkit-text-size-adjust: 100%; + tab-size: 4; + line-height: 1.5; + font-family: var(--default-font-family, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", "Noto Sans", Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"); + font-feature-settings: var(--default-font-feature-settings, normal); + font-variation-settings: var(--default-font-variation-settings, normal); + -webkit-tap-highlight-color: transparent; + } + + hr { + height: 0; + color: inherit; + border-top-width: 1px; + } + + abbr:where([title]) { + -webkit-text-decoration: underline dotted; + text-decoration: underline dotted; + } + + h1, h2, h3, h4, h5, h6 { + font-size: inherit; + font-weight: inherit; + } + + a { + color: inherit; + -webkit-text-decoration: inherit; + -webkit-text-decoration: inherit; + -webkit-text-decoration: inherit; + text-decoration: inherit; + } + + b, strong { + font-weight: bolder; + } + + code, kbd, samp, pre { + font-family: var(--default-mono-font-family, ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace); + font-feature-settings: var(--default-mono-font-feature-settings, normal); + font-variation-settings: var(--default-mono-font-variation-settings, normal); + font-size: 1em; + } + + small { + font-size: 80%; + } + + sub, sup { + vertical-align: baseline; + font-size: 75%; + line-height: 0; + position: relative; + } + + sub { + bottom: -.25em; + } + + sup { + top: -.5em; + } + + table { + text-indent: 0; + border-color: inherit; + border-collapse: collapse; + } + + :-moz-focusring:where(:not(iframe)) { + outline: auto; + } + + progress { + vertical-align: baseline; + } + + summary { + display: list-item; + } + + ol, ul, menu { + list-style: none; + } + + img, svg, video, canvas, audio, iframe, embed, object { + vertical-align: middle; + display: block; + } + + img, video { + max-width: 100%; + height: auto; + } + + button, input, select, optgroup, textarea { + font: inherit; + font-feature-settings: inherit; + font-variation-settings: inherit; + letter-spacing: inherit; + color: inherit; + opacity: 1; + background-color: #0000; + border-radius: 0; + } + + ::file-selector-button { + font: inherit; + font-feature-settings: inherit; + font-variation-settings: inherit; + letter-spacing: inherit; + color: inherit; + opacity: 1; + background-color: #0000; + border-radius: 0; + } + + :where(select:is([multiple], [size])) optgroup { + font-weight: bolder; + } + + :where(select:is([multiple], [size])) optgroup option { + padding-inline-start: 20px; + } + + ::file-selector-button { + margin-inline-end: 4px; + } + + ::placeholder { + opacity: 1; + } + + @supports (not ((-webkit-appearance: -apple-pay-button))) or (contain-intrinsic-size: 1px) { + ::placeholder { + color: currentColor; + } + + @supports (color: color-mix(in lab, red, red)) { + ::placeholder { + color: color-mix(in oklab, currentcolor 50%, transparent); + } + } + } + + textarea { + resize: vertical; + } + + ::-webkit-search-decoration { + -webkit-appearance: none; + } + + ::-webkit-date-and-time-value { + min-height: 1lh; + text-align: inherit; + } + + ::-webkit-datetime-edit { + display: inline-flex; + } + + ::-webkit-datetime-edit-fields-wrapper { + padding: 0; + } + + ::-webkit-datetime-edit { + padding-block: 0; + } + + ::-webkit-datetime-edit-year-field { + padding-block: 0; + } + + ::-webkit-datetime-edit-month-field { + padding-block: 0; + } + + ::-webkit-datetime-edit-day-field { + padding-block: 0; + } + + ::-webkit-datetime-edit-hour-field { + padding-block: 0; + } + + ::-webkit-datetime-edit-minute-field { + padding-block: 0; + } + + ::-webkit-datetime-edit-second-field { + padding-block: 0; + } + + ::-webkit-datetime-edit-millisecond-field { + padding-block: 0; + } + + ::-webkit-datetime-edit-meridiem-field { + padding-block: 0; + } + + ::-webkit-calendar-picker-indicator { + line-height: 1; + } + + :-moz-ui-invalid { + box-shadow: none; + } + + button, input:where([type="button"], [type="reset"], [type="submit"]) { + appearance: button; + } + + ::file-selector-button { + appearance: button; + } + + ::-webkit-inner-spin-button { + height: auto; + } + + ::-webkit-outer-spin-button { + height: auto; + } + + [hidden]:where(:not([hidden="until-found"])) { + display: none !important; + } + + * { + border-color: var(--border); + outline-color: var(--ring); + } + + @supports (color: color-mix(in lab, red, red)) { + * { + outline-color: color-mix(in oklab, var(--ring) 50%, transparent); + } + } + + body { + background-color: var(--background); + color: var(--foreground); + } + + html { + font-family: var(--appearance-font-sans); + font-size: var(--appearance-font-size-base); + } +} + +@layer components; + +@layer utilities { + .\@container\/card-header { + container: card-header / inline-size; + } + + .pointer-events-none { + pointer-events: none; + } + + .collapse { + visibility: collapse; + } + + .visible { + visibility: visible; + } + + .sr-only { + clip-path: inset(50%); + white-space: nowrap; + border-width: 0; + width: 1px; + height: 1px; + margin: -1px; + padding: 0; + position: absolute; + overflow: hidden; + } + + .absolute { + position: absolute; + } + + .fixed { + position: fixed; + } + + .relative { + position: relative; + } + + .static { + position: static; + } + + .sticky { + position: sticky; + } + + .inset-0 { + inset: 0; + } + + .inset-y-0 { + inset-block: 0; + } + + .top-0 { + top: 0; + } + + .top-1 { + top: var(--appearance-spacing); + } + + .top-1\.5 { + top: calc(var(--appearance-spacing) * 1.5); + } + + .top-1\/2 { + top: 50%; + } + + .top-1\/3 { + top: 33.3333%; + } + + .top-2 { + top: calc(var(--appearance-spacing) * 2); + } + + .top-3 { + top: calc(var(--appearance-spacing) * 3); + } + + .top-3\.5 { + top: calc(var(--appearance-spacing) * 3.5); + } + + .right-0 { + right: 0; + } + + .right-1 { + right: var(--appearance-spacing); + } + + .right-2 { + right: calc(var(--appearance-spacing) * 2); + } + + .right-3 { + right: calc(var(--appearance-spacing) * 3); + } + + .right-4 { + right: calc(var(--appearance-spacing) * 4); + } + + .bottom-0 { + bottom: 0; + } + + .bottom-4 { + bottom: calc(var(--appearance-spacing) * 4); + } + + .left-0 { + left: 0; + } + + .left-1\/2 { + left: 50%; + } + + .left-4 { + left: calc(var(--appearance-spacing) * 4); + } + + .isolate { + isolation: isolate; + } + + .-z-10 { + z-index: calc(10 * -1); + } + + .z-10 { + z-index: 10; + } + + .z-20 { + z-index: 20; + } + + .z-50 { + z-index: 50; + } + + .order-first { + order: -9999; + } + + .order-last { + order: 9999; + } + + .col-start-2 { + grid-column-start: 2; + } + + .row-span-2 { + grid-row: span 2 / span 2; + } + + .row-start-1 { + grid-row-start: 1; + } + + .container { + width: 100%; + } + + @media (min-width: 40rem) { + .container { + max-width: 40rem; + } + } + + @media (min-width: 48rem) { + .container { + max-width: 48rem; + } + } + + @media (min-width: 64rem) { + .container { + max-width: 64rem; + } + } + + @media (min-width: 80rem) { + .container { + max-width: 80rem; + } + } + + @media (min-width: 96rem) { + .container { + max-width: 96rem; + } + } + + .m-auto { + margin: auto; + } + + .-mx-1 { + margin-inline: calc(var(--appearance-spacing) * -1); + } + + .-mx-4 { + margin-inline: calc(var(--appearance-spacing) * -4); + } + + .mx-2 { + margin-inline: calc(var(--appearance-spacing) * 2); + } + + .mx-3\.5 { + margin-inline: calc(var(--appearance-spacing) * 3.5); + } + + .mx-auto { + margin-inline: auto; + } + + .my-1 { + margin-block: var(--appearance-spacing); + } + + .my-2 { + margin-block: calc(var(--appearance-spacing) * 2); + } + + .my-4 { + margin-block: calc(var(--appearance-spacing) * 4); + } + + .-mt-1 { + margin-top: calc(var(--appearance-spacing) * -1); + } + + .mt-0\.5 { + margin-top: calc(var(--appearance-spacing) * .5); + } + + .mt-1 { + margin-top: var(--appearance-spacing); + } + + .mt-4 { + margin-top: calc(var(--appearance-spacing) * 4); + } + + .mt-auto { + margin-top: auto; + } + + .-mr-8 { + margin-right: calc(var(--appearance-spacing) * -8); + } + + .mr-1 { + margin-right: var(--appearance-spacing); + } + + .mr-auto { + margin-right: auto; + } + + .-mb-4 { + margin-bottom: calc(var(--appearance-spacing) * -4); + } + + .mb-1 { + margin-bottom: var(--appearance-spacing); + } + + .mb-2 { + margin-bottom: calc(var(--appearance-spacing) * 2); + } + + .mb-3 { + margin-bottom: calc(var(--appearance-spacing) * 3); + } + + .ml-0\.5 { + margin-left: calc(var(--appearance-spacing) * .5); + } + + .ml-1 { + margin-left: var(--appearance-spacing); + } + + .ml-2 { + margin-left: calc(var(--appearance-spacing) * 2); + } + + .ml-5 { + margin-left: calc(var(--appearance-spacing) * 5); + } + + .ml-auto { + margin-left: auto; + } + + .no-scrollbar { + -ms-overflow-style: none; + scrollbar-width: none; + } + + .no-scrollbar::-webkit-scrollbar { + display: none; + } + + .line-clamp-2 { + -webkit-line-clamp: 2; + -webkit-box-orient: vertical; + display: -webkit-box; + overflow: hidden; + } + + .line-clamp-3 { + -webkit-line-clamp: 3; + -webkit-box-orient: vertical; + display: -webkit-box; + overflow: hidden; + } + + .block { + display: block; + } + + .contents { + display: contents; + } + + .flex { + display: flex; + } + + .grid { + display: grid; + } + + .hidden { + display: none; + } + + .inline { + display: inline; + } + + .inline-block { + display: inline-block; + } + + .inline-flex { + display: inline-flex; + } + + .list-item { + display: list-item; + } + + .table { + display: table; + } + + .table-caption { + display: table-caption; + } + + .table-cell { + display: table-cell; + } + + .table-row { + display: table-row; + } + + .field-sizing-content { + field-sizing: content; + } + + .aspect-square { + aspect-ratio: 1; + } + + :where(.control-icon-default) { + height: var(--control-default-height); + width: var(--control-default-height); + font-size: var(--control-default-text-size); + font-weight: var(--control-default-text-weight); + line-height: var(--control-default-text-leading); + padding-inline: 0; + } + + :where(.control-icon-lg) { + height: var(--control-lg-height); + width: var(--control-lg-height); + font-size: var(--control-lg-text-size); + font-weight: var(--control-lg-text-weight); + line-height: var(--control-lg-text-leading); + padding-inline: 0; + } + + :where(.control-icon-sm) { + height: var(--control-sm-height); + width: var(--control-sm-height); + font-size: var(--control-sm-text-size); + font-weight: var(--control-sm-text-weight); + line-height: var(--control-sm-text-leading); + padding-inline: 0; + } + + :where(.control-icon-xs) { + height: var(--control-xs-height); + width: var(--control-xs-height); + font-size: var(--control-xs-text-size); + font-weight: var(--control-xs-text-weight); + line-height: var(--control-xs-text-leading); + padding-inline: 0; + } + + .size-1 { + width: var(--appearance-spacing); + height: var(--appearance-spacing); + } + + .size-1\.5 { + width: calc(var(--appearance-spacing) * 1.5); + height: calc(var(--appearance-spacing) * 1.5); + } + + .size-2 { + width: calc(var(--appearance-spacing) * 2); + height: calc(var(--appearance-spacing) * 2); + } + + .size-2\.5 { + width: calc(var(--appearance-spacing) * 2.5); + height: calc(var(--appearance-spacing) * 2.5); + } + + .size-3 { + width: calc(var(--appearance-spacing) * 3); + height: calc(var(--appearance-spacing) * 3); + } + + .size-4 { + width: calc(var(--appearance-spacing) * 4); + height: calc(var(--appearance-spacing) * 4); + } + + .size-6 { + width: calc(var(--appearance-spacing) * 6); + height: calc(var(--appearance-spacing) * 6); + } + + .size-8 { + width: calc(var(--appearance-spacing) * 8); + height: calc(var(--appearance-spacing) * 8); + } + + .size-10 { + width: calc(var(--appearance-spacing) * 10); + height: calc(var(--appearance-spacing) * 10); + } + + .size-14 { + width: calc(var(--appearance-spacing) * 14); + height: calc(var(--appearance-spacing) * 14); + } + + .size-full { + width: 100%; + height: 100%; + } + + :where(.control-default) { + height: var(--control-default-height); + padding-inline: var(--control-default-padding-x); + font-size: var(--control-default-text-size); + font-weight: var(--control-default-text-weight); + line-height: var(--control-default-text-leading); + } + + :where(.control-lg) { + height: var(--control-lg-height); + padding-inline: var(--control-lg-padding-x); + font-size: var(--control-lg-text-size); + font-weight: var(--control-lg-text-weight); + line-height: var(--control-lg-text-leading); + } + + :where(.control-sm) { + height: var(--control-sm-height); + padding-inline: var(--control-sm-padding-x); + font-size: var(--control-sm-text-size); + font-weight: var(--control-sm-text-weight); + line-height: var(--control-sm-text-leading); + } + + :where(.control-xs) { + height: var(--control-xs-height); + padding-inline: var(--control-xs-padding-x); + font-size: var(--control-xs-text-size); + font-weight: var(--control-xs-text-weight); + line-height: var(--control-xs-text-leading); + } + + :where(.control-height-default) { + height: var(--control-default-height); + } + + :where(.control-height-lg) { + height: var(--control-lg-height); + } + + :where(.control-height-sm) { + height: var(--control-sm-height); + } + + :where(.control-height-xs) { + height: var(--control-xs-height); + } + + .h-0 { + height: 0; + } + + .h-0\.5 { + height: calc(var(--appearance-spacing) * .5); + } + + .h-2 { + height: calc(var(--appearance-spacing) * 2); + } + + .h-2\.5 { + height: calc(var(--appearance-spacing) * 2.5); + } + + .h-4 { + height: calc(var(--appearance-spacing) * 4); + } + + .h-5 { + height: calc(var(--appearance-spacing) * 5); + } + + .h-6 { + height: calc(var(--appearance-spacing) * 6); + } + + .h-8 { + height: calc(var(--appearance-spacing) * 8); + } + + .h-8\! { + height: calc(var(--appearance-spacing) * 8) !important; + } + + .h-10 { + height: calc(var(--appearance-spacing) * 10); + } + + .h-11 { + height: calc(var(--appearance-spacing) * 11); + } + + .h-12 { + height: calc(var(--appearance-spacing) * 12); + } + + .h-24 { + height: calc(var(--appearance-spacing) * 24); + } + + .h-\[calc\(100\%-1px\)\] { + height: calc(100% - 1px); + } + + .h-auto { + height: auto; + } + + .h-full { + height: 100%; + } + + .h-px { + height: 1px; + } + + .h-svh { + height: 100svh; + } + + .max-h-\(--available-height\) { + max-height: var(--available-height); + } + + .max-h-72 { + max-height: calc(var(--appearance-spacing) * 72); + } + + .min-h-0 { + min-height: 0; + } + + .min-h-9 { + min-height: calc(var(--appearance-spacing) * 9); + } + + .min-h-16 { + min-height: calc(var(--appearance-spacing) * 16); + } + + .min-h-svh { + min-height: 100svh; + } + + .w-\(--anchor-width\) { + width: var(--anchor-width); + } + + .w-\(--sidebar-width\) { + width: var(--sidebar-width); + } + + .w-1\.5 { + width: calc(var(--appearance-spacing) * 1.5); + } + + .w-2\.5 { + width: calc(var(--appearance-spacing) * 2.5); + } + + .w-3 { + width: calc(var(--appearance-spacing) * 3); + } + + .w-4 { + width: calc(var(--appearance-spacing) * 4); + } + + .w-5 { + width: calc(var(--appearance-spacing) * 5); + } + + .w-8 { + width: calc(var(--appearance-spacing) * 8); + } + + .w-10 { + width: calc(var(--appearance-spacing) * 10); + } + + .w-12 { + width: calc(var(--appearance-spacing) * 12); + } + + .w-16 { + width: calc(var(--appearance-spacing) * 16); + } + + .w-24 { + width: calc(var(--appearance-spacing) * 24); + } + + .w-32 { + width: calc(var(--appearance-spacing) * 32); + } + + .w-72 { + width: calc(var(--appearance-spacing) * 72); + } + + .w-\[120px\] { + width: 120px; + } + + .w-auto { + width: auto; + } + + .w-fit { + width: fit-content; + } + + .w-full { + width: 100%; + } + + .max-w-\(--available-width\) { + max-width: var(--available-width); + } + + .max-w-\(--skeleton-width\) { + max-width: var(--skeleton-width); + } + + .max-w-7xl { + max-width: var(--container-7xl); + } + + .max-w-\[75\%\] { + max-width: 75%; + } + + .max-w-\[calc\(100\%-2rem\)\] { + max-width: calc(100% - 2rem); + } + + .max-w-full { + max-width: 100%; + } + + .max-w-lg { + max-width: var(--container-lg); + } + + .max-w-sm { + max-width: var(--container-sm); + } + + .max-w-xs { + max-width: var(--container-xs); + } + + .min-w-0 { + min-width: 0; + } + + .min-w-5 { + min-width: calc(var(--appearance-spacing) * 5); + } + + .min-w-32 { + min-width: calc(var(--appearance-spacing) * 32); + } + + .min-w-36 { + min-width: calc(var(--appearance-spacing) * 36); + } + + .min-w-\[96px\] { + min-width: 96px; + } + + .flex-1 { + flex: 1; + } + + .shrink-0 { + flex-shrink: 0; + } + + .grow { + flex-grow: 1; + } + + .caption-bottom { + caption-side: bottom; + } + + .border-collapse { + border-collapse: collapse; + } + + .origin-\(--transform-origin\) { + transform-origin: var(--transform-origin); + } + + .-translate-x-1\/2 { + --tw-translate-x: calc(calc(1 / 2 * 100%) * -1); + translate: var(--tw-translate-x) var(--tw-translate-y); + } + + .-translate-x-px { + --tw-translate-x: -1px; + translate: var(--tw-translate-x) var(--tw-translate-y); + } + + .translate-x-px { + --tw-translate-x: 1px; + translate: var(--tw-translate-x) var(--tw-translate-y); + } + + .-translate-y-1\/2 { + --tw-translate-y: calc(calc(1 / 2 * 100%) * -1); + translate: var(--tw-translate-x) var(--tw-translate-y); + } + + .translate-y-0 { + --tw-translate-y: 0px; + translate: var(--tw-translate-x) var(--tw-translate-y); + } + + .translate-y-\[calc\(-50\%-2px\)\] { + --tw-translate-y: calc(-50% - 2px); + translate: var(--tw-translate-x) var(--tw-translate-y); + } + + .rotate-45 { + rotate: 45deg; + } + + .rotate-90 { + rotate: 90deg; + } + + .transform { + transform: var(--tw-rotate-x, ) var(--tw-rotate-y, ) var(--tw-rotate-z, ) var(--tw-skew-x, ) var(--tw-skew-y, ); + } + + .animate-pulse { + animation: var(--animate-pulse); + } + + .animate-spin { + animation: var(--animate-spin); + } + + .cursor-default { + cursor: default; + } + + .cursor-grab { + cursor: grab; + } + + .cursor-grabbing { + cursor: grabbing; + } + + .cursor-pointer { + cursor: pointer; + } + + .cursor-text { + cursor: text; + } + + .touch-manipulation { + touch-action: manipulation; + } + + .resize-none { + resize: none; + } + + .scroll-my-1 { + scroll-margin-block: var(--appearance-spacing); + } + + .scroll-py-1 { + scroll-padding-block: var(--appearance-spacing); + } + + .list-decimal { + list-style-type: decimal; + } + + .list-disc { + list-style-type: disc; + } + + .list-none { + list-style-type: none; + } + + .auto-rows-min { + grid-auto-rows: min-content; + } + + .grid-cols-1 { + grid-template-columns: repeat(1, minmax(0, 1fr)); + } + + .grid-cols-2 { + grid-template-columns: repeat(2, minmax(0, 1fr)); + } + + .grid-cols-\[minmax\(0\,max-content\)_minmax\(0\,1fr\)\] { + grid-template-columns: minmax(0, max-content) minmax(0, 1fr); + } + + .grid-rows-\[auto_1fr\] { + grid-template-rows: auto 1fr; + } + + .flex-col { + flex-direction: column; + } + + .flex-col-reverse { + flex-direction: column-reverse; + } + + .flex-row { + flex-direction: row; + } + + .flex-row-reverse { + flex-direction: row-reverse; + } + + .flex-wrap { + flex-wrap: wrap; + } + + .place-content-center { + place-content: center; + } + + .place-items-center { + place-items: center; + } + + .items-baseline { + align-items: baseline; + } + + .items-center { + align-items: center; + } + + .items-end { + align-items: flex-end; + } + + .items-start { + align-items: flex-start; + } + + .items-stretch { + align-items: stretch; + } + + .justify-between { + justify-content: space-between; + } + + .justify-center { + justify-content: center; + } + + .justify-end { + justify-content: flex-end; + } + + .justify-start { + justify-content: flex-start; + } + + .gap-0 { + gap: 0; + } + + .gap-0\.5 { + gap: calc(var(--appearance-spacing) * .5); + } + + .gap-1 { + gap: var(--appearance-spacing); + } + + .gap-1\.5 { + gap: calc(var(--appearance-spacing) * 1.5); + } + + .gap-2 { + gap: calc(var(--appearance-spacing) * 2); + } + + .gap-3 { + gap: calc(var(--appearance-spacing) * 3); + } + + .gap-4 { + gap: calc(var(--appearance-spacing) * 4); + } + + .gap-6 { + gap: calc(var(--appearance-spacing) * 6); + } + + .gap-8 { + gap: calc(var(--appearance-spacing) * 8); + } + + :where(.space-y-0 > :not(:last-child)) { + --tw-space-y-reverse: 0; + margin-block: 0; + } + + :where(.space-y-0\.5 > :not(:last-child)) { + --tw-space-y-reverse: 0; + margin-block-start: calc(calc(var(--appearance-spacing) * .5) * var(--tw-space-y-reverse)); + margin-block-end: calc(calc(var(--appearance-spacing) * .5) * calc(1 - var(--tw-space-y-reverse))); + } + + :where(.space-y-1 > :not(:last-child)) { + --tw-space-y-reverse: 0; + margin-block-start: calc(var(--appearance-spacing) * var(--tw-space-y-reverse)); + margin-block-end: calc(var(--appearance-spacing) * calc(1 - var(--tw-space-y-reverse))); + } + + :where(.space-y-4 > :not(:last-child)) { + --tw-space-y-reverse: 0; + margin-block-start: calc(calc(var(--appearance-spacing) * 4) * var(--tw-space-y-reverse)); + margin-block-end: calc(calc(var(--appearance-spacing) * 4) * calc(1 - var(--tw-space-y-reverse))); + } + + :where(.space-y-6 > :not(:last-child)) { + --tw-space-y-reverse: 0; + margin-block-start: calc(calc(var(--appearance-spacing) * 6) * var(--tw-space-y-reverse)); + margin-block-end: calc(calc(var(--appearance-spacing) * 6) * calc(1 - var(--tw-space-y-reverse))); + } + + .gap-x-3 { + column-gap: calc(var(--appearance-spacing) * 3); + } + + .gap-x-4 { + column-gap: calc(var(--appearance-spacing) * 4); + } + + :where(.-space-x-2 > :not(:last-child)) { + --tw-space-x-reverse: 0; + margin-inline-start: calc(calc(var(--appearance-spacing) * -2) * var(--tw-space-x-reverse)); + margin-inline-end: calc(calc(var(--appearance-spacing) * -2) * calc(1 - var(--tw-space-x-reverse))); + } + + .gap-y-1 { + row-gap: var(--appearance-spacing); + } + + .gap-y-2 { + row-gap: calc(var(--appearance-spacing) * 2); + } + + :where(.divide-y > :not(:last-child)) { + --tw-divide-y-reverse: 0; + border-bottom-style: var(--tw-border-style); + border-top-style: var(--tw-border-style); + border-top-width: calc(1px * var(--tw-divide-y-reverse)); + border-bottom-width: calc(1px * calc(1 - var(--tw-divide-y-reverse))); + } + + .self-start { + align-self: flex-start; + } + + .justify-self-end { + justify-self: flex-end; + } + + .truncate { + text-overflow: ellipsis; + white-space: nowrap; + overflow: hidden; + } + + .overflow-auto { + overflow: auto; + } + + .overflow-hidden { + overflow: hidden; + } + + .overflow-x-auto { + overflow-x: auto; + } + + .overflow-x-hidden { + overflow-x: hidden; + } + + .overflow-y-auto { + overflow-y: auto; + } + + .rounded { + border-radius: .25rem; + } + + .rounded-2xl { + border-radius: calc(var(--radius) * 1.8); + } + + .rounded-4xl { + border-radius: calc(var(--radius) * 2.6); + } + + .rounded-\[2px\] { + border-radius: 2px; + } + + .rounded-\[4px\] { + border-radius: 4px; + } + + .rounded-\[calc\(var\(--radius\)-3px\)\] { + border-radius: calc(var(--radius) - 3px); + } + + .rounded-\[var\(--appearance-button-radius\,min\(var\(--radius-md\)\,10px\)\)\] { + border-radius: var(--appearance-button-radius, min(var(--radius-md), 10px)); + } + + .rounded-\[var\(--appearance-button-radius\,min\(var\(--radius-md\)\,12px\)\)\] { + border-radius: var(--appearance-button-radius, min(var(--radius-md), 12px)); + } + + .rounded-\[var\(--appearance-button-radius\,var\(--radius-lg\)\)\] { + border-radius: var(--appearance-button-radius, var(--radius-lg)); + } + + .rounded-full { + border-radius: 3.40282e38px; + } + + .rounded-lg { + border-radius: var(--radius); + } + + .rounded-lg\! { + border-radius: var(--radius) !important; + } + + .rounded-md { + border-radius: calc(var(--radius) * .8); + } + + .rounded-none { + border-radius: 0; + } + + .rounded-sm { + border-radius: calc(var(--radius) * .6); + } + + .rounded-xl { + border-radius: calc(var(--radius) * 1.4); + } + + .rounded-xl\! { + border-radius: calc(var(--radius) * 1.4) !important; + } + + .rounded-t-xl { + border-top-left-radius: calc(var(--radius) * 1.4); + border-top-right-radius: calc(var(--radius) * 1.4); + } + + .rounded-b-xl { + border-bottom-right-radius: calc(var(--radius) * 1.4); + border-bottom-left-radius: calc(var(--radius) * 1.4); + } + + .border { + border-style: var(--tw-border-style); + border-width: 1px; + } + + .border-0 { + border-style: var(--tw-border-style); + border-width: 0; + } + + .border-2 { + border-style: var(--tw-border-style); + border-width: 2px; + } + + .border-t { + border-top-style: var(--tw-border-style); + border-top-width: 1px; + } + + .border-r-2 { + border-right-style: var(--tw-border-style); + border-right-width: 2px; + } + + .border-b { + border-bottom-style: var(--tw-border-style); + border-bottom-width: 1px; + } + + .border-l { + border-left-style: var(--tw-border-style); + border-left-width: 1px; + } + + .border-l-2 { + border-left-style: var(--tw-border-style); + border-left-width: 2px; + } + + .border-dashed { + --tw-border-style: dashed; + border-style: dashed; + } + + .border-background { + border-color: var(--background); + } + + .border-border { + border-color: var(--border); + } + + .border-current { + border-color: currentColor; + } + + .border-destructive\/40 { + border-color: var(--destructive); + } + + @supports (color: color-mix(in lab, red, red)) { + .border-destructive\/40 { + border-color: color-mix(in oklab, var(--destructive) 40%, transparent); + } + } + + .border-info\/30 { + border-color: var(--info); + } + + @supports (color: color-mix(in lab, red, red)) { + .border-info\/30 { + border-color: color-mix(in oklab, var(--info) 30%, transparent); + } + } + + .border-input, .border-input\/30 { + border-color: var(--input); + } + + @supports (color: color-mix(in lab, red, red)) { + .border-input\/30 { + border-color: color-mix(in oklab, var(--input) 30%, transparent); + } + } + + .border-primary\/30 { + border-color: var(--primary); + } + + @supports (color: color-mix(in lab, red, red)) { + .border-primary\/30 { + border-color: color-mix(in oklab, var(--primary) 30%, transparent); + } + } + + .border-sidebar-border { + border-color: var(--sidebar-border); + } + + .border-success\/30 { + border-color: var(--success); + } + + @supports (color: color-mix(in lab, red, red)) { + .border-success\/30 { + border-color: color-mix(in oklab, var(--success) 30%, transparent); + } + } + + .border-transparent { + border-color: #0000; + } + + .border-warning\/40 { + border-color: var(--warning); + } + + @supports (color: color-mix(in lab, red, red)) { + .border-warning\/40 { + border-color: color-mix(in oklab, var(--warning) 40%, transparent); + } + } + + .border-t-transparent { + border-top-color: #0000; + } + + .bg-amber-500 { + background-color: var(--color-amber-500); + } + + .bg-background { + background-color: var(--background); + } + + .bg-black\/10 { + background-color: #0000001a; + } + + @supports (color: color-mix(in lab, red, red)) { + .bg-black\/10 { + background-color: color-mix(in oklab, var(--color-black) 10%, transparent); + } + } + + .bg-border { + background-color: var(--border); + } + + .bg-card { + background-color: var(--card); + } + + .bg-current { + background-color: currentColor; + } + + .bg-destructive, .bg-destructive\/10 { + background-color: var(--destructive); + } + + @supports (color: color-mix(in lab, red, red)) { + .bg-destructive\/10 { + background-color: color-mix(in oklab, var(--destructive) 10%, transparent); + } + } + + .bg-foreground { + background-color: var(--foreground); + } + + .bg-info\/5 { + background-color: var(--info); + } + + @supports (color: color-mix(in lab, red, red)) { + .bg-info\/5 { + background-color: color-mix(in oklab, var(--info) 5%, transparent); + } + } + + .bg-info\/10 { + background-color: var(--info); + } + + @supports (color: color-mix(in lab, red, red)) { + .bg-info\/10 { + background-color: color-mix(in oklab, var(--info) 10%, transparent); + } + } + + .bg-input\/30 { + background-color: var(--input); + } + + @supports (color: color-mix(in lab, red, red)) { + .bg-input\/30 { + background-color: color-mix(in oklab, var(--input) 30%, transparent); + } + } + + .bg-muted, .bg-muted\/40 { + background-color: var(--muted); + } + + @supports (color: color-mix(in lab, red, red)) { + .bg-muted\/40 { + background-color: color-mix(in oklab, var(--muted) 40%, transparent); + } + } + + .bg-muted\/50 { + background-color: var(--muted); + } + + @supports (color: color-mix(in lab, red, red)) { + .bg-muted\/50 { + background-color: color-mix(in oklab, var(--muted) 50%, transparent); + } + } + + .bg-popover { + background-color: var(--popover); + } + + .bg-primary, .bg-primary\/5 { + background-color: var(--primary); + } + + @supports (color: color-mix(in lab, red, red)) { + .bg-primary\/5 { + background-color: color-mix(in oklab, var(--primary) 5%, transparent); + } + } + + .bg-primary\/10 { + background-color: var(--primary); + } + + @supports (color: color-mix(in lab, red, red)) { + .bg-primary\/10 { + background-color: color-mix(in oklab, var(--primary) 10%, transparent); + } + } + + .bg-secondary { + background-color: var(--secondary); + } + + .bg-sidebar { + background-color: var(--sidebar); + } + + .bg-sidebar-border { + background-color: var(--sidebar-border); + } + + .bg-success\/5 { + background-color: var(--success); + } + + @supports (color: color-mix(in lab, red, red)) { + .bg-success\/5 { + background-color: color-mix(in oklab, var(--success) 5%, transparent); + } + } + + .bg-success\/10 { + background-color: var(--success); + } + + @supports (color: color-mix(in lab, red, red)) { + .bg-success\/10 { + background-color: color-mix(in oklab, var(--success) 10%, transparent); + } + } + + .bg-transparent { + background-color: #0000; + } + + .bg-warning\/10 { + background-color: var(--warning); + } + + @supports (color: color-mix(in lab, red, red)) { + .bg-warning\/10 { + background-color: color-mix(in oklab, var(--warning) 10%, transparent); + } + } + + .bg-gradient-to-br { + --tw-gradient-position: to bottom right in oklab; + background-image: linear-gradient(var(--tw-gradient-stops)); + } + + .from-muted\/30 { + --tw-gradient-from: var(--muted); + } + + @supports (color: color-mix(in lab, red, red)) { + .from-muted\/30 { + --tw-gradient-from: color-mix(in oklab, var(--muted) 30%, transparent); + } + } + + .from-muted\/30 { + --tw-gradient-stops: var(--tw-gradient-via-stops, var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position)); + } + + .from-primary\/20 { + --tw-gradient-from: var(--primary); + } + + @supports (color: color-mix(in lab, red, red)) { + .from-primary\/20 { + --tw-gradient-from: color-mix(in oklab, var(--primary) 20%, transparent); + } + } + + .from-primary\/20 { + --tw-gradient-stops: var(--tw-gradient-via-stops, var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position)); + } + + .via-primary\/10 { + --tw-gradient-via: var(--primary); + } + + @supports (color: color-mix(in lab, red, red)) { + .via-primary\/10 { + --tw-gradient-via: color-mix(in oklab, var(--primary) 10%, transparent); + } + } + + .via-primary\/10 { + --tw-gradient-via-stops: var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-via) var(--tw-gradient-via-position), var(--tw-gradient-to) var(--tw-gradient-to-position); + --tw-gradient-stops: var(--tw-gradient-via-stops); + } + + .to-accent { + --tw-gradient-to: var(--accent); + --tw-gradient-stops: var(--tw-gradient-via-stops, var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position)); + } + + .to-muted\/0 { + --tw-gradient-to: var(--muted); + } + + @supports (color: color-mix(in lab, red, red)) { + .to-muted\/0 { + --tw-gradient-to: color-mix(in oklab, var(--muted) 0%, transparent); + } + } + + .to-muted\/0 { + --tw-gradient-stops: var(--tw-gradient-via-stops, var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position)); + } + + .bg-clip-padding { + background-clip: padding-box; + } + + .fill-foreground { + fill: var(--foreground); + } + + .fill-muted-foreground, .fill-muted-foreground\/10 { + fill: var(--muted-foreground); + } + + @supports (color: color-mix(in lab, red, red)) { + .fill-muted-foreground\/10 { + fill: color-mix(in oklab, var(--muted-foreground) 10%, transparent); + } + } + + .stroke-border { + stroke: var(--border); + } + + .stroke-card { + stroke: var(--card); + } + + .stroke-muted-foreground\/40 { + stroke: var(--muted-foreground); + } + + @supports (color: color-mix(in lab, red, red)) { + .stroke-muted-foreground\/40 { + stroke: color-mix(in oklab, var(--muted-foreground) 40%, transparent); + } + } + + .object-cover { + object-fit: cover; + } + + .p-0 { + padding: 0; + } + + .p-1 { + padding: var(--appearance-spacing); + } + + .p-2 { + padding: calc(var(--appearance-spacing) * 2); + } + + .p-3 { + padding: calc(var(--appearance-spacing) * 3); + } + + .p-4 { + padding: calc(var(--appearance-spacing) * 4); + } + + .p-5 { + padding: calc(var(--appearance-spacing) * 5); + } + + .p-6 { + padding: calc(var(--appearance-spacing) * 6); + } + + .p-\[3px\] { + padding: 3px; + } + + .px-0 { + padding-inline: 0; + } + + .px-1 { + padding-inline: var(--appearance-spacing); + } + + .px-1\.5 { + padding-inline: calc(var(--appearance-spacing) * 1.5); + } + + .px-2 { + padding-inline: calc(var(--appearance-spacing) * 2); + } + + .px-2\.5 { + padding-inline: calc(var(--appearance-spacing) * 2.5); + } + + .px-3 { + padding-inline: calc(var(--appearance-spacing) * 3); + } + + .px-4 { + padding-inline: calc(var(--appearance-spacing) * 4); + } + + .px-6 { + padding-inline: calc(var(--appearance-spacing) * 6); + } + + .py-0 { + padding-block: 0; + } + + .py-0\.5 { + padding-block: calc(var(--appearance-spacing) * .5); + } + + .py-1 { + padding-block: var(--appearance-spacing); + } + + .py-1\.5 { + padding-block: calc(var(--appearance-spacing) * 1.5); + } + + .py-2 { + padding-block: calc(var(--appearance-spacing) * 2); + } + + .py-3 { + padding-block: calc(var(--appearance-spacing) * 3); + } + + .py-4 { + padding-block: calc(var(--appearance-spacing) * 4); + } + + .py-6 { + padding-block: calc(var(--appearance-spacing) * 6); + } + + .py-12 { + padding-block: calc(var(--appearance-spacing) * 12); + } + + .py-16 { + padding-block: calc(var(--appearance-spacing) * 16); + } + + .pt-2 { + padding-top: calc(var(--appearance-spacing) * 2); + } + + .pr-1 { + padding-right: var(--appearance-spacing); + } + + .pr-2 { + padding-right: calc(var(--appearance-spacing) * 2); + } + + .pr-4 { + padding-right: calc(var(--appearance-spacing) * 4); + } + + .pr-8 { + padding-right: calc(var(--appearance-spacing) * 8); + } + + .pr-10 { + padding-right: calc(var(--appearance-spacing) * 10); + } + + .pb-0 { + padding-bottom: 0; + } + + .pb-2 { + padding-bottom: calc(var(--appearance-spacing) * 2); + } + + .pl-1 { + padding-left: var(--appearance-spacing); + } + + .pl-1\.5 { + padding-left: calc(var(--appearance-spacing) * 1.5); + } + + .pl-2 { + padding-left: calc(var(--appearance-spacing) * 2); + } + + .pl-2\.5 { + padding-left: calc(var(--appearance-spacing) * 2.5); + } + + .pl-3 { + padding-left: calc(var(--appearance-spacing) * 3); + } + + .pl-5 { + padding-left: calc(var(--appearance-spacing) * 5); + } + + .text-center { + text-align: center; + } + + .text-left { + text-align: left; + } + + .text-right { + text-align: right; + } + + .align-middle { + vertical-align: middle; + } + + .align-text-bottom { + vertical-align: text-bottom; + } + + .align-top { + vertical-align: top; + } + + :where(.type-empty-state-title-illustrated) { + font-size: var(--type-empty-state-title-illustrated-size); + font-weight: var(--type-empty-state-title-illustrated-weight); + line-height: var(--type-empty-state-title-illustrated-leading); + letter-spacing: var(--type-empty-state-title-illustrated-tracking); + font-family: var(--type-empty-state-title-illustrated-family); + } + + :where(.type-page-title) { + font-size: var(--type-page-title-size); + font-weight: var(--type-page-title-weight); + line-height: var(--type-page-title-leading); + letter-spacing: var(--type-page-title-tracking); + font-family: var(--type-page-title-family); + } + + :where(.type-section-title) { + font-size: var(--type-section-title-size); + font-weight: var(--type-section-title-weight); + line-height: var(--type-section-title-leading); + letter-spacing: var(--type-section-title-tracking); + font-family: var(--type-section-title-family); + } + + :where(.type-alert-dialog-title) { + font-size: var(--type-alert-dialog-title-size); + font-weight: var(--type-alert-dialog-title-weight); + line-height: var(--type-alert-dialog-title-leading); + font-family: var(--type-alert-dialog-title-family); + } + + :where(.type-card-title) { + font-size: var(--type-card-title-size); + font-weight: var(--type-card-title-weight); + line-height: var(--type-card-title-leading); + font-family: var(--type-card-title-family); + } + + :where(.type-card-title-sm) { + font-size: var(--type-card-title-sm-size); + font-weight: var(--type-card-title-sm-weight); + line-height: var(--type-card-title-sm-leading); + font-family: var(--type-card-title-sm-family); + } + + :where(.type-dialog-title) { + font-size: var(--type-dialog-title-size); + font-weight: var(--type-dialog-title-weight); + line-height: var(--type-dialog-title-leading); + font-family: var(--type-dialog-title-family); + } + + :where(.type-sheet-title) { + font-size: var(--type-sheet-title-size); + font-weight: var(--type-sheet-title-weight); + line-height: var(--type-sheet-title-leading); + font-family: var(--type-sheet-title-family); + } + + .font-\[inherit\] { + font-family: inherit; + } + + .font-mono { + font-family: var(--appearance-font-mono); + } + + :where(.type-metric-total) { + font-size: var(--type-metric-total-size); + font-weight: var(--type-metric-total-weight); + line-height: var(--type-metric-total-leading); + letter-spacing: var(--type-metric-total-tracking); + } + + :where(.type-metric-value) { + font-size: var(--type-metric-value-size); + font-weight: var(--type-metric-value-weight); + line-height: var(--type-metric-value-leading); + letter-spacing: var(--type-metric-value-tracking); + } + + :where(.type-metric-value-lg) { + font-size: var(--type-metric-value-lg-size); + font-weight: var(--type-metric-value-lg-weight); + line-height: var(--type-metric-value-lg-leading); + letter-spacing: var(--type-metric-value-lg-tracking); + } + + :where(.type-badge) { + font-size: var(--type-badge-size); + font-weight: var(--type-badge-weight); + line-height: var(--type-badge-leading); + } + + :where(.type-badge-lg) { + font-size: var(--type-badge-lg-size); + font-weight: var(--type-badge-lg-weight); + line-height: var(--type-badge-lg-leading); + } + + :where(.type-card-eyebrow) { + font-size: var(--type-card-eyebrow-size); + font-weight: var(--type-card-eyebrow-weight); + line-height: var(--type-card-eyebrow-leading); + } + + :where(.type-chat-author) { + font-size: var(--type-chat-author-size); + font-weight: var(--type-chat-author-weight); + line-height: var(--type-chat-author-leading); + } + + :where(.type-chip) { + font-size: var(--type-chip-size); + font-weight: var(--type-chip-weight); + line-height: var(--type-chip-leading); + } + + :where(.type-command-group) { + font-size: var(--type-command-group-size); + font-weight: var(--type-command-group-weight); + line-height: var(--type-command-group-leading); + } + + :where(.type-dropdown-menu-label) { + font-size: var(--type-dropdown-menu-label-size); + font-weight: var(--type-dropdown-menu-label-weight); + line-height: var(--type-dropdown-menu-label-leading); + } + + :where(.type-empty-state-title) { + font-size: var(--type-empty-state-title-size); + font-weight: var(--type-empty-state-title-weight); + line-height: var(--type-empty-state-title-leading); + } + + :where(.type-input-group-addon) { + font-size: var(--type-input-group-addon-size); + font-weight: var(--type-input-group-addon-weight); + line-height: var(--type-input-group-addon-leading); + } + + :where(.type-label) { + font-size: var(--type-label-size); + font-weight: var(--type-label-weight); + line-height: var(--type-label-leading); + } + + :where(.type-metric-delta) { + font-size: var(--type-metric-delta-size); + font-weight: var(--type-metric-delta-weight); + line-height: var(--type-metric-delta-leading); + } + + :where(.type-metric-heading) { + font-size: var(--type-metric-heading-size); + font-weight: var(--type-metric-heading-weight); + line-height: var(--type-metric-heading-leading); + } + + :where(.type-metric-unit) { + font-size: var(--type-metric-unit-size); + font-weight: var(--type-metric-unit-weight); + line-height: var(--type-metric-unit-leading); + } + + :where(.type-segmented-control-segment) { + font-size: var(--type-segmented-control-segment-size); + font-weight: var(--type-segmented-control-segment-weight); + line-height: var(--type-segmented-control-segment-leading); + } + + :where(.type-sidebar-group-label) { + font-size: var(--type-sidebar-group-label-size); + font-weight: var(--type-sidebar-group-label-weight); + line-height: var(--type-sidebar-group-label-leading); + } + + :where(.type-sidebar-menu-badge) { + font-size: var(--type-sidebar-menu-badge-size); + font-weight: var(--type-sidebar-menu-badge-weight); + line-height: var(--type-sidebar-menu-badge-leading); + } + + :where(.type-tabs-trigger) { + font-size: var(--type-tabs-trigger-size); + font-weight: var(--type-tabs-trigger-weight); + line-height: var(--type-tabs-trigger-leading); + } + + :where(.type-command-shortcut) { + font-size: var(--type-command-shortcut-size); + line-height: var(--type-command-shortcut-leading); + letter-spacing: var(--type-command-shortcut-tracking); + } + + :where(.type-dropdown-menu-shortcut) { + font-size: var(--type-dropdown-menu-shortcut-size); + line-height: var(--type-dropdown-menu-shortcut-leading); + letter-spacing: var(--type-dropdown-menu-shortcut-tracking); + } + + .text-lg { + font-size: var(--text-lg); + line-height: var(--tw-leading, var(--text-lg--line-height)); + } + + :where(.type-alert-dialog-description) { + font-size: var(--type-alert-dialog-description-size); + line-height: var(--type-alert-dialog-description-leading); + } + + :where(.type-avatar-fallback) { + font-size: var(--type-avatar-fallback-size); + line-height: var(--type-avatar-fallback-leading); + } + + :where(.type-avatar-group-count) { + font-size: var(--type-avatar-group-count-size); + line-height: var(--type-avatar-group-count-leading); + } + + :where(.type-banner) { + font-size: var(--type-banner-size); + line-height: var(--type-banner-leading); + } + + :where(.type-body) { + font-size: var(--type-body-size); + line-height: var(--type-body-leading); + } + + :where(.type-caption-plain) { + font-size: var(--type-caption-plain-size); + line-height: var(--type-caption-plain-leading); + } + + :where(.type-card) { + font-size: var(--type-card-size); + line-height: var(--type-card-leading); + } + + :where(.type-card-description) { + font-size: var(--type-card-description-size); + line-height: var(--type-card-description-leading); + } + + :where(.type-card-metadata) { + font-size: var(--type-card-metadata-size); + line-height: var(--type-card-metadata-leading); + } + + :where(.type-chart-label) { + font-size: var(--type-chart-label-size); + line-height: var(--type-chart-label-leading); + } + + :where(.type-chat-message) { + font-size: var(--type-chat-message-size); + line-height: var(--type-chat-message-leading); + } + + :where(.type-chip-meta) { + font-size: var(--type-chip-meta-size); + line-height: var(--type-chip-meta-leading); + } + + :where(.type-command-empty) { + font-size: var(--type-command-empty-size); + line-height: var(--type-command-empty-leading); + } + + :where(.type-command-input) { + font-size: var(--type-command-input-size); + line-height: var(--type-command-input-leading); + } + + :where(.type-command-item) { + font-size: var(--type-command-item-size); + line-height: var(--type-command-item-leading); + } + + :where(.type-description-details) { + font-size: var(--type-description-details-size); + line-height: var(--type-description-details-leading); + } + + :where(.type-description-term) { + font-size: var(--type-description-term-size); + line-height: var(--type-description-term-leading); + } + + :where(.type-dialog-content) { + font-size: var(--type-dialog-content-size); + line-height: var(--type-dialog-content-leading); + } + + :where(.type-dialog-description) { + font-size: var(--type-dialog-description-size); + line-height: var(--type-dialog-description-leading); + } + + :where(.type-dropdown-menu-checkbox-item) { + font-size: var(--type-dropdown-menu-checkbox-item-size); + line-height: var(--type-dropdown-menu-checkbox-item-leading); + } + + :where(.type-dropdown-menu-item) { + font-size: var(--type-dropdown-menu-item-size); + line-height: var(--type-dropdown-menu-item-leading); + } + + :where(.type-dropdown-menu-radio-item) { + font-size: var(--type-dropdown-menu-radio-item-size); + line-height: var(--type-dropdown-menu-radio-item-leading); + } + + :where(.type-dropdown-menu-sub-trigger) { + font-size: var(--type-dropdown-menu-sub-trigger-size); + line-height: var(--type-dropdown-menu-sub-trigger-leading); + } + + :where(.type-empty-state-description) { + font-size: var(--type-empty-state-description-size); + line-height: var(--type-empty-state-description-leading); + } + + :where(.type-empty-state-reference) { + font-size: var(--type-empty-state-reference-size); + line-height: var(--type-empty-state-reference-leading); + } + + :where(.type-error-state) { + font-size: var(--type-error-state-size); + line-height: var(--type-error-state-leading); + } + + :where(.type-field-description) { + font-size: var(--type-field-description-size); + line-height: var(--type-field-description-leading); + } + + :where(.type-field-error) { + font-size: var(--type-field-error-size); + line-height: var(--type-field-error-leading); + } + + :where(.type-input-group-control) { + font-size: var(--type-input-group-control-size); + line-height: var(--type-input-group-control-leading); + } + + :where(.type-input-group-text) { + font-size: var(--type-input-group-text-size); + line-height: var(--type-input-group-text-leading); + } + + :where(.type-input-touch) { + font-size: var(--type-input-touch-size); + line-height: var(--type-input-touch-leading); + } + + :where(.type-list-item) { + font-size: var(--type-list-item-size); + line-height: var(--type-list-item-leading); + } + + :where(.type-list-item-description) { + font-size: var(--type-list-item-description-size); + line-height: var(--type-list-item-description-leading); + } + + :where(.type-metric-label) { + font-size: var(--type-metric-label-size); + line-height: var(--type-metric-label-leading); + } + + :where(.type-pagination-ellipsis) { + font-size: var(--type-pagination-ellipsis-size); + line-height: var(--type-pagination-ellipsis-leading); + } + + :where(.type-section-description) { + font-size: var(--type-section-description-size); + line-height: var(--type-section-description-leading); + } + + :where(.type-select-item) { + font-size: var(--type-select-item-size); + line-height: var(--type-select-item-leading); + } + + :where(.type-select-label) { + font-size: var(--type-select-label-size); + line-height: var(--type-select-label-leading); + } + + :where(.type-select-trigger) { + font-size: var(--type-select-trigger-size); + line-height: var(--type-select-trigger-leading); + } + + :where(.type-sheet-content) { + font-size: var(--type-sheet-content-size); + line-height: var(--type-sheet-content-leading); + } + + :where(.type-sheet-description) { + font-size: var(--type-sheet-description-size); + line-height: var(--type-sheet-description-leading); + } + + :where(.type-sidebar-group-content) { + font-size: var(--type-sidebar-group-content-size); + line-height: var(--type-sidebar-group-content-leading); + } + + :where(.type-sidebar-menu-button) { + font-size: var(--type-sidebar-menu-button-size); + line-height: var(--type-sidebar-menu-button-leading); + } + + :where(.type-sidebar-menu-button-lg) { + font-size: var(--type-sidebar-menu-button-lg-size); + line-height: var(--type-sidebar-menu-button-lg-leading); + } + + :where(.type-sidebar-menu-button-sm) { + font-size: var(--type-sidebar-menu-button-sm-size); + line-height: var(--type-sidebar-menu-button-sm-leading); + } + + :where(.type-table) { + font-size: var(--type-table-size); + line-height: var(--type-table-leading); + } + + :where(.type-table-caption) { + font-size: var(--type-table-caption-size); + line-height: var(--type-table-caption-leading); + } + + :where(.type-table-empty-state) { + font-size: var(--type-table-empty-state-size); + line-height: var(--type-table-empty-state-leading); + } + + :where(.type-table-toolbar) { + font-size: var(--type-table-toolbar-size); + line-height: var(--type-table-toolbar-leading); + } + + :where(.type-tabs-content) { + font-size: var(--type-tabs-content-size); + line-height: var(--type-tabs-content-leading); + } + + :where(.type-textarea-touch) { + font-size: var(--type-textarea-touch-size); + line-height: var(--type-textarea-touch-leading); + } + + :where(.type-tooltip-content) { + font-size: var(--type-tooltip-content-size); + line-height: var(--type-tooltip-content-leading); + } + + :where(.type-card-heading) { + font-size: var(--type-card-heading-size); + font-weight: var(--type-card-heading-weight); + } + + .\[font-size\:inherit\] { + font-size: inherit; + } + + :where(.type-banner-title) { + font-weight: var(--type-banner-title-weight); + } + + :where(.type-emphasis) { + font-weight: var(--type-emphasis-weight); + } + + :where(.type-error-state-title) { + font-weight: var(--type-error-state-title-weight); + } + + :where(.type-metric-delta-label) { + font-weight: var(--type-metric-delta-label-weight); + } + + :where(.type-table-footer) { + font-weight: var(--type-table-footer-weight); + } + + :where(.type-table-head) { + font-weight: var(--type-table-head-weight); + } + + .text-balance { + text-wrap: balance; + } + + .\[overflow-wrap\:anywhere\] { + overflow-wrap: anywhere; + } + + .break-words { + overflow-wrap: break-word; + } + + .whitespace-nowrap { + white-space: nowrap; + } + + .whitespace-pre { + white-space: pre; + } + + .whitespace-pre-wrap { + white-space: pre-wrap; + } + + .text-\[color-mix\(in_oklab\,var\(--chart-1\)_55\%\,var\(--foreground\)\)\] { + color: var(--chart-1); + } + + @supports (color: color-mix(in lab, red, red)) { + .text-\[color-mix\(in_oklab\,var\(--chart-1\)_55\%\,var\(--foreground\)\)\] { + color: color-mix(in oklab,var(--chart-1) 55%,var(--foreground)); + } + } + + .text-\[color-mix\(in_oklab\,var\(--chart-2\)_55\%\,var\(--foreground\)\)\] { + color: var(--chart-2); + } + + @supports (color: color-mix(in lab, red, red)) { + .text-\[color-mix\(in_oklab\,var\(--chart-2\)_55\%\,var\(--foreground\)\)\] { + color: color-mix(in oklab,var(--chart-2) 55%,var(--foreground)); + } + } + + .text-\[color-mix\(in_oklab\,var\(--chart-3\)_55\%\,var\(--foreground\)\)\] { + color: var(--chart-3); + } + + @supports (color: color-mix(in lab, red, red)) { + .text-\[color-mix\(in_oklab\,var\(--chart-3\)_55\%\,var\(--foreground\)\)\] { + color: color-mix(in oklab,var(--chart-3) 55%,var(--foreground)); + } + } + + .text-\[color-mix\(in_oklab\,var\(--chart-4\)_55\%\,var\(--foreground\)\)\] { + color: var(--chart-4); + } + + @supports (color: color-mix(in lab, red, red)) { + .text-\[color-mix\(in_oklab\,var\(--chart-4\)_55\%\,var\(--foreground\)\)\] { + color: color-mix(in oklab,var(--chart-4) 55%,var(--foreground)); + } + } + + .text-\[color-mix\(in_oklab\,var\(--chart-5\)_55\%\,var\(--foreground\)\)\] { + color: var(--chart-5); + } + + @supports (color: color-mix(in lab, red, red)) { + .text-\[color-mix\(in_oklab\,var\(--chart-5\)_55\%\,var\(--foreground\)\)\] { + color: color-mix(in oklab,var(--chart-5) 55%,var(--foreground)); + } + } + + .text-amber-500 { + color: var(--color-amber-500); + } + + .text-background { + color: var(--background); + } + + .text-card-foreground { + color: var(--card-foreground); + } + + .text-current { + color: currentColor; + } + + .text-destructive { + color: var(--destructive); + } + + .text-foreground { + color: var(--foreground); + } + + .text-info { + color: var(--info); + } + + .text-muted-foreground, .text-muted-foreground\/70 { + color: var(--muted-foreground); + } + + @supports (color: color-mix(in lab, red, red)) { + .text-muted-foreground\/70 { + color: color-mix(in oklab, var(--muted-foreground) 70%, transparent); + } + } + + .text-popover-foreground { + color: var(--popover-foreground); + } + + .text-primary { + color: var(--primary); + } + + .text-primary-foreground { + color: var(--primary-foreground); + } + + .text-secondary-foreground { + color: var(--secondary-foreground); + } + + .text-sidebar-foreground, .text-sidebar-foreground\/70 { + color: var(--sidebar-foreground); + } + + @supports (color: color-mix(in lab, red, red)) { + .text-sidebar-foreground\/70 { + color: color-mix(in oklab, var(--sidebar-foreground) 70%, transparent); + } + } + + .text-success { + color: var(--success); + } + + .text-warning { + color: var(--warning); + } + + .uppercase { + text-transform: uppercase; + } + + .italic { + font-style: italic; + } + + .tabular-nums { + --tw-numeric-spacing: tabular-nums; + font-variant-numeric: var(--tw-ordinal, ) var(--tw-slashed-zero, ) var(--tw-numeric-figure, ) var(--tw-numeric-spacing, ) var(--tw-numeric-fraction, ); + } + + .underline { + text-decoration-line: underline; + } + + .decoration-dotted { + text-decoration-style: dotted; + } + + .underline-offset-2 { + text-underline-offset: 2px; + } + + .underline-offset-4 { + text-underline-offset: 4px; + } + + .accent-primary { + accent-color: var(--primary); + } + + .opacity-40 { + opacity: .4; + } + + .opacity-50 { + opacity: .5; + } + + .opacity-75 { + opacity: .75; + } + + .opacity-100 { + opacity: 1; + } + + .bg-blend-color { + background-blend-mode: color; + } + + .shadow { + --tw-shadow: 0 1px 3px 0 var(--tw-shadow-color, #0000001a), 0 1px 2px -1px var(--tw-shadow-color, #0000001a); + box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow); + } + + .shadow-2xl { + --tw-shadow: 0 25px 50px -12px + var(--tw-shadow-color, rgb(0 0 0 / calc(.25 * var(--appearance-shadow-strength)))); + box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow); + } + + .shadow-\[0_0_0_1px_hsl\(var\(--sidebar-border\)\)\] { + --tw-shadow: 0 0 0 1px var(--tw-shadow-color, hsl(var(--sidebar-border))); + box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow); + } + + .shadow-lg { + --tw-shadow: 0 10px 15px -3px + var(--tw-shadow-color, rgb(0 0 0 / calc(.1 * var(--appearance-shadow-strength)))), 0 4px 6px -4px var(--tw-shadow-color, rgb(0 0 0 / calc(.1 * var(--appearance-shadow-strength)))); + box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow); + } + + .shadow-md { + --tw-shadow: 0 4px 6px -1px + var(--tw-shadow-color, rgb(0 0 0 / calc(.1 * var(--appearance-shadow-strength)))), 0 2px 4px -2px var(--tw-shadow-color, rgb(0 0 0 / calc(.1 * var(--appearance-shadow-strength)))); + box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow); + } + + .shadow-none { + --tw-shadow: 0 0 #0000; + box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow); + } + + .shadow-none\! { + --tw-shadow: 0 0 #0000 !important; + box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow) !important; + } + + .shadow-sm { + --tw-shadow: 0 1px 3px 0 + var(--tw-shadow-color, rgb(0 0 0 / calc(.1 * var(--appearance-shadow-strength)))), 0 1px 2px -1px var(--tw-shadow-color, rgb(0 0 0 / calc(.1 * var(--appearance-shadow-strength)))); + box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow); + } + + .ring { + --tw-ring-shadow: var(--tw-ring-inset, ) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color, currentcolor); + box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow); + } + + .ring-0 { + --tw-ring-shadow: var(--tw-ring-inset, ) 0 0 0 calc(0px + var(--tw-ring-offset-width)) var(--tw-ring-color, currentcolor); + box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow); + } + + .ring-1 { + --tw-ring-shadow: var(--tw-ring-inset, ) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color, currentcolor); + box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow); + } + + .ring-2 { + --tw-ring-shadow: var(--tw-ring-inset, ) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color, currentcolor); + box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow); + } + + .ring-background { + --tw-ring-color: var(--background); + } + + .ring-foreground\/10 { + --tw-ring-color: var(--foreground); + } + + @supports (color: color-mix(in lab, red, red)) { + .ring-foreground\/10 { + --tw-ring-color: color-mix(in oklab, var(--foreground) 10%, transparent); + } + } + + .ring-primary { + --tw-ring-color: var(--primary); + } + + .ring-sidebar-ring { + --tw-ring-color: var(--sidebar-ring); + } + + .outline-hidden { + --tw-outline-style: none; + outline-style: none; + } + + @media (forced-colors: active) { + .outline-hidden { + outline-offset: 2px; + outline: 2px solid #0000; + } + } + + .outline { + outline-style: var(--tw-outline-style); + outline-width: 1px; + } + + .blur-2xl { + --tw-blur: blur(var(--blur-2xl)); + filter: var(--tw-blur, ) var(--tw-brightness, ) var(--tw-contrast, ) var(--tw-grayscale, ) var(--tw-hue-rotate, ) var(--tw-invert, ) var(--tw-saturate, ) var(--tw-sepia, ) var(--tw-drop-shadow, ); + } + + .drop-shadow-lg { + --tw-drop-shadow-size: drop-shadow(0 4px 4px var(--tw-drop-shadow-color, #00000026)); + --tw-drop-shadow: drop-shadow(var(--drop-shadow-lg)); + filter: var(--tw-blur, ) var(--tw-brightness, ) var(--tw-contrast, ) var(--tw-grayscale, ) var(--tw-hue-rotate, ) var(--tw-invert, ) var(--tw-saturate, ) var(--tw-sepia, ) var(--tw-drop-shadow, ); + } + + .filter { + filter: var(--tw-blur, ) var(--tw-brightness, ) var(--tw-contrast, ) var(--tw-grayscale, ) var(--tw-hue-rotate, ) var(--tw-invert, ) var(--tw-saturate, ) var(--tw-sepia, ) var(--tw-drop-shadow, ); + } + + .transition { + transition-property: color, background-color, border-color, outline-color, text-decoration-color, fill, stroke, --tw-gradient-from, --tw-gradient-via, --tw-gradient-to, opacity, box-shadow, transform, translate, scale, rotate, filter, -webkit-backdrop-filter, backdrop-filter, display, content-visibility, overlay, pointer-events; + transition-timing-function: var(--tw-ease, var(--default-transition-timing-function)); + transition-duration: var(--tw-duration, var(--default-transition-duration)); + } + + .transition-\[left\,right\,width\] { + transition-property: left, right, width; + transition-timing-function: var(--tw-ease, var(--default-transition-timing-function)); + transition-duration: var(--tw-duration, var(--default-transition-duration)); + } + + .transition-\[margin\,opacity\] { + transition-property: margin, opacity; + transition-timing-function: var(--tw-ease, var(--default-transition-timing-function)); + transition-duration: var(--tw-duration, var(--default-transition-duration)); + } + + .transition-\[width\,height\,padding\] { + transition-property: width, height, padding; + transition-timing-function: var(--tw-ease, var(--default-transition-timing-function)); + transition-duration: var(--tw-duration, var(--default-transition-duration)); + } + + .transition-\[width\] { + transition-property: width; + transition-timing-function: var(--tw-ease, var(--default-transition-timing-function)); + transition-duration: var(--tw-duration, var(--default-transition-duration)); + } + + .transition-all { + transition-property: all; + transition-timing-function: var(--tw-ease, var(--default-transition-timing-function)); + transition-duration: var(--tw-duration, var(--default-transition-duration)); + } + + .transition-colors { + transition-property: color, background-color, border-color, outline-color, text-decoration-color, fill, stroke, --tw-gradient-from, --tw-gradient-via, --tw-gradient-to; + transition-timing-function: var(--tw-ease, var(--default-transition-timing-function)); + transition-duration: var(--tw-duration, var(--default-transition-duration)); + } + + .transition-opacity { + transition-property: opacity; + transition-timing-function: var(--tw-ease, var(--default-transition-timing-function)); + transition-duration: var(--tw-duration, var(--default-transition-duration)); + } + + .transition-transform { + transition-property: transform, translate, scale, rotate; + transition-timing-function: var(--tw-ease, var(--default-transition-timing-function)); + transition-duration: var(--tw-duration, var(--default-transition-duration)); + } + + .transition-none { + transition-property: none; + } + + .duration-100 { + --tw-duration: .1s; + transition-duration: .1s; + } + + .duration-150 { + --tw-duration: .15s; + transition-duration: .15s; + } + + .duration-200 { + --tw-duration: .2s; + transition-duration: .2s; + } + + .duration-500 { + --tw-duration: .5s; + transition-duration: .5s; + } + + .ease-in-out { + --tw-ease: var(--ease-in-out); + transition-timing-function: var(--ease-in-out); + } + + .ease-linear { + --tw-ease: linear; + transition-timing-function: linear; + } + + .ease-out { + --tw-ease: var(--ease-out); + transition-timing-function: var(--ease-out); + } + + .outline-none { + --tw-outline-style: none; + outline-style: none; + } + + .select-none { + -webkit-user-select: none; + user-select: none; + } + + .running { + animation-play-state: running; + } + + .group-focus-within\/menu-item\:opacity-100:is(:where(.group\/menu-item):focus-within *) { + opacity: 1; + } + + @media (hover: hover) { + .group-hover\/menu-item\:opacity-100:is(:where(.group\/menu-item):hover *) { + opacity: 1; + } + } + + .group-focus\/dropdown-menu-item\:text-accent-foreground:is(:where(.group\/dropdown-menu-item):focus *) { + color: var(--accent-foreground); + } + + .group-has-disabled\/field\:opacity-50:is(:where(.group\/field):has(:disabled) *) { + opacity: .5; + } + + .group-has-data-\[sidebar\=menu-action\]\/menu-item\:pr-8:is(:where(.group\/menu-item):has([data-sidebar="menu-action"]) *) { + padding-right: calc(var(--appearance-spacing) * 8); + } + + .group-has-data-\[size\=lg\]\/avatar-group\:size-10:is(:where(.group\/avatar-group):has([data-size="lg"]) *) { + width: calc(var(--appearance-spacing) * 10); + height: calc(var(--appearance-spacing) * 10); + } + + .group-has-data-\[size\=sm\]\/avatar-group\:size-6:is(:where(.group\/avatar-group):has([data-size="sm"]) *) { + width: calc(var(--appearance-spacing) * 6); + height: calc(var(--appearance-spacing) * 6); + } + + .group-has-\[\>input\]\/input-group\:pt-2:is(:where(.group\/input-group):has( > input) *) { + padding-top: calc(var(--appearance-spacing) * 2); + } + + .group-has-\[\>input\]\/input-group\:pb-2:is(:where(.group\/input-group):has( > input) *) { + padding-bottom: calc(var(--appearance-spacing) * 2); + } + + .group-data-highlighted\/command-item\:text-foreground:is(:where(.group\/command-item)[data-highlighted] *) { + color: var(--foreground); + } + + .group-data-\[collapsible\=icon\]\:-mt-8:is(:where(.group)[data-collapsible="icon"] *) { + margin-top: calc(var(--appearance-spacing) * -8); + } + + .group-data-\[collapsible\=icon\]\:hidden:is(:where(.group)[data-collapsible="icon"] *) { + display: none; + } + + .group-data-\[collapsible\=icon\]\:size-8\!:is(:where(.group)[data-collapsible="icon"] *) { + width: calc(var(--appearance-spacing) * 8) !important; + height: calc(var(--appearance-spacing) * 8) !important; + } + + .group-data-\[collapsible\=icon\]\:w-\(--sidebar-width-icon\):is(:where(.group)[data-collapsible="icon"] *) { + width: var(--sidebar-width-icon); + } + + .group-data-\[collapsible\=icon\]\:w-\[calc\(var\(--sidebar-width-icon\)\+\(--spacing\(4\)\)\)\]:is(:where(.group)[data-collapsible="icon"] *) { + width: calc(var(--sidebar-width-icon) + (calc(var(--appearance-spacing) * 4))); + } + + .group-data-\[collapsible\=icon\]\:w-\[calc\(var\(--sidebar-width-icon\)\+\(--spacing\(4\)\)\+2px\)\]:is(:where(.group)[data-collapsible="icon"] *) { + width: calc(var(--sidebar-width-icon) + (calc(var(--appearance-spacing) * 4)) + 2px); + } + + .group-data-\[collapsible\=icon\]\:overflow-hidden:is(:where(.group)[data-collapsible="icon"] *) { + overflow: hidden; + } + + .group-data-\[collapsible\=icon\]\:p-0\!:is(:where(.group)[data-collapsible="icon"] *) { + padding: 0 !important; + } + + .group-data-\[collapsible\=icon\]\:p-2\!:is(:where(.group)[data-collapsible="icon"] *) { + padding: calc(var(--appearance-spacing) * 2) !important; + } + + .group-data-\[collapsible\=icon\]\:opacity-0:is(:where(.group)[data-collapsible="icon"] *) { + opacity: 0; + } + + .group-data-\[collapsible\=offcanvas\]\:w-0:is(:where(.group)[data-collapsible="offcanvas"] *) { + width: 0; + } + + .group-data-\[collapsible\=offcanvas\]\:translate-x-0:is(:where(.group)[data-collapsible="offcanvas"] *) { + --tw-translate-x: 0px; + translate: var(--tw-translate-x) var(--tw-translate-y); + } + + .group-data-\[disabled\=true\]\:pointer-events-none:is(:where(.group)[data-disabled="true"] *) { + pointer-events: none; + } + + .group-data-\[disabled\=true\]\:opacity-50:is(:where(.group)[data-disabled="true"] *), .group-data-\[disabled\=true\]\/input-group\:opacity-50:is(:where(.group\/input-group)[data-disabled="true"] *) { + opacity: .5; + } + + .group-data-\[side\=left\]\:-right-4:is(:where(.group)[data-side="left"] *) { + right: calc(var(--appearance-spacing) * -4); + } + + .group-data-\[side\=left\]\:border-r:is(:where(.group)[data-side="left"] *) { + border-right-style: var(--tw-border-style); + border-right-width: 1px; + } + + .group-data-\[side\=right\]\:left-0:is(:where(.group)[data-side="right"] *) { + left: 0; + } + + .group-data-\[side\=right\]\:rotate-180:is(:where(.group)[data-side="right"] *) { + rotate: 180deg; + } + + .group-data-\[side\=right\]\:border-l:is(:where(.group)[data-side="right"] *) { + border-left-style: var(--tw-border-style); + border-left-width: 1px; + } + + .group-data-\[size\=default\]\/avatar\:size-2\.5:is(:where(.group\/avatar)[data-size="default"] *) { + width: calc(var(--appearance-spacing) * 2.5); + height: calc(var(--appearance-spacing) * 2.5); + } + + .group-data-\[size\=default\]\/switch\:size-4:is(:where(.group\/switch)[data-size="default"] *) { + width: calc(var(--appearance-spacing) * 4); + height: calc(var(--appearance-spacing) * 4); + } + + .group-data-\[size\=lg\]\/avatar\:size-3:is(:where(.group\/avatar)[data-size="lg"] *) { + width: calc(var(--appearance-spacing) * 3); + height: calc(var(--appearance-spacing) * 3); + } + + .group-data-\[size\=sm\]\/alert-dialog-content\:grid:is(:where(.group\/alert-dialog-content)[data-size="sm"] *) { + display: grid; + } + + .group-data-\[size\=sm\]\/alert-dialog-content\:grid-cols-2:is(:where(.group\/alert-dialog-content)[data-size="sm"] *) { + grid-template-columns: repeat(2, minmax(0, 1fr)); + } + + .group-data-\[size\=sm\]\/avatar\:size-2:is(:where(.group\/avatar)[data-size="sm"] *) { + width: calc(var(--appearance-spacing) * 2); + height: calc(var(--appearance-spacing) * 2); + } + + :where(.group-data-\[size\=sm\]\/avatar\:type-avatar-fallback-sm:is(:where(.group\/avatar)[data-size="sm"] *)) { + font-size: var(--type-avatar-fallback-sm-size); + line-height: var(--type-avatar-fallback-sm-leading); + } + + .group-data-\[size\=sm\]\/card\:p-3:is(:where(.group\/card)[data-size="sm"] *) { + padding: calc(var(--appearance-spacing) * 3); + } + + .group-data-\[size\=sm\]\/card\:px-3:is(:where(.group\/card)[data-size="sm"] *) { + padding-inline: calc(var(--appearance-spacing) * 3); + } + + :where(.group-data-\[size\=sm\]\/card\:type-card-title-sm:is(:where(.group\/card)[data-size="sm"] *)) { + font-size: var(--type-card-title-sm-size); + font-weight: var(--type-card-title-sm-weight); + line-height: var(--type-card-title-sm-leading); + font-family: var(--type-card-title-sm-family); + } + + .group-data-\[size\=sm\]\/switch\:size-3:is(:where(.group\/switch)[data-size="sm"] *) { + width: calc(var(--appearance-spacing) * 3); + height: calc(var(--appearance-spacing) * 3); + } + + .group-data-\[variant\=floating\]\:rounded-lg:is(:where(.group)[data-variant="floating"] *) { + border-radius: var(--radius); + } + + .group-data-\[variant\=floating\]\:shadow-sm:is(:where(.group)[data-variant="floating"] *) { + --tw-shadow: 0 1px 3px 0 + var(--tw-shadow-color, rgb(0 0 0 / calc(.1 * var(--appearance-shadow-strength)))), 0 1px 2px -1px var(--tw-shadow-color, rgb(0 0 0 / calc(.1 * var(--appearance-shadow-strength)))); + box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow); + } + + .group-data-\[variant\=floating\]\:ring-1:is(:where(.group)[data-variant="floating"] *) { + --tw-ring-shadow: var(--tw-ring-inset, ) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color, currentcolor); + box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow); + } + + .group-data-\[variant\=floating\]\:ring-sidebar-border:is(:where(.group)[data-variant="floating"] *) { + --tw-ring-color: var(--sidebar-border); + } + + .group-data-\[variant\=line\]\/tabs-list\:bg-transparent:is(:where(.group\/tabs-list)[data-variant="line"] *) { + background-color: #0000; + } + + :where(.group-data-horizontal\/tabs\:control-height-default:is(:where(.group\/tabs):where([data-orientation="horizontal"]) *)) { + height: var(--control-default-height); + } + + .group-data-vertical\/tabs\:h-fit:is(:where(.group\/tabs):where([data-orientation="vertical"]) *) { + height: fit-content; + } + + .group-data-vertical\/tabs\:w-full:is(:where(.group\/tabs):where([data-orientation="vertical"]) *) { + width: 100%; + } + + .group-data-vertical\/tabs\:flex-col:is(:where(.group\/tabs):where([data-orientation="vertical"]) *) { + flex-direction: column; + } + + .group-data-vertical\/tabs\:justify-start:is(:where(.group\/tabs):where([data-orientation="vertical"]) *) { + justify-content: flex-start; + } + + @media (hover: hover) { + .peer-hover\/menu-button\:text-sidebar-accent-foreground:is(:where(.peer\/menu-button):hover ~ *) { + color: var(--sidebar-accent-foreground); + } + } + + .peer-disabled\:cursor-not-allowed:is(:where(.peer):disabled ~ *) { + cursor: not-allowed; + } + + .peer-disabled\:opacity-50:is(:where(.peer):disabled ~ *) { + opacity: .5; + } + + .peer-data-\[size\=default\]\/menu-button\:top-1\.5:is(:where(.peer\/menu-button)[data-size="default"] ~ *) { + top: calc(var(--appearance-spacing) * 1.5); + } + + .peer-data-\[size\=lg\]\/menu-button\:top-2\.5:is(:where(.peer\/menu-button)[data-size="lg"] ~ *) { + top: calc(var(--appearance-spacing) * 2.5); + } + + .peer-data-\[size\=sm\]\/menu-button\:top-1:is(:where(.peer\/menu-button)[data-size="sm"] ~ *) { + top: var(--appearance-spacing); + } + + .peer-data-active\/menu-button\:text-sidebar-accent-foreground:is(:is(:where(.peer\/menu-button):where([data-state="active"]), :where(.peer\/menu-button):where([data-active]:not([data-active="false"]))) ~ *) { + color: var(--sidebar-accent-foreground); + } + + .file\:inline-flex::file-selector-button { + display: inline-flex; + } + + .file\:h-6::file-selector-button { + height: calc(var(--appearance-spacing) * 6); + } + + .file\:border-0::file-selector-button { + border-style: var(--tw-border-style); + border-width: 0; + } + + .file\:bg-transparent::file-selector-button { + background-color: #0000; + } + + :where() { + font-size: var(--type-input-size); + line-height: var(--type-input-leading); + } + + .file\:text-foreground::file-selector-button { + color: var(--foreground); + } + + .placeholder\:text-muted-foreground::placeholder { + color: var(--muted-foreground); + } + + .after\:absolute:after { + content: var(--tw-content); + position: absolute; + } + + .after\:-inset-2:after { + content: var(--tw-content); + inset: calc(var(--appearance-spacing) * -2); + } + + .after\:inset-0:after { + content: var(--tw-content); + inset: 0; + } + + .after\:-inset-x-3:after { + content: var(--tw-content); + inset-inline: calc(var(--appearance-spacing) * -3); + } + + .after\:-inset-y-2:after { + content: var(--tw-content); + inset-block: calc(var(--appearance-spacing) * -2); + } + + .after\:inset-y-0:after { + content: var(--tw-content); + inset-block: 0; + } + + .after\:start-1\/2:after { + content: var(--tw-content); + inset-inline-start: 50%; + } + + .after\:w-\[2px\]:after { + content: var(--tw-content); + width: 2px; + } + + .after\:rounded-full:after { + content: var(--tw-content); + border-radius: 3.40282e38px; + } + + .after\:border:after { + content: var(--tw-content); + border-style: var(--tw-border-style); + border-width: 1px; + } + + .after\:border-border:after { + content: var(--tw-content); + border-color: var(--border); + } + + .after\:bg-foreground:after { + content: var(--tw-content); + background-color: var(--foreground); + } + + .after\:opacity-0:after { + content: var(--tw-content); + opacity: 0; + } + + .after\:mix-blend-darken:after { + content: var(--tw-content); + mix-blend-mode: darken; + } + + .after\:transition-opacity:after { + content: var(--tw-content); + transition-property: opacity; + transition-timing-function: var(--tw-ease, var(--default-transition-timing-function)); + transition-duration: var(--tw-duration, var(--default-transition-duration)); + } + + .group-data-\[collapsible\=offcanvas\]\:after\:left-full:is(:where(.group)[data-collapsible="offcanvas"] *):after { + content: var(--tw-content); + left: 100%; + } + + .group-data-horizontal\/tabs\:after\:inset-x-0:is(:where(.group\/tabs):where([data-orientation="horizontal"]) *):after { + content: var(--tw-content); + inset-inline: 0; + } + + .group-data-horizontal\/tabs\:after\:bottom-\[-5px\]:is(:where(.group\/tabs):where([data-orientation="horizontal"]) *):after { + content: var(--tw-content); + bottom: -5px; + } + + .group-data-horizontal\/tabs\:after\:h-0\.5:is(:where(.group\/tabs):where([data-orientation="horizontal"]) *):after { + content: var(--tw-content); + height: calc(var(--appearance-spacing) * .5); + } + + .group-data-vertical\/tabs\:after\:inset-y-0:is(:where(.group\/tabs):where([data-orientation="vertical"]) *):after { + content: var(--tw-content); + inset-block: 0; + } + + .group-data-vertical\/tabs\:after\:-right-1:is(:where(.group\/tabs):where([data-orientation="vertical"]) *):after { + content: var(--tw-content); + right: calc(var(--appearance-spacing) * -1); + } + + .group-data-vertical\/tabs\:after\:w-0\.5:is(:where(.group\/tabs):where([data-orientation="vertical"]) *):after { + content: var(--tw-content); + width: calc(var(--appearance-spacing) * .5); + } + + .first\:mt-0:first-child { + margin-top: 0; + } + + .last\:mb-0:last-child { + margin-bottom: 0; + } + + .last\:border-0:last-child { + border-style: var(--tw-border-style); + border-width: 0; + } + + @media (hover: hover) { + .hover\:bg-accent\/50:hover { + background-color: var(--accent); + } + + @supports (color: color-mix(in lab, red, red)) { + .hover\:bg-accent\/50:hover { + background-color: color-mix(in oklab, var(--accent) 50%, transparent); + } + } + + .hover\:bg-destructive\/20:hover { + background-color: var(--destructive); + } + + @supports (color: color-mix(in lab, red, red)) { + .hover\:bg-destructive\/20:hover { + background-color: color-mix(in oklab, var(--destructive) 20%, transparent); + } + } + + .hover\:bg-foreground\/5:hover { + background-color: var(--foreground); + } + + @supports (color: color-mix(in lab, red, red)) { + .hover\:bg-foreground\/5:hover { + background-color: color-mix(in oklab, var(--foreground) 5%, transparent); + } + } + + .hover\:bg-foreground\/10:hover { + background-color: var(--foreground); + } + + @supports (color: color-mix(in lab, red, red)) { + .hover\:bg-foreground\/10:hover { + background-color: color-mix(in oklab, var(--foreground) 10%, transparent); + } + } + + .hover\:bg-muted:hover, .hover\:bg-muted\/50:hover { + background-color: var(--muted); + } + + @supports (color: color-mix(in lab, red, red)) { + .hover\:bg-muted\/50:hover { + background-color: color-mix(in oklab, var(--muted) 50%, transparent); + } + } + + .hover\:bg-primary-hover:hover { + background-color: var(--primary-hover); + } + + .hover\:bg-secondary\/80:hover { + background-color: var(--secondary); + } + + @supports (color: color-mix(in lab, red, red)) { + .hover\:bg-secondary\/80:hover { + background-color: color-mix(in oklab, var(--secondary) 80%, transparent); + } + } + + .hover\:bg-sidebar-accent:hover { + background-color: var(--sidebar-accent); + } + + .hover\:bg-transparent:hover { + background-color: #0000; + } + + .hover\:text-foreground:hover { + color: var(--foreground); + } + + .hover\:text-muted-foreground:hover { + color: var(--muted-foreground); + } + + .hover\:text-sidebar-accent-foreground:hover { + color: var(--sidebar-accent-foreground); + } + + .hover\:no-underline:hover { + text-decoration-line: none; + } + + .hover\:underline:hover { + text-decoration-line: underline; + } + + .hover\:opacity-100:hover { + opacity: 1; + } + + .hover\:shadow-\[0_0_0_1px_hsl\(var\(--sidebar-accent\)\)\]:hover { + --tw-shadow: 0 0 0 1px var(--tw-shadow-color, hsl(var(--sidebar-accent))); + box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow); + } + + .hover\:group-data-\[collapsible\=offcanvas\]\:bg-sidebar:hover:is(:where(.group)[data-collapsible="offcanvas"] *) { + background-color: var(--sidebar); + } + + .hover\:after\:bg-sidebar-border:hover:after { + content: var(--tw-content); + background-color: var(--sidebar-border); + } + } + + .focus\:bg-accent:focus { + background-color: var(--accent); + } + + .focus\:text-accent-foreground:focus, :is(.focus\:\*\*\:text-accent-foreground:focus *), :is(.not-data-\[variant\=destructive\]\:focus\:\*\*\:text-accent-foreground:not([data-variant="destructive"]):focus *) { + color: var(--accent-foreground); + } + + .focus-visible\:border-destructive\/40:focus-visible { + border-color: var(--destructive); + } + + @supports (color: color-mix(in lab, red, red)) { + .focus-visible\:border-destructive\/40:focus-visible { + border-color: color-mix(in oklab, var(--destructive) 40%, transparent); + } + } + + .focus-visible\:border-ring:focus-visible { + border-color: var(--ring); + } + + .focus-visible\:ring-0:focus-visible { + --tw-ring-shadow: var(--tw-ring-inset, ) 0 0 0 calc(0px + var(--tw-ring-offset-width)) var(--tw-ring-color, currentcolor); + box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow); + } + + .focus-visible\:ring-2:focus-visible { + --tw-ring-shadow: var(--tw-ring-inset, ) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color, currentcolor); + box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow); + } + + .focus-visible\:ring-3:focus-visible, .focus-visible\:ring-\[3px\]:focus-visible { + --tw-ring-shadow: var(--tw-ring-inset, ) 0 0 0 calc(3px + var(--tw-ring-offset-width)) var(--tw-ring-color, currentcolor); + box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow); + } + + .focus-visible\:ring-destructive\/20:focus-visible { + --tw-ring-color: var(--destructive); + } + + @supports (color: color-mix(in lab, red, red)) { + .focus-visible\:ring-destructive\/20:focus-visible { + --tw-ring-color: color-mix(in oklab, var(--destructive) 20%, transparent); + } + } + + .focus-visible\:ring-ring:focus-visible, .focus-visible\:ring-ring\/50:focus-visible { + --tw-ring-color: var(--ring); + } + + @supports (color: color-mix(in lab, red, red)) { + .focus-visible\:ring-ring\/50:focus-visible { + --tw-ring-color: color-mix(in oklab, var(--ring) 50%, transparent); + } + } + + .focus-visible\:outline-1:focus-visible { + outline-style: var(--tw-outline-style); + outline-width: 1px; + } + + .focus-visible\:outline-ring:focus-visible { + outline-color: var(--ring); + } + + .focus-visible\:outline-none:focus-visible { + --tw-outline-style: none; + outline-style: none; + } + + .active\:bg-primary-active:active { + background-color: var(--primary-active); + } + + .active\:bg-sidebar-accent:active { + background-color: var(--sidebar-accent); + } + + .active\:text-sidebar-accent-foreground:active { + color: var(--sidebar-accent-foreground); + } + + .active\:not-aria-\[haspopup\]\:translate-y-px:active:not([aria-haspopup]) { + --tw-translate-y: 1px; + translate: var(--tw-translate-x) var(--tw-translate-y); + } + + .disabled\:pointer-events-none:disabled { + pointer-events: none; + } + + .disabled\:cursor-not-allowed:disabled { + cursor: not-allowed; + } + + .disabled\:bg-disabled:disabled { + background-color: var(--disabled); + } + + .disabled\:bg-input\/50:disabled { + background-color: var(--input); + } + + @supports (color: color-mix(in lab, red, red)) { + .disabled\:bg-input\/50:disabled { + background-color: color-mix(in oklab, var(--input) 50%, transparent); + } + } + + .disabled\:bg-transparent:disabled { + background-color: #0000; + } + + .disabled\:text-disabled-foreground:disabled { + color: var(--disabled-foreground); + } + + .disabled\:opacity-50:disabled { + opacity: .5; + } + + .disabled\:opacity-\[var\(--disabled-opacity\,0\.5\)\]:disabled { + opacity: var(--disabled-opacity, .5); + } + + :where([data-side="left"]) .in-data-\[side\=left\]\:cursor-w-resize { + cursor: w-resize; + } + + :where([data-side="right"]) .in-data-\[side\=right\]\:cursor-e-resize { + cursor: e-resize; + } + + :where([data-slot="button-group"]) .in-data-\[slot\=button-group\]\:rounded-lg { + border-radius: var(--radius); + } + + :where([data-slot="combobox-content"]) .in-data-\[slot\=combobox-content\]\:focus-within\:border-inherit:focus-within { + border-color: inherit; + } + + :where([data-slot="combobox-content"]) .in-data-\[slot\=combobox-content\]\:focus-within\:ring-0:focus-within { + --tw-ring-shadow: var(--tw-ring-inset, ) 0 0 0 calc(0px + var(--tw-ring-offset-width)) var(--tw-ring-color, currentcolor); + box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow); + } + + :where([data-slot="dialog-content"]) .in-data-\[slot\=dialog-content\]\:rounded-lg\! { + border-radius: var(--radius) !important; + } + + .has-disabled\:bg-input\/50:has(:disabled) { + background-color: var(--input); + } + + @supports (color: color-mix(in lab, red, red)) { + .has-disabled\:bg-input\/50:has(:disabled) { + background-color: color-mix(in oklab, var(--input) 50%, transparent); + } + } + + .has-disabled\:opacity-50:has(:disabled) { + opacity: .5; + } + + .has-aria-expanded\:bg-muted\/50:has([aria-expanded="true"]) { + background-color: var(--muted); + } + + @supports (color: color-mix(in lab, red, red)) { + .has-aria-expanded\:bg-muted\/50:has([aria-expanded="true"]) { + background-color: color-mix(in oklab, var(--muted) 50%, transparent); + } + } + + .has-data-\[icon\=inline-end\]\:pr-1:has([data-icon="inline-end"]) { + padding-right: var(--appearance-spacing); + } + + .has-data-\[icon\=inline-end\]\:pr-1\.5:has([data-icon="inline-end"]) { + padding-right: calc(var(--appearance-spacing) * 1.5); + } + + .has-data-\[icon\=inline-end\]\:pr-2:has([data-icon="inline-end"]) { + padding-right: calc(var(--appearance-spacing) * 2); + } + + .has-data-\[icon\=inline-start\]\:pl-1:has([data-icon="inline-start"]) { + padding-left: var(--appearance-spacing); + } + + .has-data-\[icon\=inline-start\]\:pl-1\.5:has([data-icon="inline-start"]) { + padding-left: calc(var(--appearance-spacing) * 1.5); + } + + .has-data-\[icon\=inline-start\]\:pl-2:has([data-icon="inline-start"]) { + padding-left: calc(var(--appearance-spacing) * 2); + } + + .has-data-\[slot\=alert-dialog-media\]\:grid-rows-\[auto_auto_1fr\]:has([data-slot="alert-dialog-media"]) { + grid-template-rows: auto auto 1fr; + } + + .has-data-\[slot\=alert-dialog-media\]\:gap-x-4:has([data-slot="alert-dialog-media"]) { + column-gap: calc(var(--appearance-spacing) * 4); + } + + .has-data-\[slot\=card-action\]\:grid-cols-\[1fr_auto\]:has([data-slot="card-action"]) { + grid-template-columns: 1fr auto; + } + + .has-data-\[slot\=card-description\]\:grid-rows-\[auto_auto\]:has([data-slot="card-description"]) { + grid-template-rows: auto auto; + } + + .has-data-\[slot\=card-footer\]\:pb-0:has([data-slot="card-footer"]) { + padding-bottom: 0; + } + + .has-data-\[slot\=kbd\]\:pr-1\.5:has([data-slot="kbd"]) { + padding-right: calc(var(--appearance-spacing) * 1.5); + } + + .has-data-\[variant\=inset\]\:bg-sidebar:has([data-variant="inset"]) { + background-color: var(--sidebar); + } + + .has-\[\[data-slot\=input-group-control\]\:focus-visible\]\:border-ring:has([data-slot="input-group-control"]:focus-visible) { + border-color: var(--ring); + } + + .has-\[\[data-slot\=input-group-control\]\:focus-visible\]\:ring-3:has([data-slot="input-group-control"]:focus-visible) { + --tw-ring-shadow: var(--tw-ring-inset, ) 0 0 0 calc(3px + var(--tw-ring-offset-width)) var(--tw-ring-color, currentcolor); + box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow); + } + + .has-\[\[data-slot\=input-group-control\]\:focus-visible\]\:ring-ring\/50:has([data-slot="input-group-control"]:focus-visible) { + --tw-ring-color: var(--ring); + } + + @supports (color: color-mix(in lab, red, red)) { + .has-\[\[data-slot\=input-group-control\]\:focus-visible\]\:ring-ring\/50:has([data-slot="input-group-control"]:focus-visible) { + --tw-ring-color: color-mix(in oklab, var(--ring) 50%, transparent); + } + } + + .has-\[\[data-slot\]\[aria-invalid\=true\]\]\:border-destructive:has([data-slot][aria-invalid="true"]) { + border-color: var(--destructive); + } + + .has-\[\[data-slot\]\[aria-invalid\=true\]\]\:ring-3:has([data-slot][aria-invalid="true"]) { + --tw-ring-shadow: var(--tw-ring-inset, ) 0 0 0 calc(3px + var(--tw-ring-offset-width)) var(--tw-ring-color, currentcolor); + box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow); + } + + .has-\[\[data-slot\]\[aria-invalid\=true\]\]\:ring-destructive\/20:has([data-slot][aria-invalid="true"]) { + --tw-ring-color: var(--destructive); + } + + @supports (color: color-mix(in lab, red, red)) { + .has-\[\[data-slot\]\[aria-invalid\=true\]\]\:ring-destructive\/20:has([data-slot][aria-invalid="true"]) { + --tw-ring-color: color-mix(in oklab, var(--destructive) 20%, transparent); + } + } + + .has-\[\>\[data-align\=block-end\]\]\:h-auto:has( > [data-align="block-end"]) { + height: auto; + } + + .has-\[\>\[data-align\=block-end\]\]\:flex-col:has( > [data-align="block-end"]) { + flex-direction: column; + } + + .has-\[\>\[data-align\=block-start\]\]\:h-auto:has( > [data-align="block-start"]) { + height: auto; + } + + .has-\[\>\[data-align\=block-start\]\]\:flex-col:has( > [data-align="block-start"]) { + flex-direction: column; + } + + .has-\[\>button\]\:mr-\[-0\.3rem\]:has( > button) { + margin-right: -.3rem; + } + + .has-\[\>button\]\:ml-\[-0\.3rem\]:has( > button) { + margin-left: -.3rem; + } + + .has-\[\>img\:first-child\]\:pt-0:has( > img:first-child) { + padding-top: 0; + } + + .has-\[\>kbd\]\:mr-\[-0\.15rem\]:has( > kbd) { + margin-right: -.15rem; + } + + .has-\[\>kbd\]\:ml-\[-0\.15rem\]:has( > kbd) { + margin-left: -.15rem; + } + + .has-\[\>svg\]\:p-0:has( > svg) { + padding: 0; + } + + .has-\[\>textarea\]\:h-auto:has( > textarea) { + height: auto; + } + + .aria-disabled\:pointer-events-none[aria-disabled="true"] { + pointer-events: none; + } + + .aria-disabled\:opacity-50[aria-disabled="true"] { + opacity: .5; + } + + .aria-expanded\:bg-muted[aria-expanded="true"] { + background-color: var(--muted); + } + + .aria-expanded\:bg-secondary[aria-expanded="true"] { + background-color: var(--secondary); + } + + .aria-expanded\:text-foreground[aria-expanded="true"] { + color: var(--foreground); + } + + .aria-expanded\:text-secondary-foreground[aria-expanded="true"] { + color: var(--secondary-foreground); + } + + .aria-expanded\:opacity-100[aria-expanded="true"] { + opacity: 1; + } + + .aria-invalid\:border-destructive[aria-invalid="true"] { + border-color: var(--destructive); + } + + .aria-invalid\:ring-0[aria-invalid="true"] { + --tw-ring-shadow: var(--tw-ring-inset, ) 0 0 0 calc(0px + var(--tw-ring-offset-width)) var(--tw-ring-color, currentcolor); + box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow); + } + + .aria-invalid\:ring-3[aria-invalid="true"] { + --tw-ring-shadow: var(--tw-ring-inset, ) 0 0 0 calc(3px + var(--tw-ring-offset-width)) var(--tw-ring-color, currentcolor); + box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow); + } + + .aria-invalid\:ring-destructive\/20[aria-invalid="true"] { + --tw-ring-color: var(--destructive); + } + + @supports (color: color-mix(in lab, red, red)) { + .aria-invalid\:ring-destructive\/20[aria-invalid="true"] { + --tw-ring-color: color-mix(in oklab, var(--destructive) 20%, transparent); + } + } + + .aria-invalid\:aria-checked\:border-primary[aria-invalid="true"][aria-checked="true"] { + border-color: var(--primary); + } + + .data-ending-style\:opacity-0[data-ending-style] { + opacity: 0; + } + + .data-highlighted\:bg-muted[data-highlighted] { + background-color: var(--muted); + } + + .data-highlighted\:text-foreground[data-highlighted] { + color: var(--foreground); + } + + .data-inset\:pl-7[data-inset] { + padding-left: calc(var(--appearance-spacing) * 7); + } + + .data-placeholder\:text-muted-foreground[data-placeholder] { + color: var(--muted-foreground); + } + + .data-popup-open\:bg-accent[data-popup-open] { + background-color: var(--accent); + } + + .data-popup-open\:text-accent-foreground[data-popup-open] { + color: var(--accent-foreground); + } + + .data-pressed\:bg-background[data-pressed] { + background-color: var(--background); + } + + .data-pressed\:text-foreground[data-pressed] { + color: var(--foreground); + } + + .data-pressed\:shadow-sm[data-pressed] { + --tw-shadow: 0 1px 3px 0 + var(--tw-shadow-color, rgb(0 0 0 / calc(.1 * var(--appearance-shadow-strength)))), 0 1px 2px -1px var(--tw-shadow-color, rgb(0 0 0 / calc(.1 * var(--appearance-shadow-strength)))); + box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow); + } + + .data-starting-style\:opacity-0[data-starting-style] { + opacity: 0; + } + + .data-\[align-trigger\=true\]\:animate-none[data-align-trigger="true"] { + animation: none; + } + + .data-\[side\=bottom\]\:inset-x-0[data-side="bottom"] { + inset-inline: 0; + } + + .data-\[side\=bottom\]\:top-1[data-side="bottom"] { + top: var(--appearance-spacing); + } + + .data-\[side\=bottom\]\:bottom-0[data-side="bottom"] { + bottom: 0; + } + + .data-\[side\=bottom\]\:h-auto[data-side="bottom"] { + height: auto; + } + + .data-\[side\=bottom\]\:border-t[data-side="bottom"] { + border-top-style: var(--tw-border-style); + border-top-width: 1px; + } + + .data-\[side\=bottom\]\:slide-in-from-top-2[data-side="bottom"] { + --tw-enter-translate-y: calc(2*var(--spacing)*-1); + } + + .data-\[side\=bottom\]\:data-ending-style\:translate-y-\[2\.5rem\][data-side="bottom"][data-ending-style], .data-\[side\=bottom\]\:data-starting-style\:translate-y-\[2\.5rem\][data-side="bottom"][data-starting-style] { + --tw-translate-y: 2.5rem; + translate: var(--tw-translate-x) var(--tw-translate-y); + } + + .data-\[side\=inline-end\]\:top-1\/2\![data-side="inline-end"] { + top: 50% !important; + } + + .data-\[side\=inline-end\]\:-left-1[data-side="inline-end"] { + left: calc(var(--appearance-spacing) * -1); + } + + .data-\[side\=inline-end\]\:-translate-y-1\/2[data-side="inline-end"] { + --tw-translate-y: calc(calc(1 / 2 * 100%) * -1); + translate: var(--tw-translate-x) var(--tw-translate-y); + } + + .data-\[side\=inline-end\]\:slide-in-from-left-2[data-side="inline-end"] { + --tw-enter-translate-x: calc(2*var(--spacing)*-1); + } + + .data-\[side\=inline-start\]\:top-1\/2\![data-side="inline-start"] { + top: 50% !important; + } + + .data-\[side\=inline-start\]\:-right-1[data-side="inline-start"] { + right: calc(var(--appearance-spacing) * -1); + } + + .data-\[side\=inline-start\]\:-translate-y-1\/2[data-side="inline-start"] { + --tw-translate-y: calc(calc(1 / 2 * 100%) * -1); + translate: var(--tw-translate-x) var(--tw-translate-y); + } + + .data-\[side\=inline-start\]\:slide-in-from-right-2[data-side="inline-start"] { + --tw-enter-translate-x: calc(2*var(--spacing)); + } + + .data-\[side\=left\]\:inset-y-0[data-side="left"] { + inset-block: 0; + } + + .data-\[side\=left\]\:top-1\/2\![data-side="left"] { + top: 50% !important; + } + + .data-\[side\=left\]\:-right-1[data-side="left"] { + right: calc(var(--appearance-spacing) * -1); + } + + .data-\[side\=left\]\:left-0[data-side="left"] { + left: 0; + } + + .data-\[side\=left\]\:h-full[data-side="left"] { + height: 100%; + } + + .data-\[side\=left\]\:w-3\/4[data-side="left"] { + width: 75%; + } + + .data-\[side\=left\]\:-translate-y-1\/2[data-side="left"] { + --tw-translate-y: calc(calc(1 / 2 * 100%) * -1); + translate: var(--tw-translate-x) var(--tw-translate-y); + } + + .data-\[side\=left\]\:border-r[data-side="left"] { + border-right-style: var(--tw-border-style); + border-right-width: 1px; + } + + .data-\[side\=left\]\:slide-in-from-right-2[data-side="left"] { + --tw-enter-translate-x: calc(2*var(--spacing)); + } + + .data-\[side\=left\]\:group-data-\[collapsible\=offcanvas\]\:left-\[calc\(var\(--sidebar-width\)\*-1\)\][data-side="left"]:is(:where(.group)[data-collapsible="offcanvas"] *) { + left: calc(var(--sidebar-width) * -1); + } + + .data-\[side\=left\]\:data-ending-style\:translate-x-\[-2\.5rem\][data-side="left"][data-ending-style], .data-\[side\=left\]\:data-starting-style\:translate-x-\[-2\.5rem\][data-side="left"][data-starting-style] { + --tw-translate-x: -2.5rem; + translate: var(--tw-translate-x) var(--tw-translate-y); + } + + .data-\[side\=right\]\:inset-y-0[data-side="right"] { + inset-block: 0; + } + + .data-\[side\=right\]\:top-1\/2\![data-side="right"] { + top: 50% !important; + } + + .data-\[side\=right\]\:right-0[data-side="right"] { + right: 0; + } + + .data-\[side\=right\]\:-left-1[data-side="right"] { + left: calc(var(--appearance-spacing) * -1); + } + + .data-\[side\=right\]\:h-full[data-side="right"] { + height: 100%; + } + + .data-\[side\=right\]\:w-3\/4[data-side="right"] { + width: 75%; + } + + .data-\[side\=right\]\:-translate-y-1\/2[data-side="right"] { + --tw-translate-y: calc(calc(1 / 2 * 100%) * -1); + translate: var(--tw-translate-x) var(--tw-translate-y); + } + + .data-\[side\=right\]\:border-l[data-side="right"] { + border-left-style: var(--tw-border-style); + border-left-width: 1px; + } + + .data-\[side\=right\]\:slide-in-from-left-2[data-side="right"] { + --tw-enter-translate-x: calc(2*var(--spacing)*-1); + } + + .data-\[side\=right\]\:group-data-\[collapsible\=offcanvas\]\:right-\[calc\(var\(--sidebar-width\)\*-1\)\][data-side="right"]:is(:where(.group)[data-collapsible="offcanvas"] *) { + right: calc(var(--sidebar-width) * -1); + } + + .data-\[side\=right\]\:data-ending-style\:translate-x-\[2\.5rem\][data-side="right"][data-ending-style], .data-\[side\=right\]\:data-starting-style\:translate-x-\[2\.5rem\][data-side="right"][data-starting-style] { + --tw-translate-x: 2.5rem; + translate: var(--tw-translate-x) var(--tw-translate-y); + } + + .data-\[side\=top\]\:inset-x-0[data-side="top"] { + inset-inline: 0; + } + + .data-\[side\=top\]\:top-0[data-side="top"] { + top: 0; + } + + .data-\[side\=top\]\:-bottom-2\.5[data-side="top"] { + bottom: calc(var(--appearance-spacing) * -2.5); + } + + .data-\[side\=top\]\:h-auto[data-side="top"] { + height: auto; + } + + .data-\[side\=top\]\:border-b[data-side="top"] { + border-bottom-style: var(--tw-border-style); + border-bottom-width: 1px; + } + + .data-\[side\=top\]\:slide-in-from-bottom-2[data-side="top"] { + --tw-enter-translate-y: calc(2*var(--spacing)); + } + + .data-\[side\=top\]\:data-ending-style\:translate-y-\[-2\.5rem\][data-side="top"][data-ending-style], .data-\[side\=top\]\:data-starting-style\:translate-y-\[-2\.5rem\][data-side="top"][data-starting-style] { + --tw-translate-y: -2.5rem; + translate: var(--tw-translate-x) var(--tw-translate-y); + } + + :where(.data-\[size\=default\]\:control-height-default[data-size="default"]) { + height: var(--control-default-height); + } + + .data-\[size\=default\]\:h-\[18\.4px\][data-size="default"] { + height: 18.4px; + } + + .data-\[size\=default\]\:w-\[32px\][data-size="default"] { + width: 32px; + } + + .data-\[size\=default\]\:max-w-xs[data-size="default"] { + max-width: var(--container-xs); + } + + .data-\[size\=lg\]\:size-10[data-size="lg"] { + width: calc(var(--appearance-spacing) * 10); + height: calc(var(--appearance-spacing) * 10); + } + + :where(.data-\[size\=md\]\:type-sidebar-menu-sub-button[data-size="md"]) { + font-size: var(--type-sidebar-menu-sub-button-size); + line-height: var(--type-sidebar-menu-sub-button-leading); + } + + .data-\[size\=sm\]\:size-6[data-size="sm"] { + width: calc(var(--appearance-spacing) * 6); + height: calc(var(--appearance-spacing) * 6); + } + + :where(.data-\[size\=sm\]\:control-height-sm[data-size="sm"]) { + height: var(--control-sm-height); + } + + .data-\[size\=sm\]\:h-\[14px\][data-size="sm"] { + height: 14px; + } + + .data-\[size\=sm\]\:w-\[24px\][data-size="sm"] { + width: 24px; + } + + .data-\[size\=sm\]\:max-w-xs[data-size="sm"] { + max-width: var(--container-xs); + } + + .data-\[size\=sm\]\:gap-3[data-size="sm"] { + gap: calc(var(--appearance-spacing) * 3); + } + + .data-\[size\=sm\]\:rounded-\[min\(var\(--radius-md\)\,10px\)\][data-size="sm"] { + border-radius: min(var(--radius-md), 10px); + } + + .data-\[size\=sm\]\:py-3[data-size="sm"] { + padding-block: calc(var(--appearance-spacing) * 3); + } + + :where(.data-\[size\=sm\]\:type-sidebar-menu-sub-button-sm[data-size="sm"]) { + font-size: var(--type-sidebar-menu-sub-button-sm-size); + line-height: var(--type-sidebar-menu-sub-button-sm-leading); + } + + .data-\[size\=sm\]\:has-data-\[slot\=card-footer\]\:pb-0[data-size="sm"]:has([data-slot="card-footer"]) { + padding-bottom: 0; + } + + :is(.\*\:data-\[slot\=avatar\]\:ring-2 > *)[data-slot="avatar"] { + --tw-ring-shadow: var(--tw-ring-inset, ) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color, currentcolor); + box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow); + } + + :is(.\*\:data-\[slot\=avatar\]\:ring-background > *)[data-slot="avatar"] { + --tw-ring-color: var(--background); + } + + :is(.\*\:data-\[slot\=input-group-addon\]\:pl-2\! > *)[data-slot="input-group-addon"] { + padding-left: calc(var(--appearance-spacing) * 2) !important; + } + + :is(.\*\*\:data-\[slot\=kbd\]\:relative *)[data-slot="kbd"] { + position: relative; + } + + :is(.\*\*\:data-\[slot\=kbd\]\:isolate *)[data-slot="kbd"] { + isolation: isolate; + } + + :is(.\*\*\:data-\[slot\=kbd\]\:z-50 *)[data-slot="kbd"] { + z-index: 50; + } + + :is(.\*\*\:data-\[slot\=kbd\]\:rounded-sm *)[data-slot="kbd"] { + border-radius: calc(var(--radius) * .6); + } + + :is(.\*\:data-\[slot\=select-value\]\:line-clamp-1 > *)[data-slot="select-value"] { + -webkit-line-clamp: 1; + -webkit-box-orient: vertical; + display: -webkit-box; + overflow: hidden; + } + + :is(.\*\:data-\[slot\=select-value\]\:flex > *)[data-slot="select-value"] { + display: flex; + } + + :is(.\*\:data-\[slot\=select-value\]\:items-center > *)[data-slot="select-value"] { + align-items: center; + } + + :is(.\*\:data-\[slot\=select-value\]\:gap-1\.5 > *)[data-slot="select-value"] { + gap: calc(var(--appearance-spacing) * 1.5); + } + + .data-\[state\=delayed-open\]\:animate-in[data-state="delayed-open"] { + animation: enter var(--tw-animation-duration, var(--tw-duration, .15s))var(--tw-ease, ease)var(--tw-animation-delay, 0s)var(--tw-animation-iteration-count, 1)var(--tw-animation-direction, normal)var(--tw-animation-fill-mode, none); + } + + .data-\[state\=delayed-open\]\:fade-in-0[data-state="delayed-open"] { + --tw-enter-opacity: 0; + } + + .data-\[state\=delayed-open\]\:zoom-in-95[data-state="delayed-open"] { + --tw-enter-scale: .95; + } + + .data-\[state\=selected\]\:bg-muted[data-state="selected"] { + background-color: var(--muted); + } + + .data-\[variant\=destructive\]\:text-destructive[data-variant="destructive"] { + color: var(--destructive); + } + + .data-\[variant\=destructive\]\:focus\:bg-destructive\/10[data-variant="destructive"]:focus { + background-color: var(--destructive); + } + + @supports (color: color-mix(in lab, red, red)) { + .data-\[variant\=destructive\]\:focus\:bg-destructive\/10[data-variant="destructive"]:focus { + background-color: color-mix(in oklab, var(--destructive) 10%, transparent); + } + } + + .data-\[variant\=destructive\]\:focus\:text-destructive[data-variant="destructive"]:focus { + color: var(--destructive); + } + + .data-\[variant\=line\]\:rounded-none[data-variant="line"] { + border-radius: 0; + } + + @supports ((-webkit-backdrop-filter: var(--tw)) or (backdrop-filter: var(--tw))) { + .supports-backdrop-filter\:backdrop-blur-xs { + --tw-backdrop-blur: blur(var(--blur-xs)); + -webkit-backdrop-filter: var(--tw-backdrop-blur, ) var(--tw-backdrop-brightness, ) var(--tw-backdrop-contrast, ) var(--tw-backdrop-grayscale, ) var(--tw-backdrop-hue-rotate, ) var(--tw-backdrop-invert, ) var(--tw-backdrop-opacity, ) var(--tw-backdrop-saturate, ) var(--tw-backdrop-sepia, ); + backdrop-filter: var(--tw-backdrop-blur, ) var(--tw-backdrop-brightness, ) var(--tw-backdrop-contrast, ) var(--tw-backdrop-grayscale, ) var(--tw-backdrop-hue-rotate, ) var(--tw-backdrop-invert, ) var(--tw-backdrop-opacity, ) var(--tw-backdrop-saturate, ) var(--tw-backdrop-sepia, ); + } + } + + @media (prefers-reduced-motion: no-preference) { + .motion-safe\:animate-spin { + animation: var(--animate-spin); + } + } + + @media (prefers-reduced-motion: reduce) { + .motion-reduce\:animate-pulse { + animation: var(--animate-pulse); + } + } + + @media (min-width: 40rem) { + .sm\:col-span-2 { + grid-column: span 2 / span 2; + } + + .sm\:flex { + display: flex; + } + + .sm\:max-w-sm { + max-width: var(--container-sm); + } + + .sm\:grid-cols-2 { + grid-template-columns: repeat(2, minmax(0, 1fr)); + } + + .sm\:flex-row { + flex-direction: row; + } + + .sm\:justify-end { + justify-content: flex-end; + } + + .sm\:opacity-0 { + opacity: 0; + } + + .sm\:group-focus-within\/code\:opacity-100:is(:where(.group\/code):focus-within *) { + opacity: 1; + } + + @media (hover: hover) { + .sm\:group-hover\/code\:opacity-100:is(:where(.group\/code):hover *) { + opacity: 1; + } + } + + .sm\:group-data-\[size\=default\]\/alert-dialog-content\:row-span-2:is(:where(.group\/alert-dialog-content)[data-size="default"] *) { + grid-row: span 2 / span 2; + } + + .sm\:group-data-\[size\=default\]\/alert-dialog-content\:place-items-start:is(:where(.group\/alert-dialog-content)[data-size="default"] *) { + place-items: start; + } + + .sm\:group-data-\[size\=default\]\/alert-dialog-content\:text-left:is(:where(.group\/alert-dialog-content)[data-size="default"] *) { + text-align: left; + } + + .sm\:group-data-\[size\=default\]\/alert-dialog-content\:group-has-data-\[slot\=alert-dialog-media\]\/alert-dialog-content\:col-start-2:is(:where(.group\/alert-dialog-content)[data-size="default"] *):is(:where(.group\/alert-dialog-content):has([data-slot="alert-dialog-media"]) *) { + grid-column-start: 2; + } + + .sm\:group-data-\[size\=default\]\/alert-dialog-content\:has-data-\[slot\=alert-dialog-media\]\:grid-rows-\[auto_1fr\]:is(:where(.group\/alert-dialog-content)[data-size="default"] *):has([data-slot="alert-dialog-media"]) { + grid-template-rows: auto 1fr; + } + + .data-\[side\=left\]\:sm\:max-w-sm[data-side="left"], .data-\[side\=right\]\:sm\:max-w-sm[data-side="right"], .data-\[size\=default\]\:sm\:max-w-sm[data-size="default"] { + max-width: var(--container-sm); + } + } + + @media (min-width: 48rem) { + .md\:block { + display: block; + } + + .md\:flex { + display: flex; + } + + :where(.md\:type-input) { + font-size: var(--type-input-size); + line-height: var(--type-input-leading); + } + + :where(.md\:type-textarea) { + font-size: var(--type-textarea-size); + line-height: var(--type-textarea-leading); + } + + .md\:text-pretty { + text-wrap: pretty; + } + + .md\:opacity-0 { + opacity: 0; + } + + .md\:peer-data-\[variant\=inset\]\:m-2:is(:where(.peer)[data-variant="inset"] ~ *) { + margin: calc(var(--appearance-spacing) * 2); + } + + .md\:peer-data-\[variant\=inset\]\:ml-0:is(:where(.peer)[data-variant="inset"] ~ *) { + margin-left: 0; + } + + .md\:peer-data-\[variant\=inset\]\:rounded-xl:is(:where(.peer)[data-variant="inset"] ~ *) { + border-radius: calc(var(--radius) * 1.4); + } + + .md\:peer-data-\[variant\=inset\]\:shadow-sm:is(:where(.peer)[data-variant="inset"] ~ *) { + --tw-shadow: 0 1px 3px 0 + var(--tw-shadow-color, rgb(0 0 0 / calc(.1 * var(--appearance-shadow-strength)))), 0 1px 2px -1px var(--tw-shadow-color, rgb(0 0 0 / calc(.1 * var(--appearance-shadow-strength)))); + box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow); + } + + .md\:peer-data-\[variant\=inset\]\:peer-data-\[state\=collapsed\]\:ml-2:is(:where(.peer)[data-variant="inset"] ~ *):is(:where(.peer)[data-state="collapsed"] ~ *) { + margin-left: calc(var(--appearance-spacing) * 2); + } + + .md\:after\:hidden:after { + content: var(--tw-content); + display: none; + } + } + + @media (min-width: 64rem) { + .lg\:col-span-3 { + grid-column: span 3 / span 3; + } + + .lg\:col-span-4 { + grid-column: span 4 / span 4; + } + + .lg\:grid-cols-3 { + grid-template-columns: repeat(3, minmax(0, 1fr)); + } + + .lg\:grid-cols-4 { + grid-template-columns: repeat(4, minmax(0, 1fr)); + } + } + + .ltr\:-translate-x-1\/2:where(:dir(ltr), [dir="ltr"], [dir="ltr"] *), .rtl\:-translate-x-1\/2:where(:dir(rtl), [dir="rtl"], [dir="rtl"] *) { + --tw-translate-x: calc(calc(1 / 2 * 100%) * -1); + translate: var(--tw-translate-x) var(--tw-translate-y); + } + + .dark\:border-input:is(.dark *) { + border-color: var(--input); + } + + .dark\:bg-amber-400:is(.dark *) { + background-color: var(--color-amber-400); + } + + .dark\:bg-destructive\/20:is(.dark *) { + background-color: var(--destructive); + } + + @supports (color: color-mix(in lab, red, red)) { + .dark\:bg-destructive\/20:is(.dark *) { + background-color: color-mix(in oklab, var(--destructive) 20%, transparent); + } + } + + .dark\:bg-info\/20:is(.dark *) { + background-color: var(--info); + } + + @supports (color: color-mix(in lab, red, red)) { + .dark\:bg-info\/20:is(.dark *) { + background-color: color-mix(in oklab, var(--info) 20%, transparent); + } + } + + .dark\:bg-input\/30:is(.dark *) { + background-color: var(--input); + } + + @supports (color: color-mix(in lab, red, red)) { + .dark\:bg-input\/30:is(.dark *) { + background-color: color-mix(in oklab, var(--input) 30%, transparent); + } + } + + .dark\:bg-success\/20:is(.dark *) { + background-color: var(--success); + } + + @supports (color: color-mix(in lab, red, red)) { + .dark\:bg-success\/20:is(.dark *) { + background-color: color-mix(in oklab, var(--success) 20%, transparent); + } + } + + .dark\:bg-transparent:is(.dark *) { + background-color: #0000; + } + + .dark\:bg-warning\/20:is(.dark *) { + background-color: var(--warning); + } + + @supports (color: color-mix(in lab, red, red)) { + .dark\:bg-warning\/20:is(.dark *) { + background-color: color-mix(in oklab, var(--warning) 20%, transparent); + } + } + + .dark\:text-muted-foreground:is(.dark *) { + color: var(--muted-foreground); + } + + .dark\:after\:mix-blend-lighten:is(.dark *):after { + content: var(--tw-content); + mix-blend-mode: lighten; + } + + @media (hover: hover) { + .dark\:hover\:bg-destructive\/30:is(.dark *):hover { + background-color: var(--destructive); + } + + @supports (color: color-mix(in lab, red, red)) { + .dark\:hover\:bg-destructive\/30:is(.dark *):hover { + background-color: color-mix(in oklab, var(--destructive) 30%, transparent); + } + } + + .dark\:hover\:bg-input\/50:is(.dark *):hover { + background-color: var(--input); + } + + @supports (color: color-mix(in lab, red, red)) { + .dark\:hover\:bg-input\/50:is(.dark *):hover { + background-color: color-mix(in oklab, var(--input) 50%, transparent); + } + } + + .dark\:hover\:bg-muted\/50:is(.dark *):hover { + background-color: var(--muted); + } + + @supports (color: color-mix(in lab, red, red)) { + .dark\:hover\:bg-muted\/50:is(.dark *):hover { + background-color: color-mix(in oklab, var(--muted) 50%, transparent); + } + } + + .dark\:hover\:text-foreground:is(.dark *):hover { + color: var(--foreground); + } + } + + .dark\:focus-visible\:ring-destructive\/40:is(.dark *):focus-visible { + --tw-ring-color: var(--destructive); + } + + @supports (color: color-mix(in lab, red, red)) { + .dark\:focus-visible\:ring-destructive\/40:is(.dark *):focus-visible { + --tw-ring-color: color-mix(in oklab, var(--destructive) 40%, transparent); + } + } + + .dark\:disabled\:bg-input\/80:is(.dark *):disabled { + background-color: var(--input); + } + + @supports (color: color-mix(in lab, red, red)) { + .dark\:disabled\:bg-input\/80:is(.dark *):disabled { + background-color: color-mix(in oklab, var(--input) 80%, transparent); + } + } + + .dark\:disabled\:bg-transparent:is(.dark *):disabled { + background-color: #0000; + } + + .dark\:has-disabled\:bg-input\/80:is(.dark *):has(:disabled) { + background-color: var(--input); + } + + @supports (color: color-mix(in lab, red, red)) { + .dark\:has-disabled\:bg-input\/80:is(.dark *):has(:disabled) { + background-color: color-mix(in oklab, var(--input) 80%, transparent); + } + } + + .dark\:has-\[\[data-slot\]\[aria-invalid\=true\]\]\:ring-destructive\/40:is(.dark *):has([data-slot][aria-invalid="true"]) { + --tw-ring-color: var(--destructive); + } + + @supports (color: color-mix(in lab, red, red)) { + .dark\:has-\[\[data-slot\]\[aria-invalid\=true\]\]\:ring-destructive\/40:is(.dark *):has([data-slot][aria-invalid="true"]) { + --tw-ring-color: color-mix(in oklab, var(--destructive) 40%, transparent); + } + } + + .dark\:aria-invalid\:border-destructive\/50:is(.dark *)[aria-invalid="true"] { + border-color: var(--destructive); + } + + @supports (color: color-mix(in lab, red, red)) { + .dark\:aria-invalid\:border-destructive\/50:is(.dark *)[aria-invalid="true"] { + border-color: color-mix(in oklab, var(--destructive) 50%, transparent); + } + } + + .dark\:aria-invalid\:ring-destructive\/40:is(.dark *)[aria-invalid="true"] { + --tw-ring-color: var(--destructive); + } + + @supports (color: color-mix(in lab, red, red)) { + .dark\:aria-invalid\:ring-destructive\/40:is(.dark *)[aria-invalid="true"] { + --tw-ring-color: color-mix(in oklab, var(--destructive) 40%, transparent); + } + } + + .dark\:data-\[variant\=destructive\]\:focus\:bg-destructive\/20:is(.dark *)[data-variant="destructive"]:focus { + background-color: var(--destructive); + } + + @supports (color: color-mix(in lab, red, red)) { + .dark\:data-\[variant\=destructive\]\:focus\:bg-destructive\/20:is(.dark *)[data-variant="destructive"]:focus { + background-color: color-mix(in oklab, var(--destructive) 20%, transparent); + } + } + + .data-open\:animate-in:where([data-state="open"]), .data-open\:animate-in:where([data-open]:not([data-open="false"])) { + animation: enter var(--tw-animation-duration, var(--tw-duration, .15s))var(--tw-ease, ease)var(--tw-animation-delay, 0s)var(--tw-animation-iteration-count, 1)var(--tw-animation-direction, normal)var(--tw-animation-fill-mode, none); + } + + .data-open\:bg-accent:where([data-state="open"]), .data-open\:bg-accent:where([data-open]:not([data-open="false"])) { + background-color: var(--accent); + } + + .data-open\:text-accent-foreground:where([data-state="open"]), .data-open\:text-accent-foreground:where([data-open]:not([data-open="false"])) { + color: var(--accent-foreground); + } + + .data-open\:fade-in-0:where([data-state="open"]), .data-open\:fade-in-0:where([data-open]:not([data-open="false"])) { + --tw-enter-opacity: 0; + } + + .data-open\:zoom-in-95:where([data-state="open"]), .data-open\:zoom-in-95:where([data-open]:not([data-open="false"])) { + --tw-enter-scale: .95; + } + + @media (hover: hover) { + :is(.data-open\:hover\:bg-sidebar-accent:where([data-state="open"]), .data-open\:hover\:bg-sidebar-accent:where([data-open]:not([data-open="false"]))):hover { + background-color: var(--sidebar-accent); + } + + :is(.data-open\:hover\:text-sidebar-accent-foreground:where([data-state="open"]), .data-open\:hover\:text-sidebar-accent-foreground:where([data-open]:not([data-open="false"]))):hover { + color: var(--sidebar-accent-foreground); + } + } + + .data-closed\:animate-out:where([data-state="closed"]), .data-closed\:animate-out:where([data-closed]:not([data-closed="false"])) { + animation: exit var(--tw-animation-duration, var(--tw-duration, .15s))var(--tw-ease, ease)var(--tw-animation-delay, 0s)var(--tw-animation-iteration-count, 1)var(--tw-animation-direction, normal)var(--tw-animation-fill-mode, none); + } + + .data-closed\:overflow-hidden:where([data-state="closed"]), .data-closed\:overflow-hidden:where([data-closed]:not([data-closed="false"])) { + overflow: hidden; + } + + .data-closed\:fade-out-0:where([data-state="closed"]), .data-closed\:fade-out-0:where([data-closed]:not([data-closed="false"])) { + --tw-exit-opacity: 0; + } + + .data-closed\:zoom-out-95:where([data-state="closed"]), .data-closed\:zoom-out-95:where([data-closed]:not([data-closed="false"])) { + --tw-exit-scale: .95; + } + + .data-checked\:border-primary:where([data-state="checked"]), .data-checked\:border-primary:where([data-checked]:not([data-checked="false"])) { + border-color: var(--primary); + } + + .data-checked\:bg-primary:where([data-state="checked"]), .data-checked\:bg-primary:where([data-checked]:not([data-checked="false"])) { + background-color: var(--primary); + } + + .data-checked\:text-primary-foreground:where([data-state="checked"]), .data-checked\:text-primary-foreground:where([data-checked]:not([data-checked="false"])) { + color: var(--primary-foreground); + } + + .group-data-\[size\=default\]\/switch\:data-checked\:translate-x-\[calc\(100\%-2px\)\]:is(:where(.group\/switch)[data-size="default"] *):where([data-state="checked"]), .group-data-\[size\=default\]\/switch\:data-checked\:translate-x-\[calc\(100\%-2px\)\]:is(:where(.group\/switch)[data-size="default"] *):where([data-checked]:not([data-checked="false"])), .group-data-\[size\=sm\]\/switch\:data-checked\:translate-x-\[calc\(100\%-2px\)\]:is(:where(.group\/switch)[data-size="sm"] *):where([data-state="checked"]), .group-data-\[size\=sm\]\/switch\:data-checked\:translate-x-\[calc\(100\%-2px\)\]:is(:where(.group\/switch)[data-size="sm"] *):where([data-checked]:not([data-checked="false"])) { + --tw-translate-x: calc(100% - 2px); + translate: var(--tw-translate-x) var(--tw-translate-y); + } + + .dark\:data-checked\:bg-primary:is(.dark *):where([data-state="checked"]), .dark\:data-checked\:bg-primary:is(.dark *):where([data-checked]:not([data-checked="false"])) { + background-color: var(--primary); + } + + .dark\:data-checked\:bg-primary-foreground:is(.dark *):where([data-state="checked"]), .dark\:data-checked\:bg-primary-foreground:is(.dark *):where([data-checked]:not([data-checked="false"])) { + background-color: var(--primary-foreground); + } + + .data-unchecked\:bg-input:where([data-state="unchecked"]), .data-unchecked\:bg-input:where([data-unchecked]:not([data-unchecked="false"])) { + background-color: var(--input); + } + + .group-data-\[size\=default\]\/switch\:data-unchecked\:translate-x-0:is(:where(.group\/switch)[data-size="default"] *):where([data-state="unchecked"]), .group-data-\[size\=default\]\/switch\:data-unchecked\:translate-x-0:is(:where(.group\/switch)[data-size="default"] *):where([data-unchecked]:not([data-unchecked="false"])), .group-data-\[size\=sm\]\/switch\:data-unchecked\:translate-x-0:is(:where(.group\/switch)[data-size="sm"] *):where([data-state="unchecked"]), .group-data-\[size\=sm\]\/switch\:data-unchecked\:translate-x-0:is(:where(.group\/switch)[data-size="sm"] *):where([data-unchecked]:not([data-unchecked="false"])) { + --tw-translate-x: 0px; + translate: var(--tw-translate-x) var(--tw-translate-y); + } + + .dark\:data-unchecked\:bg-foreground:is(.dark *):where([data-state="unchecked"]), .dark\:data-unchecked\:bg-foreground:is(.dark *):where([data-unchecked]:not([data-unchecked="false"])) { + background-color: var(--foreground); + } + + .dark\:data-unchecked\:bg-input\/80:is(.dark *):where([data-state="unchecked"]), .dark\:data-unchecked\:bg-input\/80:is(.dark *):where([data-unchecked]:not([data-unchecked="false"])) { + background-color: var(--input); + } + + @supports (color: color-mix(in lab, red, red)) { + .dark\:data-unchecked\:bg-input\/80:is(.dark *):where([data-state="unchecked"]), .dark\:data-unchecked\:bg-input\/80:is(.dark *):where([data-unchecked]:not([data-unchecked="false"])) { + background-color: color-mix(in oklab, var(--input) 80%, transparent); + } + } + + .data-disabled\:pointer-events-none:where([data-disabled="true"]), .data-disabled\:pointer-events-none:where([data-disabled]:not([data-disabled="false"])) { + pointer-events: none; + } + + .data-disabled\:cursor-not-allowed:where([data-disabled="true"]), .data-disabled\:cursor-not-allowed:where([data-disabled]:not([data-disabled="false"])) { + cursor: not-allowed; + } + + .data-disabled\:opacity-50:where([data-disabled="true"]), .data-disabled\:opacity-50:where([data-disabled]:not([data-disabled="false"])) { + opacity: .5; + } + + .data-active\:bg-background:where([data-state="active"]), .data-active\:bg-background:where([data-active]:not([data-active="false"])) { + background-color: var(--background); + } + + .data-active\:bg-sidebar-accent:where([data-state="active"]), .data-active\:bg-sidebar-accent:where([data-active]:not([data-active="false"])) { + background-color: var(--sidebar-accent); + } + + :where(:is(.data-active\:type-sidebar-menu-button-active:where([data-state="active"]), .data-active\:type-sidebar-menu-button-active:where([data-active]:not([data-active="false"])))) { + font-weight: var(--type-sidebar-menu-button-active-weight); + } + + .data-active\:text-foreground:where([data-state="active"]), .data-active\:text-foreground:where([data-active]:not([data-active="false"])) { + color: var(--foreground); + } + + .data-active\:text-sidebar-accent-foreground:where([data-state="active"]), .data-active\:text-sidebar-accent-foreground:where([data-active]:not([data-active="false"])) { + color: var(--sidebar-accent-foreground); + } + + .group-data-\[variant\=default\]\/tabs-list\:data-active\:shadow-sm:is(:where(.group\/tabs-list)[data-variant="default"] *):where([data-state="active"]), .group-data-\[variant\=default\]\/tabs-list\:data-active\:shadow-sm:is(:where(.group\/tabs-list)[data-variant="default"] *):where([data-active]:not([data-active="false"])) { + --tw-shadow: 0 1px 3px 0 + var(--tw-shadow-color, rgb(0 0 0 / calc(.1 * var(--appearance-shadow-strength)))), 0 1px 2px -1px var(--tw-shadow-color, rgb(0 0 0 / calc(.1 * var(--appearance-shadow-strength)))); + box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow); + } + + .group-data-\[variant\=line\]\/tabs-list\:data-active\:bg-transparent:is(:where(.group\/tabs-list)[data-variant="line"] *):where([data-state="active"]), .group-data-\[variant\=line\]\/tabs-list\:data-active\:bg-transparent:is(:where(.group\/tabs-list)[data-variant="line"] *):where([data-active]:not([data-active="false"])) { + background-color: #0000; + } + + .group-data-\[variant\=line\]\/tabs-list\:data-active\:shadow-none:is(:where(.group\/tabs-list)[data-variant="line"] *):where([data-state="active"]), .group-data-\[variant\=line\]\/tabs-list\:data-active\:shadow-none:is(:where(.group\/tabs-list)[data-variant="line"] *):where([data-active]:not([data-active="false"])) { + --tw-shadow: 0 0 #0000; + box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow); + } + + :is(.group-data-\[variant\=line\]\/tabs-list\:data-active\:after\:opacity-100:is(:where(.group\/tabs-list)[data-variant="line"] *):where([data-state="active"]), .group-data-\[variant\=line\]\/tabs-list\:data-active\:after\:opacity-100:is(:where(.group\/tabs-list)[data-variant="line"] *):where([data-active]:not([data-active="false"]))):after { + content: var(--tw-content); + opacity: 1; + } + + .dark\:data-active\:border-input:is(.dark *):where([data-state="active"]), .dark\:data-active\:border-input:is(.dark *):where([data-active]:not([data-active="false"])) { + border-color: var(--input); + } + + .dark\:data-active\:bg-input\/30:is(.dark *):where([data-state="active"]), .dark\:data-active\:bg-input\/30:is(.dark *):where([data-active]:not([data-active="false"])) { + background-color: var(--input); + } + + @supports (color: color-mix(in lab, red, red)) { + .dark\:data-active\:bg-input\/30:is(.dark *):where([data-state="active"]), .dark\:data-active\:bg-input\/30:is(.dark *):where([data-active]:not([data-active="false"])) { + background-color: color-mix(in oklab, var(--input) 30%, transparent); + } + } + + .dark\:data-active\:text-foreground:is(.dark *):where([data-state="active"]), .dark\:data-active\:text-foreground:is(.dark *):where([data-active]:not([data-active="false"])) { + color: var(--foreground); + } + + .dark\:group-data-\[variant\=line\]\/tabs-list\:data-active\:border-transparent:is(.dark *):is(:where(.group\/tabs-list)[data-variant="line"] *):where([data-state="active"]), .dark\:group-data-\[variant\=line\]\/tabs-list\:data-active\:border-transparent:is(.dark *):is(:where(.group\/tabs-list)[data-variant="line"] *):where([data-active]:not([data-active="false"])) { + border-color: #0000; + } + + .dark\:group-data-\[variant\=line\]\/tabs-list\:data-active\:bg-transparent:is(.dark *):is(:where(.group\/tabs-list)[data-variant="line"] *):where([data-state="active"]), .dark\:group-data-\[variant\=line\]\/tabs-list\:data-active\:bg-transparent:is(.dark *):is(:where(.group\/tabs-list)[data-variant="line"] *):where([data-active]:not([data-active="false"])) { + background-color: #0000; + } + + .data-horizontal\:h-px:where([data-orientation="horizontal"]) { + height: 1px; + } + + .data-horizontal\:w-full:where([data-orientation="horizontal"]) { + width: 100%; + } + + .data-horizontal\:flex-col:where([data-orientation="horizontal"]) { + flex-direction: column; + } + + .data-vertical\:w-px:where([data-orientation="vertical"]) { + width: 1px; + } + + .data-vertical\:self-stretch:where([data-orientation="vertical"]) { + align-self: stretch; + } + + .\[\&_\[data-slot\=banner-icon\]\]\:text-destructive [data-slot="banner-icon"] { + color: var(--destructive); + } + + .\[\&_\[data-slot\=banner-icon\]\]\:text-info [data-slot="banner-icon"] { + color: var(--info); + } + + .\[\&_\[data-slot\=banner-icon\]\]\:text-success [data-slot="banner-icon"] { + color: var(--success); + } + + .\[\&_\[data-slot\=banner-icon\]\]\:text-warning [data-slot="banner-icon"] { + color: var(--warning); + } + + .\[\&_a\]\:underline a { + text-decoration-line: underline; + } + + .\[\&_a\]\:underline-offset-4 a { + text-underline-offset: 4px; + } + + .\[\&_a\:hover\]\:text-foreground a:hover { + color: var(--foreground); + } + + .\[\&_svg\]\:pointer-events-none svg { + pointer-events: none; + } + + .\[\&_svg\]\:size-3 svg { + width: calc(var(--appearance-spacing) * 3); + height: calc(var(--appearance-spacing) * 3); + } + + .\[\&_svg\]\:size-3\.5 svg { + width: calc(var(--appearance-spacing) * 3.5); + height: calc(var(--appearance-spacing) * 3.5); + } + + .\[\&_svg\]\:size-4 svg { + width: calc(var(--appearance-spacing) * 4); + height: calc(var(--appearance-spacing) * 4); + } + + .\[\&_svg\]\:size-6 svg { + width: calc(var(--appearance-spacing) * 6); + height: calc(var(--appearance-spacing) * 6); + } + + .\[\&_svg\]\:size-10 svg { + width: calc(var(--appearance-spacing) * 10); + height: calc(var(--appearance-spacing) * 10); + } + + .\[\&_svg\]\:shrink-0 svg { + flex-shrink: 0; + } + + .\[\&_svg\]\:translate-y-0\.5 svg { + --tw-translate-y: calc(var(--appearance-spacing) * .5); + translate: var(--tw-translate-x) var(--tw-translate-y); + } + + .\[\&_svg\]\:\[stroke-width\:1\.5\] svg { + stroke-width: 1.5px; + } + + .\[\&_svg\:not\(\[class\*\=\'size-\'\]\)\]\:size-4 svg:not([class*="size-"]) { + width: calc(var(--appearance-spacing) * 4); + height: calc(var(--appearance-spacing) * 4); + } + + :where(.\[\&_svg\:not\(\[class\*\=\'size-\'\]\)\]\:control-glyph-default svg:not([class*="size-"])) { + width: var(--control-default-icon); + height: var(--control-default-icon); + } + + :where(.\[\&_svg\:not\(\[class\*\=\'size-\'\]\)\]\:control-glyph-lg svg:not([class*="size-"])) { + width: var(--control-lg-icon); + height: var(--control-lg-icon); + } + + :where(.\[\&_svg\:not\(\[class\*\=\'size-\'\]\)\]\:control-glyph-sm svg:not([class*="size-"])) { + width: var(--control-sm-icon); + height: var(--control-sm-icon); + } + + :where(.\[\&_svg\:not\(\[class\*\=\'size-\'\]\)\]\:control-glyph-xs svg:not([class*="size-"])) { + width: var(--control-xs-icon); + height: var(--control-xs-icon); + } + + .\[\&_tr\]\:border-b tr { + border-bottom-style: var(--tw-border-style); + border-bottom-width: 1px; + } + + .\[\&_tr\:last-child\]\:border-0 tr:last-child { + border-style: var(--tw-border-style); + border-width: 0; + } + + .\[\&\:has\(\[role\=checkbox\]\)\]\:pr-0:has([role="checkbox"]) { + padding-right: 0; + } + + .\[\.border-b\]\:pb-2.border-b { + padding-bottom: calc(var(--appearance-spacing) * 2); + } + + .\[\.border-b\]\:pb-4.border-b { + padding-bottom: calc(var(--appearance-spacing) * 4); + } + + .group-data-\[size\=sm\]\/card\:\[\.border-b\]\:pb-3:is(:where(.group\/card)[data-size="sm"] *).border-b { + padding-bottom: calc(var(--appearance-spacing) * 3); + } + + .\[\.border-t\]\:pt-2.border-t { + padding-top: calc(var(--appearance-spacing) * 2); + } + + :is(.\*\:\[a\]\:underline > *):is(a) { + text-decoration-line: underline; + } + + :is(.\*\:\[a\]\:underline-offset-3 > *):is(a) { + text-underline-offset: 3px; + } + + @media (hover: hover) { + .\[a\]\:hover\:bg-destructive\/20:is(a):hover { + background-color: var(--destructive); + } + + @supports (color: color-mix(in lab, red, red)) { + .\[a\]\:hover\:bg-destructive\/20:is(a):hover { + background-color: color-mix(in oklab, var(--destructive) 20%, transparent); + } + } + + .\[a\]\:hover\:bg-info\/20:is(a):hover { + background-color: var(--info); + } + + @supports (color: color-mix(in lab, red, red)) { + .\[a\]\:hover\:bg-info\/20:is(a):hover { + background-color: color-mix(in oklab, var(--info) 20%, transparent); + } + } + + .\[a\]\:hover\:bg-muted:is(a):hover, .\[a\]\:hover\:bg-muted\/80:is(a):hover { + background-color: var(--muted); + } + + @supports (color: color-mix(in lab, red, red)) { + .\[a\]\:hover\:bg-muted\/80:is(a):hover { + background-color: color-mix(in oklab, var(--muted) 80%, transparent); + } + } + + .\[a\]\:hover\:bg-primary\/80:is(a):hover { + background-color: var(--primary); + } + + @supports (color: color-mix(in lab, red, red)) { + .\[a\]\:hover\:bg-primary\/80:is(a):hover { + background-color: color-mix(in oklab, var(--primary) 80%, transparent); + } + } + + .\[a\]\:hover\:bg-secondary\/80:is(a):hover { + background-color: var(--secondary); + } + + @supports (color: color-mix(in lab, red, red)) { + .\[a\]\:hover\:bg-secondary\/80:is(a):hover { + background-color: color-mix(in oklab, var(--secondary) 80%, transparent); + } + } + + .\[a\]\:hover\:bg-success\/20:is(a):hover { + background-color: var(--success); + } + + @supports (color: color-mix(in lab, red, red)) { + .\[a\]\:hover\:bg-success\/20:is(a):hover { + background-color: color-mix(in oklab, var(--success) 20%, transparent); + } + } + + .\[a\]\:hover\:bg-warning\/20:is(a):hover { + background-color: var(--warning); + } + + @supports (color: color-mix(in lab, red, red)) { + .\[a\]\:hover\:bg-warning\/20:is(a):hover { + background-color: color-mix(in oklab, var(--warning) 20%, transparent); + } + } + + .\[a\]\:hover\:text-muted-foreground:is(a):hover { + color: var(--muted-foreground); + } + + :is(.\*\:\[a\]\:hover\:text-foreground > *):is(a):hover { + color: var(--foreground); + } + } + + :is(.\*\:\[img\:first-child\]\:rounded-t-xl > *):is(img:first-child) { + border-top-left-radius: calc(var(--radius) * 1.4); + border-top-right-radius: calc(var(--radius) * 1.4); + } + + :is(.\*\:\[img\:last-child\]\:rounded-b-xl > *):is(img:last-child) { + border-bottom-right-radius: calc(var(--radius) * 1.4); + border-bottom-left-radius: calc(var(--radius) * 1.4); + } + + :is(.\*\:\[span\]\:last\:flex > *):is(span):last-child { + display: flex; + } + + :is(.\*\:\[span\]\:last\:items-center > *):is(span):last-child { + align-items: center; + } + + :is(.\*\:\[span\]\:last\:gap-2 > *):is(span):last-child { + gap: calc(var(--appearance-spacing) * 2); + } + + :is(.data-highlighted\:\*\:\[svg\]\:text-foreground[data-highlighted] > *):is(svg) { + color: var(--foreground); + } + + :is(.data-\[variant\=destructive\]\:\*\:\[svg\]\:text-destructive[data-variant="destructive"] > *):is(svg) { + color: var(--destructive); + } + + :is(.\*\:\[svg\:not\(\[class\*\=\'size-\'\]\)\]\:size-6 > *):is(svg:not([class*="size-"])) { + width: calc(var(--appearance-spacing) * 6); + height: calc(var(--appearance-spacing) * 6); + } + + .\[\&\>button\]\:hidden > button { + display: none; + } + + .has-\[\>\[data-align\=block-end\]\]\:\[\&\>input\]\:pt-3:has( > [data-align="block-end"]) > input { + padding-top: calc(var(--appearance-spacing) * 3); + } + + .has-\[\>\[data-align\=block-start\]\]\:\[\&\>input\]\:pb-3:has( > [data-align="block-start"]) > input { + padding-bottom: calc(var(--appearance-spacing) * 3); + } + + .has-\[\>\[data-align\=inline-end\]\]\:\[\&\>input\]\:pr-1\.5:has( > [data-align="inline-end"]) > input { + padding-right: calc(var(--appearance-spacing) * 1.5); + } + + .has-\[\>\[data-align\=inline-start\]\]\:\[\&\>input\]\:pl-1\.5:has( > [data-align="inline-start"]) > input { + padding-left: calc(var(--appearance-spacing) * 1.5); + } + + .\[\&\>kbd\]\:rounded-\[calc\(var\(--radius\)-5px\)\] > kbd { + border-radius: calc(var(--radius) - 5px); + } + + .\[\&\>li\]\:py-2 > li { + padding-block: calc(var(--appearance-spacing) * 2); + } + + .\[\&\>li\:first-child\]\:pt-0 > li:first-child { + padding-top: 0; + } + + .\[\&\>li\:last-child\]\:pb-0 > li:last-child { + padding-bottom: 0; + } + + .\[\&\>span\:last-child\]\:truncate > span:last-child { + text-overflow: ellipsis; + white-space: nowrap; + overflow: hidden; + } + + .\[\&\>svg\]\:pointer-events-none > svg { + pointer-events: none; + } + + .\[\&\>svg\]\:size-3\! > svg { + width: calc(var(--appearance-spacing) * 3) !important; + height: calc(var(--appearance-spacing) * 3) !important; + } + + .\[\&\>svg\]\:size-3\.5 > svg { + width: calc(var(--appearance-spacing) * 3.5); + height: calc(var(--appearance-spacing) * 3.5); + } + + .\[\&\>svg\]\:size-3\.5\! > svg { + width: calc(var(--appearance-spacing) * 3.5) !important; + height: calc(var(--appearance-spacing) * 3.5) !important; + } + + .\[\&\>svg\]\:size-4 > svg { + width: calc(var(--appearance-spacing) * 4); + height: calc(var(--appearance-spacing) * 4); + } + + .\[\&\>svg\]\:shrink-0 > svg { + flex-shrink: 0; + } + + .\[\&\>svg\]\:text-sidebar-accent-foreground > svg { + color: var(--sidebar-accent-foreground); + } + + .group-has-data-\[size\=lg\]\/avatar-group\:\[\&\>svg\]\:size-5:is(:where(.group\/avatar-group):has([data-size="lg"]) *) > svg { + width: calc(var(--appearance-spacing) * 5); + height: calc(var(--appearance-spacing) * 5); + } + + .group-has-data-\[size\=sm\]\/avatar-group\:\[\&\>svg\]\:size-3:is(:where(.group\/avatar-group):has([data-size="sm"]) *) > svg { + width: calc(var(--appearance-spacing) * 3); + height: calc(var(--appearance-spacing) * 3); + } + + .group-data-\[size\=default\]\/avatar\:\[\&\>svg\]\:size-2:is(:where(.group\/avatar)[data-size="default"] *) > svg, .group-data-\[size\=lg\]\/avatar\:\[\&\>svg\]\:size-2:is(:where(.group\/avatar)[data-size="lg"] *) > svg { + width: calc(var(--appearance-spacing) * 2); + height: calc(var(--appearance-spacing) * 2); + } + + .group-data-\[size\=sm\]\/avatar\:\[\&\>svg\]\:hidden:is(:where(.group\/avatar)[data-size="sm"] *) > svg { + display: none; + } + + .\[\&\>svg\:not\(\[class\*\=\'size-\'\]\)\]\:size-3\.5 > svg:not([class*="size-"]) { + width: calc(var(--appearance-spacing) * 3.5); + height: calc(var(--appearance-spacing) * 3.5); + } + + .\[\&\>svg\:not\(\[class\*\=\'size-\'\]\)\]\:size-4 > svg:not([class*="size-"]) { + width: calc(var(--appearance-spacing) * 4); + height: calc(var(--appearance-spacing) * 4); + } + + .\[\&\>tr\]\:last\:border-b-0 > tr:last-child { + border-bottom-style: var(--tw-border-style); + border-bottom-width: 0; + } + + [data-side="left"][data-collapsible="offcanvas"] .\[\[data-side\=left\]\[data-collapsible\=offcanvas\]_\&\]\:-right-2 { + right: calc(var(--appearance-spacing) * -2); + } + + [data-side="left"][data-state="collapsed"] .\[\[data-side\=left\]\[data-state\=collapsed\]_\&\]\:cursor-e-resize { + cursor: e-resize; + } + + [data-side="right"][data-collapsible="offcanvas"] .\[\[data-side\=right\]\[data-collapsible\=offcanvas\]_\&\]\:-left-2 { + left: calc(var(--appearance-spacing) * -2); + } + + [data-side="right"][data-state="collapsed"] .\[\[data-side\=right\]\[data-state\=collapsed\]_\&\]\:cursor-w-resize { + cursor: w-resize; + } +} + +@property --tw-animation-delay { + syntax: "*"; + inherits: false; + initial-value: 0s; +} + +@property --tw-animation-direction { + syntax: "*"; + inherits: false; + initial-value: normal; +} + +@property --tw-animation-duration { + syntax: "*"; + inherits: false +} + +@property --tw-animation-fill-mode { + syntax: "*"; + inherits: false; + initial-value: none; +} + +@property --tw-animation-iteration-count { + syntax: "*"; + inherits: false; + initial-value: 1; +} + +@property --tw-enter-blur { + syntax: "*"; + inherits: false; + initial-value: 0; +} + +@property --tw-enter-opacity { + syntax: "*"; + inherits: false; + initial-value: 1; +} + +@property --tw-enter-rotate { + syntax: "*"; + inherits: false; + initial-value: 0; +} + +@property --tw-enter-scale { + syntax: "*"; + inherits: false; + initial-value: 1; +} + +@property --tw-enter-translate-x { + syntax: "*"; + inherits: false; + initial-value: 0; +} + +@property --tw-enter-translate-y { + syntax: "*"; + inherits: false; + initial-value: 0; +} + +@property --tw-exit-blur { + syntax: "*"; + inherits: false; + initial-value: 0; +} + +@property --tw-exit-opacity { + syntax: "*"; + inherits: false; + initial-value: 1; +} + +@property --tw-exit-rotate { + syntax: "*"; + inherits: false; + initial-value: 0; +} + +@property --tw-exit-scale { + syntax: "*"; + inherits: false; + initial-value: 1; +} + +@property --tw-exit-translate-x { + syntax: "*"; + inherits: false; + initial-value: 0; +} + +@property --tw-exit-translate-y { + syntax: "*"; + inherits: false; + initial-value: 0; +} + +:root { + --radius: var(--appearance-radius); + --background: var(--appearance-light-background); + --foreground: var(--appearance-light-foreground); + --card: var(--appearance-light-card); + --card-foreground: var(--appearance-light-card-foreground); + --popover: var(--appearance-light-popover); + --popover-foreground: var(--appearance-light-popover-foreground); + --primary: var(--appearance-light-primary); + --primary-foreground: var(--appearance-light-primary-foreground); + --primary-hover: var(--appearance-light-primary-hover, var(--primary)); +} + +@supports (color: color-mix(in lab, red, red)) { + :root { + --primary-hover: var(--appearance-light-primary-hover, color-mix(in oklab, var(--primary) 90%, transparent)); + } +} + +:root { + --primary-active: var(--appearance-light-primary-active, var(--primary)); +} + +@supports (color: color-mix(in lab, red, red)) { + :root { + --primary-active: var(--appearance-light-primary-active, color-mix(in oklab, var(--primary) 80%, transparent)); + } +} + +:root { + --disabled: var(--appearance-light-disabled, var(--primary)); + --disabled-foreground: var(--appearance-light-disabled-foreground, var(--primary-foreground)); + --disabled-opacity: var(--appearance-light-disabled-opacity, .5); + --secondary: var(--appearance-light-secondary); + --secondary-foreground: var(--appearance-light-secondary-foreground); + --muted: var(--appearance-light-muted); + --muted-foreground: var(--appearance-light-muted-foreground); + --accent: var(--appearance-light-accent); + --accent-foreground: var(--appearance-light-accent-foreground); + --destructive: var(--appearance-light-destructive); + --success: var(--appearance-light-success); + --warning: var(--appearance-light-warning); + --info: var(--appearance-light-info); + --border: var(--appearance-light-border); + --input: var(--appearance-light-input); + --ring: var(--appearance-light-ring); + --sidebar: var(--appearance-light-sidebar); + --sidebar-foreground: var(--appearance-light-sidebar-foreground); + --sidebar-primary: var(--appearance-light-sidebar-primary); + --sidebar-primary-foreground: var(--appearance-light-sidebar-primary-foreground); + --sidebar-accent: var(--appearance-light-sidebar-accent); + --sidebar-accent-foreground: var(--appearance-light-sidebar-accent-foreground); + --sidebar-border: var(--appearance-light-sidebar-border); + --sidebar-ring: var(--appearance-light-sidebar-ring); + --chart-1: var(--appearance-light-chart1); + --chart-2: var(--appearance-light-chart2); + --chart-3: var(--appearance-light-chart3); + --chart-4: var(--appearance-light-chart4); + --chart-5: var(--appearance-light-chart5); +} + +.dark { + --background: var(--appearance-dark-background); + --foreground: var(--appearance-dark-foreground); + --card: var(--appearance-dark-card); + --card-foreground: var(--appearance-dark-card-foreground); + --popover: var(--appearance-dark-popover); + --popover-foreground: var(--appearance-dark-popover-foreground); + --primary: var(--appearance-dark-primary); + --primary-foreground: var(--appearance-dark-primary-foreground); + --primary-hover: var(--appearance-dark-primary-hover, var(--primary)); +} + +@supports (color: color-mix(in lab, red, red)) { + .dark { + --primary-hover: var(--appearance-dark-primary-hover, color-mix(in oklab, var(--primary) 90%, transparent)); + } +} + +.dark { + --primary-active: var(--appearance-dark-primary-active, var(--primary)); +} + +@supports (color: color-mix(in lab, red, red)) { + .dark { + --primary-active: var(--appearance-dark-primary-active, color-mix(in oklab, var(--primary) 80%, transparent)); + } +} + +.dark { + --disabled: var(--appearance-dark-disabled, var(--primary)); + --disabled-foreground: var(--appearance-dark-disabled-foreground, var(--primary-foreground)); + --disabled-opacity: var(--appearance-dark-disabled-opacity, .5); + --secondary: var(--appearance-dark-secondary); + --secondary-foreground: var(--appearance-dark-secondary-foreground); + --muted: var(--appearance-dark-muted); + --muted-foreground: var(--appearance-dark-muted-foreground); + --accent: var(--appearance-dark-accent); + --accent-foreground: var(--appearance-dark-accent-foreground); + --destructive: var(--appearance-dark-destructive); + --success: var(--appearance-dark-success); + --warning: var(--appearance-dark-warning); + --info: var(--appearance-dark-info); + --border: var(--appearance-dark-border); + --input: var(--appearance-dark-input); + --ring: var(--appearance-dark-ring); + --sidebar: var(--appearance-dark-sidebar); + --sidebar-foreground: var(--appearance-dark-sidebar-foreground); + --sidebar-primary: var(--appearance-dark-sidebar-primary); + --sidebar-primary-foreground: var(--appearance-dark-sidebar-primary-foreground); + --sidebar-accent: var(--appearance-dark-sidebar-accent); + --sidebar-accent-foreground: var(--appearance-dark-sidebar-accent-foreground); + --sidebar-border: var(--appearance-dark-sidebar-border); + --sidebar-ring: var(--appearance-dark-sidebar-ring); + --chart-1: var(--appearance-dark-chart1); + --chart-2: var(--appearance-dark-chart2); + --chart-3: var(--appearance-dark-chart3); + --chart-4: var(--appearance-dark-chart4); + --chart-5: var(--appearance-dark-chart5); +} + +body { + font-family: var(--appearance-font-sans); +} + +.border { + border-width: var(--appearance-border-width); +} + +.border-t { + border-top-width: var(--appearance-border-width); +} + +.border-r { + border-right-width: var(--appearance-border-width); +} + +.border-b { + border-bottom-width: var(--appearance-border-width); +} + +.border-l { + border-left-width: var(--appearance-border-width); +} + +.border-x { + border-inline-width: var(--appearance-border-width); +} + +.border-y { + border-block-width: var(--appearance-border-width); +} + +:root { + --appearance-date-pattern: parts; + --appearance-radius: .625rem; + --appearance-font-sans: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; + --appearance-font-heading: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; + --appearance-font-mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; + --appearance-spacing: .25rem; + --appearance-font-size-base: 1rem; + --appearance-sidebar-width: 16rem; + --appearance-sidebar-width-icon: 3rem; + --appearance-border-width: 1px; + --appearance-shadow-strength: 1; + --appearance-light-background: oklch(100% 0 0); + --appearance-light-foreground: oklch(14.5% 0 0); + --appearance-light-card: oklch(100% 0 0); + --appearance-light-card-foreground: oklch(14.5% 0 0); + --appearance-light-popover: oklch(100% 0 0); + --appearance-light-popover-foreground: oklch(14.5% 0 0); + --appearance-light-primary: oklch(20.5% 0 0); + --appearance-light-primary-foreground: oklch(98.5% 0 0); + --appearance-light-secondary: oklch(97% 0 0); + --appearance-light-secondary-foreground: oklch(20.5% 0 0); + --appearance-light-muted: oklch(97% 0 0); + --appearance-light-muted-foreground: oklch(55.6% 0 0); + --appearance-light-accent: oklch(97% 0 0); + --appearance-light-accent-foreground: oklch(20.5% 0 0); + --appearance-light-destructive: oklch(57.7% .245 27.325); + --appearance-light-success: oklch(52.7% .154 150.069); + --appearance-light-warning: oklch(55.5% .163 48.998); + --appearance-light-info: oklch(54.6% .245 262.881); + --appearance-light-border: oklch(92.2% 0 0); + --appearance-light-input: oklch(92.2% 0 0); + --appearance-light-ring: oklch(70.8% 0 0); + --appearance-light-sidebar: oklch(98.5% 0 0); + --appearance-light-sidebar-foreground: oklch(14.5% 0 0); + --appearance-light-sidebar-primary: oklch(20.5% 0 0); + --appearance-light-sidebar-primary-foreground: oklch(98.5% 0 0); + --appearance-light-sidebar-accent: oklch(97% 0 0); + --appearance-light-sidebar-accent-foreground: oklch(20.5% 0 0); + --appearance-light-sidebar-border: oklch(92.2% 0 0); + --appearance-light-sidebar-ring: oklch(70.8% 0 0); + --appearance-light-chart1: oklch(87% 0 0); + --appearance-light-chart2: oklch(55.6% 0 0); + --appearance-light-chart3: oklch(43.9% 0 0); + --appearance-light-chart4: oklch(37.1% 0 0); + --appearance-light-chart5: oklch(26.9% 0 0); + --appearance-dark-background: oklch(14.5% 0 0); + --appearance-dark-foreground: oklch(98.5% 0 0); + --appearance-dark-card: oklch(20.5% 0 0); + --appearance-dark-card-foreground: oklch(98.5% 0 0); + --appearance-dark-popover: oklch(20.5% 0 0); + --appearance-dark-popover-foreground: oklch(98.5% 0 0); + --appearance-dark-primary: oklch(92.2% 0 0); + --appearance-dark-primary-foreground: oklch(20.5% 0 0); + --appearance-dark-secondary: oklch(26.9% 0 0); + --appearance-dark-secondary-foreground: oklch(98.5% 0 0); + --appearance-dark-muted: oklch(26.9% 0 0); + --appearance-dark-muted-foreground: oklch(70.8% 0 0); + --appearance-dark-accent: oklch(26.9% 0 0); + --appearance-dark-accent-foreground: oklch(98.5% 0 0); + --appearance-dark-destructive: oklch(70.4% .191 22.216); + --appearance-dark-success: oklch(79.2% .209 151.711); + --appearance-dark-warning: oklch(82.8% .189 84.429); + --appearance-dark-info: oklch(70.7% .165 254.624); + --appearance-dark-border: oklch(100% 0 0 / .1); + --appearance-dark-input: oklch(100% 0 0 / .15); + --appearance-dark-ring: oklch(55.6% 0 0); + --appearance-dark-sidebar: oklch(20.5% 0 0); + --appearance-dark-sidebar-foreground: oklch(98.5% 0 0); + --appearance-dark-sidebar-primary: oklch(48.8% .243 264.376); + --appearance-dark-sidebar-primary-foreground: oklch(98.5% 0 0); + --appearance-dark-sidebar-accent: oklch(26.9% 0 0); + --appearance-dark-sidebar-accent-foreground: oklch(98.5% 0 0); + --appearance-dark-sidebar-border: oklch(100% 0 0 / .1); + --appearance-dark-sidebar-ring: oklch(55.6% 0 0); + --appearance-dark-chart1: oklch(87% 0 0); + --appearance-dark-chart2: oklch(55.6% 0 0); + --appearance-dark-chart3: oklch(43.9% 0 0); + --appearance-dark-chart4: oklch(37.1% 0 0); + --appearance-dark-chart5: oklch(26.9% 0 0); + --type-body-size: .875rem; + --type-body-leading: 1.25rem; + --type-caption-plain-size: .75rem; + --type-caption-plain-leading: 1rem; + --type-emphasis-weight: 500; + --type-card-size: .875rem; + --type-card-leading: 1.25rem; + --type-card-heading-size: 1rem; + --type-card-heading-weight: 500; + --type-card-title-size: 1rem; + --type-card-title-weight: 500; + --type-card-title-leading: 1.375; + --type-card-title-family: var(--font-heading); + --type-card-title-sm-size: .875rem; + --type-card-title-sm-weight: 500; + --type-card-title-sm-leading: 1.375; + --type-card-title-sm-family: var(--font-heading); + --type-card-description-size: .875rem; + --type-card-description-leading: 1.25rem; + --type-dialog-content-size: .875rem; + --type-dialog-content-leading: 1.25rem; + --type-dialog-title-size: 1rem; + --type-dialog-title-weight: 500; + --type-dialog-title-leading: 1; + --type-dialog-title-family: var(--font-heading); + --type-dialog-description-size: .875rem; + --type-dialog-description-leading: 1.25rem; + --type-sheet-content-size: .875rem; + --type-sheet-content-leading: 1.25rem; + --type-sheet-title-size: 1rem; + --type-sheet-title-weight: 500; + --type-sheet-title-leading: 1.5rem; + --type-sheet-title-family: var(--font-heading); + --type-sheet-description-size: .875rem; + --type-sheet-description-leading: 1.25rem; + --type-alert-dialog-title-size: 1rem; + --type-alert-dialog-title-weight: 500; + --type-alert-dialog-title-leading: 1.5rem; + --type-alert-dialog-title-family: var(--font-heading); + --type-alert-dialog-description-size: .875rem; + --type-alert-dialog-description-leading: 1.25rem; + --type-page-title-size: 1.5rem; + --type-page-title-weight: 700; + --type-page-title-leading: 2rem; + --type-page-title-tracking: -.025em; + --type-page-title-family: var(--font-sans); + --type-section-title-size: 1.125rem; + --type-section-title-weight: 600; + --type-section-title-leading: 1.75rem; + --type-section-title-tracking: -.025em; + --type-section-title-family: var(--font-sans); + --type-section-description-size: .875rem; + --type-section-description-leading: 1.25rem; + --type-label-size: .875rem; + --type-label-weight: 500; + --type-label-leading: 1; + --type-input-size: .875rem; + --type-input-leading: 1.25rem; + --type-input-touch-size: 1rem; + --type-input-touch-leading: 1.5rem; + --type-textarea-size: .875rem; + --type-textarea-leading: 1.25rem; + --type-textarea-touch-size: 1rem; + --type-textarea-touch-leading: 1.5rem; + --type-select-trigger-size: .875rem; + --type-select-trigger-leading: 1.25rem; + --type-select-label-size: .75rem; + --type-select-label-leading: 1rem; + --type-select-item-size: .875rem; + --type-select-item-leading: 1.25rem; + --type-dropdown-menu-item-size: .875rem; + --type-dropdown-menu-item-leading: 1.25rem; + --type-dropdown-menu-checkbox-item-size: .875rem; + --type-dropdown-menu-checkbox-item-leading: 1.25rem; + --type-dropdown-menu-radio-item-size: .875rem; + --type-dropdown-menu-radio-item-leading: 1.25rem; + --type-dropdown-menu-sub-trigger-size: .875rem; + --type-dropdown-menu-sub-trigger-leading: 1.25rem; + --type-dropdown-menu-label-size: .75rem; + --type-dropdown-menu-label-weight: 500; + --type-dropdown-menu-label-leading: 1rem; + --type-dropdown-menu-shortcut-size: .75rem; + --type-dropdown-menu-shortcut-leading: 1rem; + --type-dropdown-menu-shortcut-tracking: .1em; + --type-command-input-size: .875rem; + --type-command-input-leading: 1.25rem; + --type-command-group-size: .75rem; + --type-command-group-weight: 500; + --type-command-group-leading: 1rem; + --type-command-item-size: .875rem; + --type-command-item-leading: 1.25rem; + --type-command-empty-size: .875rem; + --type-command-empty-leading: 1.25rem; + --type-command-shortcut-size: .75rem; + --type-command-shortcut-leading: 1rem; + --type-command-shortcut-tracking: .1em; + --type-badge-size: .75rem; + --type-badge-weight: 500; + --type-badge-leading: 1rem; + --type-badge-lg-size: .875rem; + --type-badge-lg-weight: 500; + --type-badge-lg-leading: 1.25rem; + --type-chip-size: .75rem; + --type-chip-weight: 500; + --type-chip-leading: 1rem; + --type-chip-meta-size: .75rem; + --type-chip-meta-leading: 1rem; + --type-banner-size: .875rem; + --type-banner-leading: 1.25rem; + --type-banner-title-weight: 500; + --type-list-item-size: .875rem; + --type-list-item-leading: 1.25rem; + --type-list-item-description-size: .875rem; + --type-list-item-description-leading: 1.25rem; + --type-description-term-size: .75rem; + --type-description-term-leading: 1rem; + --type-description-details-size: .875rem; + --type-description-details-leading: 1.25rem; + --type-table-size: .875rem; + --type-table-leading: 1.25rem; + --type-segmented-control-segment-size: .875rem; + --type-segmented-control-segment-weight: 500; + --type-segmented-control-segment-leading: 1.25rem; + --type-pagination-ellipsis-size: .875rem; + --type-pagination-ellipsis-leading: 1.25rem; + --type-field-description-size: .875rem; + --type-field-description-leading: 1.25rem; + --type-field-error-size: .875rem; + --type-field-error-leading: 1.25rem; + --type-table-toolbar-size: .875rem; + --type-table-toolbar-leading: 1.25rem; + --type-table-empty-state-size: .875rem; + --type-table-empty-state-leading: 1.25rem; + --type-card-eyebrow-size: .75rem; + --type-card-eyebrow-weight: 500; + --type-card-eyebrow-leading: 1rem; + --type-card-metadata-size: .875rem; + --type-card-metadata-leading: 1.25rem; + --type-table-head-weight: 500; + --type-table-footer-weight: 500; + --type-table-caption-size: .875rem; + --type-table-caption-leading: 1.25rem; + --type-tabs-trigger-size: .875rem; + --type-tabs-trigger-weight: 500; + --type-tabs-trigger-leading: 1.25rem; + --type-tabs-content-size: .875rem; + --type-tabs-content-leading: 1.25rem; + --type-tooltip-content-size: .75rem; + --type-tooltip-content-leading: 1rem; + --type-avatar-fallback-size: .875rem; + --type-avatar-fallback-leading: 1.25rem; + --type-avatar-fallback-sm-size: .75rem; + --type-avatar-fallback-sm-leading: 1rem; + --type-avatar-group-count-size: .875rem; + --type-avatar-group-count-leading: 1.25rem; + --type-sidebar-group-content-size: .875rem; + --type-sidebar-group-content-leading: 1.25rem; + --type-sidebar-group-label-size: .75rem; + --type-sidebar-group-label-weight: 500; + --type-sidebar-group-label-leading: 1rem; + --type-sidebar-menu-button-size: .875rem; + --type-sidebar-menu-button-leading: 1.25rem; + --type-sidebar-menu-button-sm-size: .75rem; + --type-sidebar-menu-button-sm-leading: 1rem; + --type-sidebar-menu-button-lg-size: .875rem; + --type-sidebar-menu-button-lg-leading: 1.25rem; + --type-sidebar-menu-button-active-weight: 500; + --type-sidebar-menu-sub-button-size: .875rem; + --type-sidebar-menu-sub-button-leading: 1.25rem; + --type-sidebar-menu-sub-button-sm-size: .75rem; + --type-sidebar-menu-sub-button-sm-leading: 1rem; + --type-sidebar-menu-badge-size: .75rem; + --type-sidebar-menu-badge-weight: 500; + --type-sidebar-menu-badge-leading: 1rem; + --type-error-state-size: .875rem; + --type-error-state-leading: 1.25rem; + --type-error-state-title-weight: 500; + --type-input-group-addon-size: .875rem; + --type-input-group-addon-weight: 500; + --type-input-group-addon-leading: 1.25rem; + --type-input-group-text-size: .875rem; + --type-input-group-text-leading: 1.25rem; + --type-input-group-control-size: .875rem; + --type-input-group-control-leading: 1.25rem; + --type-empty-state-title-size: .875rem; + --type-empty-state-title-weight: 500; + --type-empty-state-title-leading: 1.25rem; + --type-empty-state-title-illustrated-size: 1.125rem; + --type-empty-state-title-illustrated-weight: 600; + --type-empty-state-title-illustrated-leading: 1.75rem; + --type-empty-state-title-illustrated-tracking: -.025em; + --type-empty-state-title-illustrated-family: var(--font-sans); + --type-empty-state-description-size: .875rem; + --type-empty-state-description-leading: 1.25rem; + --type-empty-state-reference-size: .875rem; + --type-empty-state-reference-leading: 1.25rem; + --type-metric-value-size: 1.5rem; + --type-metric-value-weight: 600; + --type-metric-value-leading: 2rem; + --type-metric-value-tracking: -.025em; + --type-metric-value-lg-size: 1.875rem; + --type-metric-value-lg-weight: 600; + --type-metric-value-lg-leading: 2.25rem; + --type-metric-value-lg-tracking: -.025em; + --type-metric-unit-size: .875rem; + --type-metric-unit-weight: 400; + --type-metric-unit-leading: 1.25rem; + --type-metric-total-size: 2.25rem; + --type-metric-total-weight: 700; + --type-metric-total-leading: 2.5rem; + --type-metric-total-tracking: -.025em; + --type-metric-delta-size: .75rem; + --type-metric-delta-weight: 500; + --type-metric-delta-leading: 1rem; + --type-metric-delta-label-weight: 400; + --type-metric-label-size: .875rem; + --type-metric-label-leading: 1.25rem; + --type-metric-heading-size: .875rem; + --type-metric-heading-weight: 500; + --type-metric-heading-leading: 1.25rem; + --type-chart-label-size: .625rem; + --type-chart-label-leading: 1; + --type-chat-author-size: .75rem; + --type-chat-author-weight: 500; + --type-chat-author-leading: 1rem; + --type-chat-message-size: .875rem; + --type-chat-message-leading: 1.25rem; + --control-xs-height: calc(var(--spacing) * 6); + --control-xs-padding-x: calc(var(--spacing) * 2); + --control-xs-icon: calc(var(--spacing) * 3); + --control-xs-text-size: .75rem; + --control-xs-text-weight: 500; + --control-xs-text-leading: 1rem; + --control-sm-height: calc(var(--spacing) * 7); + --control-sm-padding-x: calc(var(--spacing) * 2.5); + --control-sm-icon: calc(var(--spacing) * 3.5); + --control-sm-text-size: .75rem; + --control-sm-text-weight: 500; + --control-sm-text-leading: 1rem; + --control-default-height: calc(var(--spacing) * 8); + --control-default-padding-x: calc(var(--spacing) * 2.5); + --control-default-icon: calc(var(--spacing) * 4); + --control-default-text-size: .875rem; + --control-default-text-weight: 500; + --control-default-text-leading: 1.25rem; + --control-lg-height: calc(var(--spacing) * 9); + --control-lg-padding-x: calc(var(--spacing) * 2.5); + --control-lg-icon: calc(var(--spacing) * 4); + --control-lg-text-size: .875rem; + --control-lg-text-weight: 500; + --control-lg-text-leading: 1.25rem; +} + +@property --tw-translate-x { + syntax: "*"; + inherits: false; + initial-value: 0; +} + +@property --tw-translate-y { + syntax: "*"; + inherits: false; + initial-value: 0; +} + +@property --tw-translate-z { + syntax: "*"; + inherits: false; + initial-value: 0; +} + +@property --tw-rotate-x { + syntax: "*"; + inherits: false +} + +@property --tw-rotate-y { + syntax: "*"; + inherits: false +} + +@property --tw-rotate-z { + syntax: "*"; + inherits: false +} + +@property --tw-skew-x { + syntax: "*"; + inherits: false +} + +@property --tw-skew-y { + syntax: "*"; + inherits: false +} + +@property --tw-space-y-reverse { + syntax: "*"; + inherits: false; + initial-value: 0; +} + +@property --tw-space-x-reverse { + syntax: "*"; + inherits: false; + initial-value: 0; +} + +@property --tw-divide-y-reverse { + syntax: "*"; + inherits: false; + initial-value: 0; +} + +@property --tw-border-style { + syntax: "*"; + inherits: false; + initial-value: solid; +} + +@property --tw-gradient-position { + syntax: "*"; + inherits: false +} + +@property --tw-gradient-from { + syntax: ""; + inherits: false; + initial-value: #0000; +} + +@property --tw-gradient-via { + syntax: ""; + inherits: false; + initial-value: #0000; +} + +@property --tw-gradient-to { + syntax: ""; + inherits: false; + initial-value: #0000; +} + +@property --tw-gradient-stops { + syntax: "*"; + inherits: false +} + +@property --tw-gradient-via-stops { + syntax: "*"; + inherits: false +} + +@property --tw-gradient-from-position { + syntax: ""; + inherits: false; + initial-value: 0%; +} + +@property --tw-gradient-via-position { + syntax: ""; + inherits: false; + initial-value: 50%; +} + +@property --tw-gradient-to-position { + syntax: ""; + inherits: false; + initial-value: 100%; +} + +@property --tw-ordinal { + syntax: "*"; + inherits: false +} + +@property --tw-slashed-zero { + syntax: "*"; + inherits: false +} + +@property --tw-numeric-figure { + syntax: "*"; + inherits: false +} + +@property --tw-numeric-spacing { + syntax: "*"; + inherits: false +} + +@property --tw-numeric-fraction { + syntax: "*"; + inherits: false +} + +@property --tw-shadow { + syntax: "*"; + inherits: false; + initial-value: 0 0 #0000; +} + +@property --tw-shadow-color { + syntax: "*"; + inherits: false +} + +@property --tw-shadow-alpha { + syntax: ""; + inherits: false; + initial-value: 100%; +} + +@property --tw-inset-shadow { + syntax: "*"; + inherits: false; + initial-value: 0 0 #0000; +} + +@property --tw-inset-shadow-color { + syntax: "*"; + inherits: false +} + +@property --tw-inset-shadow-alpha { + syntax: ""; + inherits: false; + initial-value: 100%; +} + +@property --tw-ring-color { + syntax: "*"; + inherits: false +} + +@property --tw-ring-shadow { + syntax: "*"; + inherits: false; + initial-value: 0 0 #0000; +} + +@property --tw-inset-ring-color { + syntax: "*"; + inherits: false +} + +@property --tw-inset-ring-shadow { + syntax: "*"; + inherits: false; + initial-value: 0 0 #0000; +} + +@property --tw-ring-inset { + syntax: "*"; + inherits: false +} + +@property --tw-ring-offset-width { + syntax: ""; + inherits: false; + initial-value: 0; +} + +@property --tw-ring-offset-color { + syntax: "*"; + inherits: false; + initial-value: #fff; +} + +@property --tw-ring-offset-shadow { + syntax: "*"; + inherits: false; + initial-value: 0 0 #0000; +} + +@property --tw-outline-style { + syntax: "*"; + inherits: false; + initial-value: solid; +} + +@property --tw-blur { + syntax: "*"; + inherits: false +} + +@property --tw-brightness { + syntax: "*"; + inherits: false +} + +@property --tw-contrast { + syntax: "*"; + inherits: false +} + +@property --tw-grayscale { + syntax: "*"; + inherits: false +} + +@property --tw-hue-rotate { + syntax: "*"; + inherits: false +} + +@property --tw-invert { + syntax: "*"; + inherits: false +} + +@property --tw-opacity { + syntax: "*"; + inherits: false +} + +@property --tw-saturate { + syntax: "*"; + inherits: false +} + +@property --tw-sepia { + syntax: "*"; + inherits: false +} + +@property --tw-drop-shadow { + syntax: "*"; + inherits: false +} + +@property --tw-drop-shadow-color { + syntax: "*"; + inherits: false +} + +@property --tw-drop-shadow-alpha { + syntax: ""; + inherits: false; + initial-value: 100%; +} + +@property --tw-drop-shadow-size { + syntax: "*"; + inherits: false +} + +@property --tw-duration { + syntax: "*"; + inherits: false +} + +@property --tw-ease { + syntax: "*"; + inherits: false +} + +@property --tw-content { + syntax: "*"; + inherits: false; + initial-value: ""; +} + +@property --tw-backdrop-blur { + syntax: "*"; + inherits: false +} + +@property --tw-backdrop-brightness { + syntax: "*"; + inherits: false +} + +@property --tw-backdrop-contrast { + syntax: "*"; + inherits: false +} + +@property --tw-backdrop-grayscale { + syntax: "*"; + inherits: false +} + +@property --tw-backdrop-hue-rotate { + syntax: "*"; + inherits: false +} + +@property --tw-backdrop-invert { + syntax: "*"; + inherits: false +} + +@property --tw-backdrop-opacity { + syntax: "*"; + inherits: false +} + +@property --tw-backdrop-saturate { + syntax: "*"; + inherits: false +} + +@property --tw-backdrop-sepia { + syntax: "*"; + inherits: false +} + +@keyframes spin { + to { + transform: rotate(360deg); + } +} + +@keyframes pulse { + 50% { + opacity: .5; + } +} + +@keyframes enter { + from { + opacity: var(--tw-enter-opacity, 1); + transform: translate3d(var(--tw-enter-translate-x, 0),var(--tw-enter-translate-y, 0),0)scale3d(var(--tw-enter-scale, 1),var(--tw-enter-scale, 1),var(--tw-enter-scale, 1))rotate(var(--tw-enter-rotate, 0)); + filter: blur(var(--tw-enter-blur, 0)); + } +} + +@keyframes exit { + to { + opacity: var(--tw-exit-opacity, 1); + transform: translate3d(var(--tw-exit-translate-x, 0),var(--tw-exit-translate-y, 0),0)scale3d(var(--tw-exit-scale, 1),var(--tw-exit-scale, 1),var(--tw-exit-scale, 1))rotate(var(--tw-exit-rotate, 0)); + filter: blur(var(--tw-exit-blur, 0)); + } +} diff --git a/module/services/frontend/code/packages/codefly-ui/src/skin/theme.css b/module/services/frontend/code/packages/codefly-ui/src/skin/theme.css new file mode 100644 index 000000000..79a855fa5 --- /dev/null +++ b/module/services/frontend/code/packages/codefly-ui/src/skin/theme.css @@ -0,0 +1,355 @@ +/* + * The token layer of @codefly-dev/ui: where a contract token's CSS variable + * becomes a Tailwind utility (`--success` -> `bg-success`), the per-mode + * binding of those variables to the `--appearance-*` properties a resolved + * skin projects onto , and the custom variants the kit's components + * name (`data-open:`, `dark:`). + * + * It lives in the kit, not the host, because it is the other half of what the + * kit's class lists mean: a component that writes `text-success` is only + * styled where this file was compiled. The host imports it into its one + * compiled stylesheet; `preview.css` is it compiled over the kit alone with + * the default skin, for a solution previewing itself without a host. + * + * Tailwind v4 source, not compiled CSS: import it after `tailwindcss`. + */ + +/* + * Local Tailwind helpers used by the generated shadcn/Base UI components. + * + * MIT License + * Copyright (c) 2023 shadcn + * + * 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. + */ +@theme inline { + @keyframes accordion-down { + from { + height: 0; + } + to { + height: var(--accordion-panel-height, auto); + } + } + + @keyframes accordion-up { + from { + height: var(--accordion-panel-height, auto); + } + to { + height: 0; + } + } +} + +@custom-variant data-open { + &:where([data-state="open"]), + &:where([data-open]:not([data-open="false"])) { + @slot; + } +} + +@custom-variant data-closed { + &:where([data-state="closed"]), + &:where([data-closed]:not([data-closed="false"])) { + @slot; + } +} + +@custom-variant data-checked { + &:where([data-state="checked"]), + &:where([data-checked]:not([data-checked="false"])) { + @slot; + } +} + +@custom-variant data-unchecked { + &:where([data-state="unchecked"]), + &:where([data-unchecked]:not([data-unchecked="false"])) { + @slot; + } +} + +@custom-variant data-selected { + &:where([data-selected="true"]) { + @slot; + } +} + +@custom-variant data-disabled { + &:where([data-disabled="true"]), + &:where([data-disabled]:not([data-disabled="false"])) { + @slot; + } +} + +@custom-variant data-active { + &:where([data-state="active"]), + &:where([data-active]:not([data-active="false"])) { + @slot; + } +} + +@custom-variant data-horizontal { + &:where([data-orientation="horizontal"]) { + @slot; + } +} + +@custom-variant data-vertical { + &:where([data-orientation="vertical"]) { + @slot; + } +} + +@utility no-scrollbar { + -ms-overflow-style: none; + scrollbar-width: none; + + &::-webkit-scrollbar { + display: none; + } +} + +@custom-variant dark (&:is(.dark *)); + +@theme inline { + --color-background: var(--background); + --color-foreground: var(--foreground); + --color-card: var(--card); + --color-card-foreground: var(--card-foreground); + --color-primary: var(--primary); + --color-primary-foreground: var(--primary-foreground); + --color-secondary: var(--secondary); + --color-secondary-foreground: var(--secondary-foreground); + --color-muted: var(--muted); + --color-muted-foreground: var(--muted-foreground); + --color-accent: var(--accent); + --color-accent-foreground: var(--accent-foreground); + --color-destructive: var(--destructive); + --color-success: var(--success); + --color-warning: var(--warning); + --color-info: var(--info); + --color-border: var(--border); + --color-input: var(--input); + --color-ring: var(--ring); + --color-sidebar: var(--sidebar); + --color-sidebar-foreground: var(--sidebar-foreground); + --color-sidebar-accent: var(--sidebar-accent); + --radius-sm: calc(var(--radius) * 0.6); + --radius-md: calc(var(--radius) * 0.8); + --radius-lg: var(--radius); + --radius-xl: calc(var(--radius) * 1.4); + --font-heading: var(--appearance-font-heading); + --font-sans: var(--appearance-font-sans); + --font-mono: var(--appearance-font-mono); + /* Interaction states of the primary action. A skin that decides them wins; + otherwise they derive from --primary here, in one place. */ + --color-primary-hover: var(--primary-hover); + --color-primary-active: var(--primary-active); + --color-disabled: var(--disabled); + --color-disabled-foreground: var(--disabled-foreground); + /* Base spacing unit: rescales every spacing/size utility (density). */ + --spacing: var(--appearance-spacing); + /* Elevation scale, tunable via the shadowStrength multiplier. */ + --shadow-sm: 0 1px 3px 0 + rgb(0 0 0 / calc(0.1 * var(--appearance-shadow-strength))), + 0 1px 2px -1px rgb(0 0 0 / calc(0.1 * var(--appearance-shadow-strength))); + --shadow-md: 0 4px 6px -1px + rgb(0 0 0 / calc(0.1 * var(--appearance-shadow-strength))), + 0 2px 4px -2px rgb(0 0 0 / calc(0.1 * var(--appearance-shadow-strength))); + --shadow-lg: 0 10px 15px -3px + rgb(0 0 0 / calc(0.1 * var(--appearance-shadow-strength))), + 0 4px 6px -4px rgb(0 0 0 / calc(0.1 * var(--appearance-shadow-strength))); + --shadow-xl: 0 20px 25px -5px + rgb(0 0 0 / calc(0.1 * var(--appearance-shadow-strength))), + 0 8px 10px -6px rgb(0 0 0 / calc(0.1 * var(--appearance-shadow-strength))); + --shadow-2xl: 0 25px 50px -12px + rgb(0 0 0 / calc(0.25 * var(--appearance-shadow-strength))); + --color-sidebar-ring: var(--sidebar-ring); + --color-sidebar-border: var(--sidebar-border); + --color-sidebar-accent-foreground: var(--sidebar-accent-foreground); + --color-sidebar-primary-foreground: var(--sidebar-primary-foreground); + --color-sidebar-primary: var(--sidebar-primary); + --color-chart-5: var(--chart-5); + --color-chart-4: var(--chart-4); + --color-chart-3: var(--chart-3); + --color-chart-2: var(--chart-2); + --color-chart-1: var(--chart-1); + --color-popover-foreground: var(--popover-foreground); + --color-popover: var(--popover); + --radius-2xl: calc(var(--radius) * 1.8); + --radius-3xl: calc(var(--radius) * 2.2); + --radius-4xl: calc(var(--radius) * 2.6); +} + +:root { + --radius: var(--appearance-radius); + --background: var(--appearance-light-background); + --foreground: var(--appearance-light-foreground); + --card: var(--appearance-light-card); + --card-foreground: var(--appearance-light-card-foreground); + --popover: var(--appearance-light-popover); + --popover-foreground: var(--appearance-light-popover-foreground); + --primary: var(--appearance-light-primary); + --primary-foreground: var(--appearance-light-primary-foreground); + --primary-hover: var( + --appearance-light-primary-hover, + color-mix(in oklab, var(--primary) 90%, transparent) + ); + --primary-active: var( + --appearance-light-primary-active, + color-mix(in oklab, var(--primary) 80%, transparent) + ); + --disabled: var(--appearance-light-disabled, var(--primary)); + --disabled-foreground: var( + --appearance-light-disabled-foreground, + var(--primary-foreground) + ); + --disabled-opacity: var(--appearance-light-disabled-opacity, 0.5); + --secondary: var(--appearance-light-secondary); + --secondary-foreground: var(--appearance-light-secondary-foreground); + --muted: var(--appearance-light-muted); + --muted-foreground: var(--appearance-light-muted-foreground); + --accent: var(--appearance-light-accent); + --accent-foreground: var(--appearance-light-accent-foreground); + --destructive: var(--appearance-light-destructive); + --success: var(--appearance-light-success); + --warning: var(--appearance-light-warning); + --info: var(--appearance-light-info); + --border: var(--appearance-light-border); + --input: var(--appearance-light-input); + --ring: var(--appearance-light-ring); + --sidebar: var(--appearance-light-sidebar); + --sidebar-foreground: var(--appearance-light-sidebar-foreground); + --sidebar-primary: var(--appearance-light-sidebar-primary); + --sidebar-primary-foreground: var( + --appearance-light-sidebar-primary-foreground + ); + --sidebar-accent: var(--appearance-light-sidebar-accent); + --sidebar-accent-foreground: var( + --appearance-light-sidebar-accent-foreground + ); + --sidebar-border: var(--appearance-light-sidebar-border); + --sidebar-ring: var(--appearance-light-sidebar-ring); + --chart-1: var(--appearance-light-chart1); + --chart-2: var(--appearance-light-chart2); + --chart-3: var(--appearance-light-chart3); + --chart-4: var(--appearance-light-chart4); + --chart-5: var(--appearance-light-chart5); +} + +.dark { + --background: var(--appearance-dark-background); + --foreground: var(--appearance-dark-foreground); + --card: var(--appearance-dark-card); + --card-foreground: var(--appearance-dark-card-foreground); + --popover: var(--appearance-dark-popover); + --popover-foreground: var(--appearance-dark-popover-foreground); + --primary: var(--appearance-dark-primary); + --primary-foreground: var(--appearance-dark-primary-foreground); + --primary-hover: var( + --appearance-dark-primary-hover, + color-mix(in oklab, var(--primary) 90%, transparent) + ); + --primary-active: var( + --appearance-dark-primary-active, + color-mix(in oklab, var(--primary) 80%, transparent) + ); + --disabled: var(--appearance-dark-disabled, var(--primary)); + --disabled-foreground: var( + --appearance-dark-disabled-foreground, + var(--primary-foreground) + ); + --disabled-opacity: var(--appearance-dark-disabled-opacity, 0.5); + --secondary: var(--appearance-dark-secondary); + --secondary-foreground: var(--appearance-dark-secondary-foreground); + --muted: var(--appearance-dark-muted); + --muted-foreground: var(--appearance-dark-muted-foreground); + --accent: var(--appearance-dark-accent); + --accent-foreground: var(--appearance-dark-accent-foreground); + --destructive: var(--appearance-dark-destructive); + --success: var(--appearance-dark-success); + --warning: var(--appearance-dark-warning); + --info: var(--appearance-dark-info); + --border: var(--appearance-dark-border); + --input: var(--appearance-dark-input); + --ring: var(--appearance-dark-ring); + --sidebar: var(--appearance-dark-sidebar); + --sidebar-foreground: var(--appearance-dark-sidebar-foreground); + --sidebar-primary: var(--appearance-dark-sidebar-primary); + --sidebar-primary-foreground: var( + --appearance-dark-sidebar-primary-foreground + ); + --sidebar-accent: var(--appearance-dark-sidebar-accent); + --sidebar-accent-foreground: var(--appearance-dark-sidebar-accent-foreground); + --sidebar-border: var(--appearance-dark-sidebar-border); + --sidebar-ring: var(--appearance-dark-sidebar-ring); + --chart-1: var(--appearance-dark-chart1); + --chart-2: var(--appearance-dark-chart2); + --chart-3: var(--appearance-dark-chart3); + --chart-4: var(--appearance-dark-chart4); + --chart-5: var(--appearance-dark-chart5); +} + +body { + font-family: var(--appearance-font-sans); +} + +@layer base { + * { + @apply border-border outline-ring/50; + } + body { + @apply bg-background text-foreground; + } + html { + @apply font-sans; + font-size: var(--appearance-font-size-base); + } +} + +/* + * Border width is an application-configurable token. These rules are + * intentionally unlayered so they win over Tailwind's `utilities` layer, + * letting the appearance token redefine the 1px `border` utility globally + * (numeric widths like `border-2` remain intentional emphasis). The token + * defaults to 1px, so omitting it leaves borders unchanged. + */ +.border { + border-width: var(--appearance-border-width); +} +.border-t { + border-top-width: var(--appearance-border-width); +} +.border-r { + border-right-width: var(--appearance-border-width); +} +.border-b { + border-bottom-width: var(--appearance-border-width); +} +.border-l { + border-left-width: var(--appearance-border-width); +} +.border-x { + border-inline-width: var(--appearance-border-width); +} +.border-y { + border-block-width: var(--appearance-border-width); +} diff --git a/module/services/frontend/code/packages/codefly-ui/src/skin/type-slots.generated.css b/module/services/frontend/code/packages/codefly-ui/src/skin/type-slots.generated.css index 1c8268077..5aaa144fa 100644 --- a/module/services/frontend/code/packages/codefly-ui/src/skin/type-slots.generated.css +++ b/module/services/frontend/code/packages/codefly-ui/src/skin/type-slots.generated.css @@ -319,6 +319,29 @@ } } +@utility type-badge-lg { + :where(&) { + font-size: var(--type-badge-lg-size); + font-weight: var(--type-badge-lg-weight); + line-height: var(--type-badge-lg-leading); + } +} + +@utility type-chip { + :where(&) { + font-size: var(--type-chip-size); + font-weight: var(--type-chip-weight); + line-height: var(--type-chip-leading); + } +} + +@utility type-chip-meta { + :where(&) { + font-size: var(--type-chip-meta-size); + line-height: var(--type-chip-meta-leading); + } +} + @utility type-banner { :where(&) { font-size: var(--type-banner-size); @@ -332,6 +355,34 @@ } } +@utility type-list-item { + :where(&) { + font-size: var(--type-list-item-size); + line-height: var(--type-list-item-leading); + } +} + +@utility type-list-item-description { + :where(&) { + font-size: var(--type-list-item-description-size); + line-height: var(--type-list-item-description-leading); + } +} + +@utility type-description-term { + :where(&) { + font-size: var(--type-description-term-size); + line-height: var(--type-description-term-leading); + } +} + +@utility type-description-details { + :where(&) { + font-size: var(--type-description-details-size); + line-height: var(--type-description-details-leading); + } +} + @utility type-table { :where(&) { font-size: var(--type-table-size); @@ -583,6 +634,13 @@ } } +@utility type-empty-state-reference { + :where(&) { + font-size: var(--type-empty-state-reference-size); + line-height: var(--type-empty-state-reference-leading); + } +} + @utility type-metric-value { :where(&) { font-size: var(--type-metric-value-size); diff --git a/module/services/frontend/code/packages/codefly-ui/stories/primitives.stories.tsx b/module/services/frontend/code/packages/codefly-ui/stories/primitives.stories.tsx index f1e4913a4..962cc921d 100644 --- a/module/services/frontend/code/packages/codefly-ui/stories/primitives.stories.tsx +++ b/module/services/frontend/code/packages/codefly-ui/stories/primitives.stories.tsx @@ -51,7 +51,14 @@ import { EmptyState, ErrorState, Notice, + Card, + Chip, + ChipGroup, + List, + ListItem, + DescriptionList, } from "../src/layout/index.js"; +import { FileTextIcon, TriangleAlertIcon, UserIcon } from "lucide-react"; export default { title: "Shared UI/Primitives" }; export const Buttons = { @@ -276,3 +283,169 @@ export const DecisionNotice = { ); }, }; + +// The three states a declared source can be in, told apart by tone and by +// the dot's shape as well as by their words. +export const StatusBadges = { + render: () => ( +
    + + Set up + + + Connected + + + Error + + Paused + Syncing + + Live + + + In review + +
    + ), +}; + +export const Chips = { + render: function ChipsExample() { + const [owners, setOwners] = useState(["Jane Doe", "Acme Finance"]); + return ( +
    + + } meta="unread"> + Pricing change + + }> + Onboarding flow + + + + {owners.map((owner) => ( + } + onRemove={() => + setOwners((current) => current.filter((o) => o !== owner)) + } + removeLabel={`Remove ${owner}`} + > + {owner} + + ))} + + + {}}> + Accepted + + Needs review + Rejected + + Comments + + +
    + ); + }, +}; + +export const StatusBanners = { + render: () => ( +
    + + Nothing on this page reached a host. + + + + Scheduled pulls resume once the credential is renewed. + + Retry} + /> +
    + ), +}; + +export const Lists = { + render: () => ( +
    + + } + description="Line 12: front matter has no owner." + meta="proposal.md" + > + Missing owner + + } + description="Two headings share level two." + actions={} + > + Heading level reused + + + + Pricing change + Onboarding flow + +
    + ), +}; + +export const DescriptionLists = { + render: () => ( +
    + In review }, + { term: "Owner", value: "Jane Doe" }, + { term: "Updated", value: "2 October 2026" }, + ]} + /> + +
    + ), +}; + +export const CardWithDescription = { + render: () => ( + Sync now} + > + + Connected + + + ), +}; + +export const EmptyWithReference = { + render: () => ( + + The source card above says where this stands. + + } + /> + ), +}; diff --git a/module/services/frontend/code/packages/saas-plugin-contract/package.json b/module/services/frontend/code/packages/saas-plugin-contract/package.json index ceb497b61..4ab4cfbbc 100644 --- a/module/services/frontend/code/packages/saas-plugin-contract/package.json +++ b/module/services/frontend/code/packages/saas-plugin-contract/package.json @@ -1,6 +1,6 @@ { "name": "@codefly/saas-plugin-contract", - "version": "2.3.0", + "version": "2.4.0", "description": "React-free serializable frontend plugin metadata and composition for the Codefly SaaS host.", "type": "module", "sideEffects": false, diff --git a/module/services/frontend/code/packages/saas-plugin-contract/src/appearance.ts b/module/services/frontend/code/packages/saas-plugin-contract/src/appearance.ts index 13f7df604..93c9b3a80 100644 --- a/module/services/frontend/code/packages/saas-plugin-contract/src/appearance.ts +++ b/module/services/frontend/code/packages/saas-plugin-contract/src/appearance.ts @@ -29,6 +29,11 @@ const light: FrontendThemeTokens = { accent: "oklch(0.97 0 0)", accentForeground: "oklch(0.205 0 0)", destructive: "oklch(0.577 0.245 27.325)", + // Dark enough to read as text on white and on a 10% tint of itself, the + // contrast `destructive` already meets. + success: "oklch(0.527 0.154 150.069)", + warning: "oklch(0.555 0.163 48.998)", + info: "oklch(0.546 0.245 262.881)", border: "oklch(0.922 0 0)", input: "oklch(0.922 0 0)", ring: "oklch(0.708 0 0)", @@ -63,6 +68,9 @@ const dark: FrontendThemeTokens = { accent: "oklch(0.269 0 0)", accentForeground: "oklch(0.985 0 0)", destructive: "oklch(0.704 0.191 22.216)", + success: "oklch(0.792 0.209 151.711)", + warning: "oklch(0.828 0.189 84.429)", + info: "oklch(0.707 0.165 254.624)", border: "oklch(1 0 0 / 10%)", input: "oklch(1 0 0 / 15%)", ring: "oklch(0.556 0 0)", diff --git a/module/services/frontend/code/packages/saas-plugin-contract/src/contracts.ts b/module/services/frontend/code/packages/saas-plugin-contract/src/contracts.ts index 50eaa7e85..fd0c6f38b 100644 --- a/module/services/frontend/code/packages/saas-plugin-contract/src/contracts.ts +++ b/module/services/frontend/code/packages/saas-plugin-contract/src/contracts.ts @@ -167,6 +167,14 @@ export const FRONTEND_APPEARANCE_TOKEN_NAMES = [ "accent", "accentForeground", "destructive", + // Status tones beside `destructive`, which doubles as the danger tone. Each + // is ONE colour read as text and as a tint of itself (`bg-success/10 + // text-success`), the shape `destructive` already has, so a status needs no + // foreground pair. Without them a healthy state could only borrow `default` + // or `secondary`, and neither reads as healthy. + "success", + "warning", + "info", "border", "input", "ring", diff --git a/module/services/frontend/code/packages/saas-plugin-contract/src/typography.ts b/module/services/frontend/code/packages/saas-plugin-contract/src/typography.ts index d36ab9a6c..31f35455a 100644 --- a/module/services/frontend/code/packages/saas-plugin-contract/src/typography.ts +++ b/module/services/frontend/code/packages/saas-plugin-contract/src/typography.ts @@ -147,8 +147,15 @@ export const FRONTEND_TYPE_SLOT_NAMES = [ "command-empty", "command-shortcut", "badge", + "badge-lg", + "chip", + "chip-meta", "banner", "banner-title", + "list-item", + "list-item-description", + "description-term", + "description-details", "table", "segmented-control-segment", "pagination-ellipsis", @@ -184,6 +191,7 @@ export const FRONTEND_TYPE_SLOT_NAMES = [ "empty-state-title", "empty-state-title-illustrated", "empty-state-description", + "empty-state-reference", "metric-value", "metric-value-lg", "metric-unit", @@ -444,8 +452,17 @@ export const DEFAULT_TYPE_SLOTS: FrontendTypeSlots = Object.freeze({ "command-empty": "body", "command-shortcut": "shortcut", badge: "group-label", + // A large badge sits beside body copy rather than inside a dense row, so it + // takes the control label's step instead of shrinking it. + "badge-lg": "control", + chip: "control-sm", + "chip-meta": "group-label-plain", banner: "body", "banner-title": "emphasis", + "list-item": "body", + "list-item-description": "surface-description", + "description-term": "group-label-plain", + "description-details": "body", table: "body", "segmented-control-segment": "control", "pagination-ellipsis": "body", @@ -481,6 +498,7 @@ export const DEFAULT_TYPE_SLOTS: FrontendTypeSlots = Object.freeze({ "empty-state-title": "control", "empty-state-title-illustrated": "section-title", "empty-state-description": "body", + "empty-state-reference": "surface-description", "metric-value": "metric-value", "metric-value-lg": "metric-value-lg", "metric-unit": "metric-unit", diff --git a/module/services/frontend/code/packages/saas-plugin-manifest/package.json b/module/services/frontend/code/packages/saas-plugin-manifest/package.json index cc2cb01c5..ea1a7f5bf 100644 --- a/module/services/frontend/code/packages/saas-plugin-manifest/package.json +++ b/module/services/frontend/code/packages/saas-plugin-manifest/package.json @@ -23,7 +23,7 @@ "test": "vitest run --config vitest.config.ts" }, "dependencies": { - "@codefly/saas-plugin-contract": "2.3.0" + "@codefly/saas-plugin-contract": "2.4.0" }, "devDependencies": { "ajv": "^8.17.1", diff --git a/module/services/frontend/code/packages/saas-plugin-react/package.json b/module/services/frontend/code/packages/saas-plugin-react/package.json index 398c6d105..7dd054ba4 100644 --- a/module/services/frontend/code/packages/saas-plugin-react/package.json +++ b/module/services/frontend/code/packages/saas-plugin-react/package.json @@ -30,11 +30,11 @@ "test": "vitest run --config vitest.config.ts" }, "peerDependencies": { - "@codefly/saas-plugin-contract": "2.3.0", + "@codefly/saas-plugin-contract": "2.4.0", "react": ">=19.2 <20" }, "devDependencies": { - "@codefly/saas-plugin-contract": "2.3.0", + "@codefly/saas-plugin-contract": "2.4.0", "@testing-library/react": "^16.1.0", "@types/react": "19.2.17", "happy-dom": "^20.8.9", diff --git a/module/services/frontend/code/packages/saas-ui/package.json b/module/services/frontend/code/packages/saas-ui/package.json index 0dcf98419..c28f0e1f6 100644 --- a/module/services/frontend/code/packages/saas-ui/package.json +++ b/module/services/frontend/code/packages/saas-ui/package.json @@ -1,6 +1,6 @@ { "name": "@codefly-dev/saas-ui", - "version": "0.11.5", + "version": "0.12.0", "description": "Reusable SaaS-domain frontend components (datasources, \u2026) the portal and solutions both import, built on @codefly-dev/ui primitives and driven by an injected typed client.", "repository": { "type": "git", @@ -42,7 +42,7 @@ "react": ">=19.2 <20", "react-hook-form": ">=7", "zod": ">=3", - "@codefly-dev/ui": "^0.11.0" + "@codefly-dev/ui": "^0.12.0" }, "devDependencies": { "@bufbuild/protobuf": "^2.11.0", @@ -59,7 +59,7 @@ "typescript": "5.9.3", "vitest": "^3.0.0", "zod": "^3.24.0", - "@codefly-dev/ui": "^0.11.0" + "@codefly-dev/ui": "^0.12.0" }, "publishConfig": { "access": "public" diff --git a/module/services/frontend/code/packages/saas-ui/src/datasources/__tests__/datasources-panel.test.tsx b/module/services/frontend/code/packages/saas-ui/src/datasources/__tests__/datasources-panel.test.tsx index a0c894af3..a3f0a295d 100644 --- a/module/services/frontend/code/packages/saas-ui/src/datasources/__tests__/datasources-panel.test.tsx +++ b/module/services/frontend/code/packages/saas-ui/src/datasources/__tests__/datasources-panel.test.tsx @@ -204,6 +204,24 @@ describe("DatasourcesPanel", () => { expect(screen.getByText(/pulls have stopped/i)).toBeTruthy(); }); + // Paused waits on someone; degraded is a fault; unknown is neither. They + // used to be secondary, destructive and outline, and secondary read as idle. + it.each([ + ["paused", "Paused", "warning"], + ["degraded", "Degraded", "danger"], + ["unknown", "Unknown", "neutral"], + ] as const)("paints a %s source in its status tone", async (status, label, tone) => { + const source: DatasourceView = { ...sampleSource, status }; + const client = fakeClient({ listSources: vi.fn(async () => [source]) }); + renderWithClient(); + + const badge = (await screen.findByText(label)).closest( + "[data-slot=badge]", + ); + expect(badge?.getAttribute("data-tone")).toBe(tone); + expect(badge?.querySelector("[data-slot=badge-dot]")).toBeTruthy(); + }); + it("leaves the status cell quiet for an active source", async () => { // Nearly every row is active, so badging it too buries the states that // need a reader — and it would sit one label away from `unknown`. @@ -477,6 +495,10 @@ describe("DatasourcesPanel", () => { const alert = await screen.findByRole("alert"); expect(alert.textContent).toContain("Couldn't sync"); expect(alert.textContent).toContain("gateway timeout"); + expect(alert.getAttribute("data-tone")).toBe("danger"); + + fireEvent.click(screen.getByRole("button", { name: "Dismiss the error" })); + expect(screen.queryByRole("alert")).toBeNull(); }); it("tracks sync state per row and blocks a double-enqueue", async () => { diff --git a/module/services/frontend/code/packages/saas-ui/src/datasources/__tests__/declared-source-card.test.tsx b/module/services/frontend/code/packages/saas-ui/src/datasources/__tests__/declared-source-card.test.tsx index 1e7da5075..f33c80f8f 100644 --- a/module/services/frontend/code/packages/saas-ui/src/datasources/__tests__/declared-source-card.test.tsx +++ b/module/services/frontend/code/packages/saas-ui/src/datasources/__tests__/declared-source-card.test.tsx @@ -112,7 +112,9 @@ describe("DeclaredSourceCard", () => { , ); - expect(await screen.findByText("Set up")).toBeTruthy(); + expect( + (await screen.findByText("Set up")).getAttribute("data-tone"), + ).toBe("neutral"); // The declaration is shown, as text — see "never offers the repository // as a field" below for the half that matters. expect(screen.getByText("example-org/handbook")).toBeTruthy(); @@ -226,7 +228,9 @@ describe("DeclaredSourceCard", () => { , ); - expect(await screen.findByText("Error")).toBeTruthy(); + expect( + (await screen.findByText("Error")).getAttribute("data-tone"), + ).toBe("danger"); // The host writes status_reason from a closed set of named reasons, so // it is rendered as it arrives rather than reworded into a guess. expect( @@ -252,6 +256,27 @@ describe("DeclaredSourceCard", () => { expect(client.addGitHubSource).not.toHaveBeenCalled(); }); + // The same source reads the same in DatasourcesPanel: paused is waiting on + // someone, not broken, and Reconnect is still the way back. + it("calls a paused source paused, as a warning, and still offers Reconnect", async () => { + const client = fakeClient({ + listSources: vi.fn(async () => [ + { ...connected, status: "paused" as const }, + ]), + }); + renderWithClient( + , + ); + + expect( + (await screen.findByText("Paused")).getAttribute("data-tone"), + ).toBe("warning"); + expect(screen.queryByText("Error")).toBeNull(); + expect( + screen.getByRole("button", { name: "Reconnect and sync" }), + ).toBeTruthy(); + }); + it("leaves a degraded source to an administrator when the viewer is not one", async () => { const client = fakeClient({ listSources: vi.fn(async () => [ @@ -278,8 +303,8 @@ describe("DeclaredSourceCard", () => { it("tells the three states apart by shape, and says the repository was not chosen here", async () => { // The consuming solution reported that a Connected badge on `secondary` // reads neutral beside an outline "Set up", so a working source looks - // like one nobody has connected. Each state now carries a dot as well as - // a tone, and Connected no longer borrows `secondary`. + // like one nobody has connected. Each state now carries a dot and its + // own status tone. const client = fakeClient({ listSources: vi.fn(async () => [connected]), }); @@ -289,6 +314,7 @@ describe("DeclaredSourceCard", () => { const badge = await screen.findByText("Connected"); expect(badge.className).not.toMatch(/bg-secondary/); + expect(badge.getAttribute("data-tone")).toBe("success"); expect(container.querySelector('[data-slot="badge-dot"]')).toBeTruthy(); // The card's own description slot, not a muted paragraph in the body: // the first thing to understand is that the repository below is not a @@ -348,6 +374,13 @@ describe("DeclaredSourceCard", () => { expect( await screen.findByText("More than one source matches this declaration"), ).toBeTruthy(); + // Both keep ingesting; nothing is broken, it is unresolved. + expect( + screen.getByText("More than one source").getAttribute("data-tone"), + ).toBe("warning"); + expect( + screen.getAllByRole("listitem").map((item) => item.textContent), + ).toEqual([expect.stringMatching(/^ds-1/), expect.stringMatching(/^ds-2/)]); expect(screen.getByText(/ds-1/)).toBeTruthy(); expect(screen.getByText(/2 connected sources read/)).toBeTruthy(); expect(screen.queryByRole("button", { name: "Sync now" })).toBeNull(); diff --git a/module/services/frontend/code/packages/saas-ui/src/datasources/datasources-panel.tsx b/module/services/frontend/code/packages/saas-ui/src/datasources/datasources-panel.tsx index 9ff43a617..457adcf86 100644 --- a/module/services/frontend/code/packages/saas-ui/src/datasources/datasources-panel.tsx +++ b/module/services/frontend/code/packages/saas-ui/src/datasources/datasources-panel.tsx @@ -13,6 +13,7 @@ import { Input, Label, Spinner, + type StatusTone, Table, TableBody, TableCell, @@ -552,7 +553,10 @@ function DatasourcesPanelView({
    ) : null} {appSetupUnredeemed && ( - + The GitHub App installation finished, but only an organization administrator can connect the repositories it granted. Ask one to connect them in Admin → Data sources; the installation itself is @@ -592,6 +596,7 @@ function DatasourcesPanelView({ it announced. */} {syncNotice && ( setSyncNotice(null)} dismissLabel="Dismiss the sync notice" @@ -600,22 +605,12 @@ function DatasourcesPanelView({ )} {actionError && ( - -
    - {actionError} - -
    -
    + setActionError(null)} + dismissLabel="Dismiss the error" + /> )} {/* One card per source whose sync is worth watching, above the table so @@ -754,13 +749,13 @@ function DatasourcesPanelView({ Read permission unresolved ) : readable ? ( - + {asPlatformAdministrator ? "You can read this collection (platform administrator)" : "You can read this collection"} ) : ( - + You do not have read access )} @@ -972,15 +967,16 @@ function LiveDeliveryCell({ source }: { source: DatasourceView }) { * How each status that is not `active` presents. Active is deliberately absent: * it is the state of nearly every row, so badging it too would bury the states * that need a reader under a column of noise — and it and `unknown` would then - * differ by their label alone. + * differ by their label alone. Paused waits on someone and is not a fault, so + * it is a warning, not the destructive red of a degraded source. */ const statusPresentation: Record< Exclude, - { label: string; variant: "outline" | "secondary" | "destructive" } + { label: string; tone: StatusTone } > = { - paused: { label: "Paused", variant: "secondary" }, - degraded: { label: "Degraded", variant: "destructive" }, - unknown: { label: "Unknown", variant: "outline" }, + paused: { label: "Paused", tone: "warning" }, + degraded: { label: "Degraded", tone: "danger" }, + unknown: { label: "Unknown", tone: "neutral" }, }; /** @@ -1009,18 +1005,22 @@ function StatusCell({ source }: { source: DatasourceView }) { return (
    {presentation && ( - {presentation.label} + + {presentation.label} + + )} + {source.statusReason && ( +

    {source.statusReason}

    )} - {source.statusReason &&

    {source.statusReason}

    } {source.status === "degraded" && ( -

    +

    Scheduled pulls have stopped; use Sync to retry once the cause is fixed. Content already ingested stays readable.

    )} {source.conformant === false && ( <> - Non-conformant provider + Non-conformant provider

    This source keeps syncing, but new sources of its provider cannot be connected until it meets the datasource connector requirements. diff --git a/module/services/frontend/code/packages/saas-ui/src/datasources/declared-source-card.tsx b/module/services/frontend/code/packages/saas-ui/src/datasources/declared-source-card.tsx index 8d7b82ec9..f18723ae5 100644 --- a/module/services/frontend/code/packages/saas-ui/src/datasources/declared-source-card.tsx +++ b/module/services/frontend/code/packages/saas-ui/src/datasources/declared-source-card.tsx @@ -5,6 +5,9 @@ import { Banner, Button, Card, + DescriptionList, + List, + ListItem, useLoadingPhase, } from "@codefly-dev/ui/layout"; import { @@ -229,7 +232,10 @@ function DeclaredSourceCardView({ reads as "not an administrator", and a failed list proves nothing and already shows its own error. */} {appSetup.unredeemable && list.isSuccess && ( - + The GitHub App is installed. Only an organization administrator can connect this source with it; ask one to open this page. The installation itself is already in place. @@ -406,32 +412,38 @@ function StateBadge({ source: DatasourceView | undefined; }) { // Every state carries a dot, so the three are told apart at a glance by - // shape as well as by colour. Connected does NOT use `secondary`: it reads - // as neutral beside an outline "Set up", which leaves a reader unable to - // tell a source that is working from one nobody has connected yet — the - // opposite of what this card exists to say. (The kit has no `success` - // token to tint it with; see the note in the PR.) + // shape as well as by tone. Two sources serving one declaration is a + // warning, not an error: both keep ingesting, and nothing on this card is + // broken — it cannot say which one is the solution's. if (state === "connected") return ( - + Connected ); if (state === "setup") return ( - + Set up ); if (state === "ambiguous") return ( - + More than one source ); + // Paused waits on someone and is not a fault, as DatasourcesPanel says of + // the same source; it still offers Reconnect below, as an error does. + if (source?.status === "paused") + return ( + + Paused + + ); return ( - - Error{source?.status === "paused" ? " · paused" : ""} + + Error ); } @@ -440,18 +452,30 @@ function StateBadge({ function Declaration({ declared }: { declared: DeclaredSource }) { const paths = declared.paths ?? []; return ( -

    -
    Repository
    -
    {declared.repo}
    -
    Paths
    -
    - {paths.length ? paths.join(", ") : "The whole repository"} -
    -
    Branch
    -
    - {declared.ref || "The repository default"} -
    -
    + {declared.repo}, + }, + { + term: "Paths", + value: paths.length ? ( + {paths.join(", ")} + ) : ( + "The whole repository" + ), + }, + { + term: "Branch", + value: declared.ref ? ( + {declared.ref} + ) : ( + "The repository default" + ), + }, + ]} + /> ); } @@ -469,7 +493,10 @@ function AmbiguousMatches({ matches: DatasourceView[]; }) { return ( - +

    {`${matches.length} connected sources read `} {declared.repo} @@ -477,17 +504,17 @@ function AmbiguousMatches({

    {/* Each one by its id: every other column can be identical, and the id is what names the row to remove in Admin → Data sources. */} -
      + {matches.map((match) => ( -
    • + {`${match.id} · ${match.branch || "default branch"} · ${ match.paths.length ? match.paths.join(", ") : "whole repository" } · ${match.status}`} -
    • + ))} -
    +
    ); } diff --git a/module/services/frontend/code/scripts/test-published-ui.mjs b/module/services/frontend/code/scripts/test-published-ui.mjs index 3615a69f7..e6e699a7b 100644 --- a/module/services/frontend/code/scripts/test-published-ui.mjs +++ b/module/services/frontend/code/scripts/test-published-ui.mjs @@ -115,6 +115,26 @@ console.log("Packed UI declarations and generic consumer rendering passed"); cwd: temporary, stdio: "inherit", }); + // A solution previews itself from the published package alone, so the + // stylesheets must resolve through its exports and arrive compiled. + writeFileSync( + join(temporary, "styles.mjs"), + ` +import { readFileSync } from "node:fs"; +import { fileURLToPath } from "node:url"; +const read = (specifier) => readFileSync(fileURLToPath(import.meta.resolve(specifier)), "utf8"); +const preview = read("@codefly-dev/ui/preview.css"); +for (const needle of [".text-success", "--appearance-light-success:", ".type-chip"]) + if (!preview.includes(needle)) throw new Error(\`published preview.css lacks \${needle}\`); +if (preview.includes("@tailwind") || preview.includes("@utility ")) throw new Error("published preview.css is not compiled"); +if (!read("@codefly-dev/ui/theme.css").includes("--color-success: var(--success)")) throw new Error("published theme.css lacks the status tokens"); +console.log("Packed kit stylesheets resolve and are complete"); +`, + ); + execFileSync(process.execPath, ["styles.mjs"], { + cwd: temporary, + stdio: "inherit", + }); } finally { rmSync(temporary, { recursive: true, force: true }); } diff --git a/module/services/frontend/code/src/app/globals.css b/module/services/frontend/code/src/app/globals.css index f011301c9..ffa357cf9 100644 --- a/module/services/frontend/code/src/app/globals.css +++ b/module/services/frontend/code/src/app/globals.css @@ -1,6 +1,10 @@ @import "tailwindcss"; @import "tw-animate-css"; -@import "./shadcn.css"; +/* The kit's token layer: contract token -> utility, the light/dark binding to + the `--appearance-*` properties src/lib/appearance.ts projects onto , + and the custom variants its components name. Owned by the kit so the classes + a component writes and the variables they read ship together. */ +@import "@codefly-dev/ui/theme.css"; /* The kit's type slots and control rungs (`type-card-title`, `control-sm`): one utility per slot and rung, generated in the kit from the contract's vocabulary and shipped with it, so the classes its components name are defined wherever @@ -10,227 +14,9 @@ /* The host owns the single compiled stylesheet (CSS ownership: host authority, host CSS authority). Its Tailwind build only scans files it can see, so it must - scan the kit's source too — the kit ships no other CSS and the promoted - primitives' Tailwind classes (issue #451) live in the kit source, not under - ./src. This generalizes to every consumer the host renders (module UI, solution + scan the kit's source too — the kit ships its layers above as Tailwind source, + not compiled utilities, and the promoted primitives' Tailwind classes (issue + #451) live in the kit source, not under ./src. This generalizes to every consumer the host renders (module UI, solution remotes) as they move into the kit. */ @source "../../packages/codefly-ui/src"; @source "../../packages/saas-ui/src"; - -@custom-variant dark (&:is(.dark *)); - -@theme inline { - --color-background: var(--background); - --color-foreground: var(--foreground); - --color-card: var(--card); - --color-card-foreground: var(--card-foreground); - --color-primary: var(--primary); - --color-primary-foreground: var(--primary-foreground); - --color-secondary: var(--secondary); - --color-secondary-foreground: var(--secondary-foreground); - --color-muted: var(--muted); - --color-muted-foreground: var(--muted-foreground); - --color-accent: var(--accent); - --color-accent-foreground: var(--accent-foreground); - --color-destructive: var(--destructive); - --color-border: var(--border); - --color-input: var(--input); - --color-ring: var(--ring); - --color-sidebar: var(--sidebar); - --color-sidebar-foreground: var(--sidebar-foreground); - --color-sidebar-accent: var(--sidebar-accent); - --radius-sm: calc(var(--radius) * 0.6); - --radius-md: calc(var(--radius) * 0.8); - --radius-lg: var(--radius); - --radius-xl: calc(var(--radius) * 1.4); - --font-heading: var(--appearance-font-heading); - --font-sans: var(--appearance-font-sans); - --font-mono: var(--appearance-font-mono); - /* Interaction states of the primary action. A skin that decides them wins; - otherwise they derive from --primary here, in one place. */ - --color-primary-hover: var(--primary-hover); - --color-primary-active: var(--primary-active); - --color-disabled: var(--disabled); - --color-disabled-foreground: var(--disabled-foreground); - /* Base spacing unit: rescales every spacing/size utility (density). */ - --spacing: var(--appearance-spacing); - /* Elevation scale, tunable via the shadowStrength multiplier. */ - --shadow-sm: 0 1px 3px 0 - rgb(0 0 0 / calc(0.1 * var(--appearance-shadow-strength))), - 0 1px 2px -1px rgb(0 0 0 / calc(0.1 * var(--appearance-shadow-strength))); - --shadow-md: 0 4px 6px -1px - rgb(0 0 0 / calc(0.1 * var(--appearance-shadow-strength))), - 0 2px 4px -2px rgb(0 0 0 / calc(0.1 * var(--appearance-shadow-strength))); - --shadow-lg: 0 10px 15px -3px - rgb(0 0 0 / calc(0.1 * var(--appearance-shadow-strength))), - 0 4px 6px -4px rgb(0 0 0 / calc(0.1 * var(--appearance-shadow-strength))); - --shadow-xl: 0 20px 25px -5px - rgb(0 0 0 / calc(0.1 * var(--appearance-shadow-strength))), - 0 8px 10px -6px rgb(0 0 0 / calc(0.1 * var(--appearance-shadow-strength))); - --shadow-2xl: 0 25px 50px -12px - rgb(0 0 0 / calc(0.25 * var(--appearance-shadow-strength))); - --color-sidebar-ring: var(--sidebar-ring); - --color-sidebar-border: var(--sidebar-border); - --color-sidebar-accent-foreground: var(--sidebar-accent-foreground); - --color-sidebar-primary-foreground: var(--sidebar-primary-foreground); - --color-sidebar-primary: var(--sidebar-primary); - --color-chart-5: var(--chart-5); - --color-chart-4: var(--chart-4); - --color-chart-3: var(--chart-3); - --color-chart-2: var(--chart-2); - --color-chart-1: var(--chart-1); - --color-popover-foreground: var(--popover-foreground); - --color-popover: var(--popover); - --radius-2xl: calc(var(--radius) * 1.8); - --radius-3xl: calc(var(--radius) * 2.2); - --radius-4xl: calc(var(--radius) * 2.6); -} - -:root { - --radius: var(--appearance-radius); - --background: var(--appearance-light-background); - --foreground: var(--appearance-light-foreground); - --card: var(--appearance-light-card); - --card-foreground: var(--appearance-light-card-foreground); - --popover: var(--appearance-light-popover); - --popover-foreground: var(--appearance-light-popover-foreground); - --primary: var(--appearance-light-primary); - --primary-foreground: var(--appearance-light-primary-foreground); - --primary-hover: var( - --appearance-light-primary-hover, - color-mix(in oklab, var(--primary) 90%, transparent) - ); - --primary-active: var( - --appearance-light-primary-active, - color-mix(in oklab, var(--primary) 80%, transparent) - ); - --disabled: var(--appearance-light-disabled, var(--primary)); - --disabled-foreground: var( - --appearance-light-disabled-foreground, - var(--primary-foreground) - ); - --disabled-opacity: var(--appearance-light-disabled-opacity, 0.5); - --secondary: var(--appearance-light-secondary); - --secondary-foreground: var(--appearance-light-secondary-foreground); - --muted: var(--appearance-light-muted); - --muted-foreground: var(--appearance-light-muted-foreground); - --accent: var(--appearance-light-accent); - --accent-foreground: var(--appearance-light-accent-foreground); - --destructive: var(--appearance-light-destructive); - --border: var(--appearance-light-border); - --input: var(--appearance-light-input); - --ring: var(--appearance-light-ring); - --sidebar: var(--appearance-light-sidebar); - --sidebar-foreground: var(--appearance-light-sidebar-foreground); - --sidebar-primary: var(--appearance-light-sidebar-primary); - --sidebar-primary-foreground: var( - --appearance-light-sidebar-primary-foreground - ); - --sidebar-accent: var(--appearance-light-sidebar-accent); - --sidebar-accent-foreground: var( - --appearance-light-sidebar-accent-foreground - ); - --sidebar-border: var(--appearance-light-sidebar-border); - --sidebar-ring: var(--appearance-light-sidebar-ring); - --chart-1: var(--appearance-light-chart1); - --chart-2: var(--appearance-light-chart2); - --chart-3: var(--appearance-light-chart3); - --chart-4: var(--appearance-light-chart4); - --chart-5: var(--appearance-light-chart5); -} - -.dark { - --background: var(--appearance-dark-background); - --foreground: var(--appearance-dark-foreground); - --card: var(--appearance-dark-card); - --card-foreground: var(--appearance-dark-card-foreground); - --popover: var(--appearance-dark-popover); - --popover-foreground: var(--appearance-dark-popover-foreground); - --primary: var(--appearance-dark-primary); - --primary-foreground: var(--appearance-dark-primary-foreground); - --primary-hover: var( - --appearance-dark-primary-hover, - color-mix(in oklab, var(--primary) 90%, transparent) - ); - --primary-active: var( - --appearance-dark-primary-active, - color-mix(in oklab, var(--primary) 80%, transparent) - ); - --disabled: var(--appearance-dark-disabled, var(--primary)); - --disabled-foreground: var( - --appearance-dark-disabled-foreground, - var(--primary-foreground) - ); - --disabled-opacity: var(--appearance-dark-disabled-opacity, 0.5); - --secondary: var(--appearance-dark-secondary); - --secondary-foreground: var(--appearance-dark-secondary-foreground); - --muted: var(--appearance-dark-muted); - --muted-foreground: var(--appearance-dark-muted-foreground); - --accent: var(--appearance-dark-accent); - --accent-foreground: var(--appearance-dark-accent-foreground); - --destructive: var(--appearance-dark-destructive); - --border: var(--appearance-dark-border); - --input: var(--appearance-dark-input); - --ring: var(--appearance-dark-ring); - --sidebar: var(--appearance-dark-sidebar); - --sidebar-foreground: var(--appearance-dark-sidebar-foreground); - --sidebar-primary: var(--appearance-dark-sidebar-primary); - --sidebar-primary-foreground: var( - --appearance-dark-sidebar-primary-foreground - ); - --sidebar-accent: var(--appearance-dark-sidebar-accent); - --sidebar-accent-foreground: var(--appearance-dark-sidebar-accent-foreground); - --sidebar-border: var(--appearance-dark-sidebar-border); - --sidebar-ring: var(--appearance-dark-sidebar-ring); - --chart-1: var(--appearance-dark-chart1); - --chart-2: var(--appearance-dark-chart2); - --chart-3: var(--appearance-dark-chart3); - --chart-4: var(--appearance-dark-chart4); - --chart-5: var(--appearance-dark-chart5); -} - -body { - font-family: var(--appearance-font-sans); -} - -@layer base { - * { - @apply border-border outline-ring/50; - } - body { - @apply bg-background text-foreground; - } - html { - @apply font-sans; - font-size: var(--appearance-font-size-base); - } -} - -/* - * Border width is an application-configurable token. These rules are - * intentionally unlayered so they win over Tailwind's `utilities` layer, - * letting the appearance token redefine the 1px `border` utility globally - * (numeric widths like `border-2` remain intentional emphasis). The token - * defaults to 1px, so omitting it leaves borders unchanged. - */ -.border { - border-width: var(--appearance-border-width); -} -.border-t { - border-top-width: var(--appearance-border-width); -} -.border-r { - border-right-width: var(--appearance-border-width); -} -.border-b { - border-bottom-width: var(--appearance-border-width); -} -.border-l { - border-left-width: var(--appearance-border-width); -} -.border-x { - border-inline-width: var(--appearance-border-width); -} -.border-y { - border-block-width: var(--appearance-border-width); -} diff --git a/module/services/frontend/code/src/app/shadcn.css b/module/services/frontend/code/src/app/shadcn.css deleted file mode 100644 index 7bafb0e5e..000000000 --- a/module/services/frontend/code/src/app/shadcn.css +++ /dev/null @@ -1,112 +0,0 @@ -/* - * Local Tailwind helpers used by the generated shadcn/Base UI components. - * - * MIT License - * Copyright (c) 2023 shadcn - * - * 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. - */ -@theme inline { - @keyframes accordion-down { - from { - height: 0; - } - to { - height: var(--accordion-panel-height, auto); - } - } - - @keyframes accordion-up { - from { - height: var(--accordion-panel-height, auto); - } - to { - height: 0; - } - } -} - -@custom-variant data-open { - &:where([data-state="open"]), - &:where([data-open]:not([data-open="false"])) { - @slot; - } -} - -@custom-variant data-closed { - &:where([data-state="closed"]), - &:where([data-closed]:not([data-closed="false"])) { - @slot; - } -} - -@custom-variant data-checked { - &:where([data-state="checked"]), - &:where([data-checked]:not([data-checked="false"])) { - @slot; - } -} - -@custom-variant data-unchecked { - &:where([data-state="unchecked"]), - &:where([data-unchecked]:not([data-unchecked="false"])) { - @slot; - } -} - -@custom-variant data-selected { - &:where([data-selected="true"]) { - @slot; - } -} - -@custom-variant data-disabled { - &:where([data-disabled="true"]), - &:where([data-disabled]:not([data-disabled="false"])) { - @slot; - } -} - -@custom-variant data-active { - &:where([data-state="active"]), - &:where([data-active]:not([data-active="false"])) { - @slot; - } -} - -@custom-variant data-horizontal { - &:where([data-orientation="horizontal"]) { - @slot; - } -} - -@custom-variant data-vertical { - &:where([data-orientation="vertical"]) { - @slot; - } -} - -@utility no-scrollbar { - -ms-overflow-style: none; - scrollbar-width: none; - - &::-webkit-scrollbar { - display: none; - } -} diff --git a/module/services/frontend/code/src/lib/__tests__/appearance.test.ts b/module/services/frontend/code/src/lib/__tests__/appearance.test.ts index 74d7d15c2..767cd7b1d 100644 --- a/module/services/frontend/code/src/lib/__tests__/appearance.test.ts +++ b/module/services/frontend/code/src/lib/__tests__/appearance.test.ts @@ -1,8 +1,4 @@ -import { readFileSync } from "node:fs"; -import { - FRONTEND_APPEARANCE_TOKEN_NAMES, - resolveFrontendAppearance, -} from "@codefly/saas-plugin-contract"; +import { resolveFrontendAppearance } from "@codefly/saas-plugin-contract"; import { describe, expect, it } from "vitest"; import { appearanceStyleProperties, @@ -62,42 +58,3 @@ describe("application appearance projection", () => { expect(readableForeground("#111827")).toBe("#ffffff"); }); }); - -// The shadcn variable a token is consumed by: camelCase → kebab, with a dash -// before the digit in the chart tokens (`chart1` → `--chart-1`). -function shadcnVariable(token: string): string { - return `--${token - .replace(/([a-z])([0-9])/g, "$1-$2") - .replace(/[A-Z]/g, (letter) => `-${letter.toLowerCase()}`)}`; -} - -// The flat CSS rule block for a selector (no nested braces in these blocks). -function cssBlock(source: string, selector: string): string { - const open = source.indexOf(`${selector} {`); - if (open < 0) throw new Error(`globals.css has no ${selector} block`); - const close = source.indexOf("\n}", open); - if (close < 0) - throw new Error(`globals.css ${selector} block is unterminated`); - return source.slice(open, close); -} - -// The contract owns the token names; globals.css is where they become the -// shadcn variables every layer above consumes. This ties the two: if a token's -// consumed variable is renamed in globals.css (or a new token gains no binding), -// the semantic utility that reads it silently breaks with no other test firing. -describe("globals.css binds every contract token to its consumed variable", () => { - const globals = readFileSync("src/app/globals.css", "utf8"); - const light = cssBlock(globals, ":root"); - const dark = cssBlock(globals, ".dark"); - - for (const token of FRONTEND_APPEARANCE_TOKEN_NAMES) { - it(`defines ${shadcnVariable(token)} in :root and .dark`, () => { - expect(light, `:root must define ${shadcnVariable(token)}`).toContain( - `${shadcnVariable(token)}:`, - ); - expect(dark, `.dark must define ${shadcnVariable(token)}`).toContain( - `${shadcnVariable(token)}:`, - ); - }); - } -}); diff --git a/module/services/frontend/code/src/lib/__tests__/design-tokens.test.ts b/module/services/frontend/code/src/lib/__tests__/design-tokens.test.ts index f4bc12798..917f87eb7 100644 --- a/module/services/frontend/code/src/lib/__tests__/design-tokens.test.ts +++ b/module/services/frontend/code/src/lib/__tests__/design-tokens.test.ts @@ -61,7 +61,7 @@ describe("optional design tokens are projected only when decided", () => { it("derives the interaction states in the stylesheet, in both themes", () => { const css = readFileSync( - join(process.cwd(), "src/app/globals.css"), + join(process.cwd(), "packages/codefly-ui/src/skin/theme.css"), "utf8", ); for (const mode of ["light", "dark"]) { diff --git a/module/services/frontend/code/src/lib/__tests__/preview-stylesheet.test.tsx b/module/services/frontend/code/src/lib/__tests__/preview-stylesheet.test.tsx new file mode 100644 index 000000000..2c37ac12a --- /dev/null +++ b/module/services/frontend/code/src/lib/__tests__/preview-stylesheet.test.tsx @@ -0,0 +1,170 @@ +import { execFileSync } from "node:child_process"; +import { mkdtempSync, readFileSync, writeFileSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { + DEFAULT_FRONTEND_APPEARANCE, + FRONTEND_APPEARANCE_TOKEN_NAMES, +} from "@codefly/saas-plugin-contract"; +import { cleanup, render } from "@testing-library/react"; +import { afterEach, describe, expect, it } from "vitest"; + +import * as composites from "../../../packages/codefly-ui/stories/composites.stories"; +import * as content from "../../../packages/codefly-ui/stories/content.stories"; +import * as controls from "../../../packages/codefly-ui/stories/controls.stories"; +import * as foundations from "../../../packages/codefly-ui/stories/foundations.stories"; +import * as metrics from "../../../packages/codefly-ui/stories/metrics.stories"; +import * as primitives from "../../../packages/codefly-ui/stories/primitives.stories"; +import * as semanticTables from "../../../packages/codefly-ui/stories/semantic-table.stories"; +import * as tables from "../../../packages/codefly-ui/stories/table.stories"; +import { appearanceVariableName } from "../appearance"; + +const KIT = join(process.cwd(), "packages/codefly-ui"); +const PREVIEW = join(KIT, "src/skin/preview.generated.css"); +const preview = readFileSync(PREVIEW, "utf8"); + +function checkPreview(env: Record = {}) { + execFileSync( + process.execPath, + ["scripts/generate-preview-stylesheet.mjs", "--check"], + { cwd: KIT, stdio: "pipe", env: { ...process.env, ...env } }, + ); +} + +afterEach(cleanup); + +describe("the kit's own stylesheets are what the host compiles", () => { + it("exports its token layer and its preview, and the host imports the layer", () => { + const kit = JSON.parse(readFileSync(join(KIT, "package.json"), "utf8")) as { + exports: Record; + }; + expect(kit.exports["./theme.css"]).toBe("./src/skin/theme.css"); + expect(kit.exports["./preview.css"]).toBe( + "./src/skin/preview.generated.css", + ); + const globals = readFileSync( + join(process.cwd(), "src/app/globals.css"), + "utf8", + ); + expect(globals).toContain('@import "@codefly-dev/ui/theme.css";'); + }); +}); + +// Its bytes depend on inputs outside the kit's folder — the contract's default +// skin, the Tailwind and Lightning CSS versions in the lockfile — which the +// kit-version gate cannot see. Committed and checked here, any of them moving +// shows up as a changed file in the kit, which the gate does see. +describe("preview.css is regenerated whenever an input moves", () => { + it("is not stale", () => { + expect(() => checkPreview()).not.toThrow(); + }); + + it("refuses a copy that differs from what the inputs produce", () => { + const stale = join(mkdtempSync(join(tmpdir(), "preview-")), "stale.css"); + writeFileSync(stale, `${preview}/* one byte off */\n`); + expect(() => checkPreview({ PREVIEW_STYLESHEET_PATH: stale })).toThrow(); + }); +}); + +describe("preview.css paints the kit without a host", () => { + it("is plain CSS, with nothing left for a Tailwind build to resolve", () => { + for (const directive of ["@import", "@theme", "@utility", "@apply", "@source", "@custom-variant"]) + expect(preview, `${directive} survived the compile`).not.toContain( + `${directive} `, + ); + }); + + // What the host would have projected onto , so every variable a kit + // class reads resolves. Names, not values: the compile normalises a value's + // spelling (`oklch(1 0 0)` becomes `oklch(100% 0 0)`), and the values are + // the projection's own output. + it("sets the default skin's values at the root", () => { + for (const mode of ["light", "dark"] as const) + for (const token of FRONTEND_APPEARANCE_TOKEN_NAMES) { + if (DEFAULT_FRONTEND_APPEARANCE[mode][token] === undefined) continue; + expect(preview).toContain(`${appearanceVariableName(mode, token)}:`); + } + expect(preview).toContain("--type-card-title-size:"); + expect(preview).toContain("--control-default-height:"); + }); + + // The claim a solution relies on: a page built from kit components is fully + // painted. Every class every story renders must be defined, or that element + // previews unstyled while it looks fine in the host — the gap this file + // exists to close, reopened silently. + it("defines every class the kit's stories render", () => { + const missing = new Set(); + for (const stories of [ + composites, + content, + controls, + foundations, + metrics, + primitives, + semanticTables, + tables, + ]) + for (const story of Object.values(stories)) { + if (typeof story !== "object" || !story || !("render" in story)) + continue; + const Story = story.render as () => React.ReactElement; + const { baseElement } = render(); + for (const element of baseElement.querySelectorAll("[class]")) + for (const name of element.getAttribute("class")?.split(/\s+/) ?? []) + if ( + name && + !MARKERS.test(name) && + !storyAuthored.has(name) && + !KNOWN_UNSTYLED.has(name) && + !defines(name) + ) + missing.add(name); + cleanup(); + } + expect([...missing].sort()).toEqual([]); + }); +}); + +// Names that style nothing themselves: Tailwind's group/peer markers, which +// other classes select through, and the icon library's own name tags. +const MARKERS = /^(?:(?:group|peer)(?:\/[\w-]+)?|lucide(?:-[\w-]+)?)$/; + +// A story lays its example out with classes of its own (`w-40` around a +// select); those are the story's, not the kit's, and preview.css is the kit. +const storyAuthored = new Set( + [ + "composites", + "content", + "controls", + "foundations", + "metrics", + "primitives", + "semantic-table", + "table", + ].flatMap((name) => + [ + ...readFileSync(join(KIT, `stories/${name}.stories.tsx`), "utf8").matchAll( + /className="([^"]*)"/g, + ), + ].flatMap((match) => match[1].split(/\s+/)), + ), +); + +// Unstyled in the host too, so not a preview gap: a type slot's utility wraps +// its rule in `:where(&)`, and behind the `file:` variant that becomes +// `:where(.x::file-selector-button)` — a pseudo-element inside `:where()`, +// which a browser drops and the preview compile removes. +const KNOWN_UNSTYLED = new Set(["file:type-input"]); + +// Tailwind's selector escaping: every character outside [A-Za-z0-9_-]. +function escape(name: string): string { + return name.replace(/[^A-Za-z0-9_-]/g, (character) => `\\${character}`); +} + +// The class as a whole selector, not a prefix: `.border` must not be satisfied +// by `.border-border`, nor `.p-4` by `.p-4\.5`. +function defines(name: string): boolean { + return new RegExp( + `\\.${escape(name).replace(/[\\^$.*+?()[\]{}|]/g, "\\$&")}(?![A-Za-z0-9_\\\\-])`, + ).test(preview); +} diff --git a/module/services/frontend/code/src/lib/appearance.ts b/module/services/frontend/code/src/lib/appearance.ts index 82d09343a..979fcd76c 100644 --- a/module/services/frontend/code/src/lib/appearance.ts +++ b/module/services/frontend/code/src/lib/appearance.ts @@ -1,128 +1,8 @@ -import { - FRONTEND_APPEARANCE_TOKEN_NAMES, - FRONTEND_CONTROL_SIZE_NAMES, - FRONTEND_TYPE_SLOT_NAMES, - type FrontendAppearance, - type FrontendAppearanceTokenName, - type ResolvedTypeSlot, - resolveTypeRole, - resolveTypeSlot, -} from "@codefly/saas-plugin-contract"; -import type { CSSProperties } from "react"; - -/** - * The custom properties a resolved skin puts on ``: the appearance tokens, - * plus the flattened type slots and control rungs the generated utilities read. - * A property a slot's role does not decide is absent rather than empty, so every - * key is optional. - */ -export type AppearanceStyleProperties = CSSProperties & - Partial< - Record< - `--appearance-${string}` | `--type-${string}` | `--control-${string}`, - string - > - >; - -export function appearanceVariableName( - mode: "light" | "dark", - token: FrontendAppearanceTokenName, -): `--appearance-${"light" | "dark"}-${string}` { - return `--appearance-${mode}-${token.replace(/[A-Z]/g, (letter) => `-${letter.toLowerCase()}`)}`; -} - -/** - * Produces the SSR-safe custom properties consumed by globals.css. Keeping - * both palettes on lets the client theme provider switch one class - * without recomputing the tenant appearance tokens. - */ -export function appearanceStyleProperties( - appearance: FrontendAppearance, -): AppearanceStyleProperties { - const properties: Record = { - // A behaviour, not a token: DateField reads it to choose its shape. It - // rides the same channel as the tokens so the explorer and SSR agree. - "--appearance-date-pattern": appearance.datePattern, - "--appearance-radius": appearance.radius, - "--appearance-font-sans": appearance.fontSans, - "--appearance-font-heading": appearance.fontHeading, - "--appearance-font-mono": appearance.fontMono, - "--appearance-spacing": appearance.spacing, - "--appearance-font-size-base": appearance.fontSizeBase, - "--appearance-sidebar-width": appearance.sidebarWidth, - "--appearance-sidebar-width-icon": appearance.sidebarWidthIcon, - "--appearance-border-width": appearance.borderWidth, - "--appearance-shadow-strength": appearance.shadowStrength, - }; - // Optional values are projected only when the skin decided them; the - // stylesheet's `var(--…, fallback)` derives the rest. `--appearance-…-disabled - // -opacity` is the presence signal for the disabled fill: a decided fill - // renders flat at full opacity, an absent one keeps the faded variant. - if (appearance.buttonRadius !== undefined) - properties["--appearance-button-radius"] = appearance.buttonRadius; - for (const mode of ["light", "dark"] as const) { - for (const token of FRONTEND_APPEARANCE_TOKEN_NAMES) { - const value = appearance[mode][token]; - if (value !== undefined) - properties[appearanceVariableName(mode, token)] = value; - } - if (appearance[mode].disabled !== undefined) - properties[`--appearance-${mode}-disabled-opacity`] = "1"; - } - // Layers 1 to 3, flattened. The kit's generated stylesheet declares one - // utility per slot reading exactly the variables the slot's role decides, and - // this projects exactly those: both derive the set from the same contract, - // which refuses a skin that would change it. A property the role does not - // decide is therefore absent on both sides — never declared, never projected - // — so the element keeps whatever else styles it, as it did before slots. - for (const slot of FRONTEND_TYPE_SLOT_NAMES) - writeTypeProperties( - properties, - `--type-${slot}`, - resolveTypeSlot(appearance, slot), - ); - for (const size of FRONTEND_CONTROL_SIZE_NAMES) { - const rung = appearance.controlSizes[size]; - // Geometry is stored as `--spacing` multiples, not lengths, so a skin that - // changes density moves control geometry with it instead of pinning it. - properties[`--control-${size}-height`] = spacingUnits(rung.height); - properties[`--control-${size}-padding-x`] = spacingUnits(rung.paddingX); - properties[`--control-${size}-icon`] = spacingUnits(rung.icon); - writeTypeProperties( - properties, - `--control-${size}-text`, - resolveTypeRole(appearance, rung.text), - ); - } - return properties as AppearanceStyleProperties; -} - -/** `8` becomes `calc(var(--spacing) * 8)`, matching Tailwind's own sizing utilities. */ -function spacingUnits(multiple: string): string { - return `calc(var(--spacing) * ${multiple})`; -} - -function writeTypeProperties( - properties: Record, - prefix: string, - resolved: ResolvedTypeSlot, -): void { - if (resolved.fontSize !== undefined) - properties[`${prefix}-size`] = resolved.fontSize; - if (resolved.fontWeight !== undefined) - properties[`${prefix}-weight`] = resolved.fontWeight; - if (resolved.lineHeight !== undefined) - properties[`${prefix}-leading`] = resolved.lineHeight; - if (resolved.letterSpacing !== undefined) - properties[`${prefix}-tracking`] = resolved.letterSpacing; - if (resolved.fontFamily !== undefined) - properties[`${prefix}-family`] = - resolved.fontFamily === "heading" - ? "var(--font-heading)" - : resolved.fontFamily === "mono" - ? "var(--font-mono)" - : "var(--font-sans)"; -} +export { + type AppearanceStyleProperties, + appearanceStyleProperties, + appearanceVariableName, +} from "@codefly-dev/ui/skin"; export function readableForeground(hexColor: string): "#000000" | "#ffffff" { const channels = [1, 3, 5].map( diff --git a/module/services/frontend/code/ui-inventory.json b/module/services/frontend/code/ui-inventory.json index c45bb2211..157a60831 100644 --- a/module/services/frontend/code/ui-inventory.json +++ b/module/services/frontend/code/ui-inventory.json @@ -435,6 +435,10 @@ { "file": "packages/codefly-ui/stories/primitives.stories.tsx", "export": "BadgesAndAvatars" + }, + { + "file": "packages/codefly-ui/stories/primitives.stories.tsx", + "export": "StatusBadges" } ], "coverage": "owner stories; browser/skin verification pending", @@ -445,6 +449,7 @@ "imports": [ "lucide-react", "react", + "./badge.js", "./button.js", "./cn.js" ], @@ -455,6 +460,10 @@ { "file": "packages/codefly-ui/stories/primitives.stories.tsx", "export": "NotificationBanner" + }, + { + "file": "packages/codefly-ui/stories/primitives.stories.tsx", + "export": "StatusBanners" } ], "coverage": "owner stories; browser/skin verification pending", @@ -519,6 +528,10 @@ { "file": "packages/codefly-ui/stories/controls.stories.tsx", "export": "CompactCard" + }, + { + "file": "packages/codefly-ui/stories/primitives.stories.tsx", + "export": "CardWithDescription" } ], "coverage": "owner stories; browser/skin verification pending", @@ -543,6 +556,27 @@ "coverage": "owner stories; browser/skin verification pending", "remaining": "Verify explorer rendering, interactive accessibility and visual baselines under external skins." }, + { + "source": "packages/codefly-ui/src/layout/chip.tsx", + "imports": [ + "@base-ui/react/use-render", + "lucide-react", + "react", + "./badge.js", + "./cn.js" + ], + "owner": "@codefly-dev/ui", + "disposition": "already canonical", + "reason": "An actionable badge (link, button, removable) for relation, scope, status and owner rows in solution pages.", + "stories": [ + { + "file": "packages/codefly-ui/stories/primitives.stories.tsx", + "export": "Chips" + } + ], + "coverage": "owner stories; browser/skin verification pending", + "remaining": "Verify explorer rendering, interactive accessibility and visual baselines under external skins." + }, { "source": "packages/codefly-ui/src/layout/command.tsx", "owner": "@codefly-dev/ui", @@ -659,6 +693,10 @@ { "file": "packages/codefly-ui/stories/primitives.stories.tsx", "export": "Empty" + }, + { + "file": "packages/codefly-ui/stories/primitives.stories.tsx", + "export": "EmptyWithReference" } ], "coverage": "owner stories; browser/skin verification pending", @@ -788,6 +826,28 @@ "coverage": "owner stories; browser/skin verification pending", "remaining": "Verify explorer rendering, interactive accessibility and visual baselines under external skins." }, + { + "source": "packages/codefly-ui/src/layout/list.tsx", + "imports": [ + "react", + "./cn.js" + ], + "owner": "@codefly-dev/ui", + "disposition": "already canonical", + "reason": "Light list and term/value primitives for rows where a Table is too heavy.", + "stories": [ + { + "file": "packages/codefly-ui/stories/primitives.stories.tsx", + "export": "Lists" + }, + { + "file": "packages/codefly-ui/stories/primitives.stories.tsx", + "export": "DescriptionLists" + } + ], + "coverage": "owner stories; browser/skin verification pending", + "remaining": "Verify explorer rendering, interactive accessibility and visual baselines under external skins." + }, { "source": "packages/codefly-ui/src/layout/notice.tsx", "imports": [