From e250d266e8bb5fa1524693e735ff57c4ee2b95cb Mon Sep 17 00:00:00 2001 From: Guillem Arias Fauste Date: Fri, 1 May 2026 14:46:33 +0200 Subject: [PATCH] refactor(design-system): single-source design tokens via DTCG JSON (#1604) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * refactor(css): rename maybe-design-system → sure-design-system Rename design system CSS file and directory to match the project name post-rebrand. Update internal imports plus references in CLAUDE.md, copilot instructions, and Junie guidelines. No CSS rules change; Tailwind compiled output is byte-identical. * build(tokens): introduce single-source tokens.json + build script Make the design system a tool-agnostic single source of truth. - tokens/sure.tokens.json: every primitive, semantic alias, and Tailwind utility token in one W3C DTCG-flavored file. - tools/tokens/build.mjs: ~120 LOC plain Node script (zero deps) that resolves token references and emits Tailwind v4 source CSS. - app/assets/tailwind/sure-design-system/_generated.css: build output — the @theme block, dark-mode overrides, and 50 @utility blocks. - Hand-written CSS split into base.css (element resets), components.css (form-field/checkbox/tooltip/qrcode), and prose.css (prose dark overrides). The 5 maybe-design-system/*-utils.css files are removed — their contents now live inside _generated.css. - application.css gains `@source not "../../../tokens"` so Tailwind's content scanner ignores the JSON file (it would otherwise treat token keys like `bg-surface` as "used" classes and skip tree-shaking). - package.json: `npm run tokens:build` and `npm run tokens:check`. - .gitattributes: _generated.css marked linguist-generated. Functional parity verified: compiled `tailwind.css` has the same 378 CSS variables and byte-identical non-:root rules as before. The only diff is which of Tailwind's internal `:root,:host` blocks each variable lands in, which is invisible to the browser. * build(tokens): wire tokens build into bin/setup Run `npm install && npm run tokens:build` after bundle so a fresh checkout reaches a runnable state with one command. * docs(css): explain @source not exclusion of tokens dir Adds a comment so future readers know why tokens/ is excluded from Tailwind's content scanner (utility keys in the JSON would otherwise be treated as used classes and bypass tree-shaking). * docs(tokens): add tokens/README Schema overview, workflow, custom $extensions reference, and a list of the edge cases the build script handles. Lands as a follow-up commit on the same branch so reviewers landing on the diff have something to read before opening sure.tokens.json. * Update tokens/README.md Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> Signed-off-by: Guillem Arias Fauste * docs(tokens): swap em-dashes for colons in README * refactor(tokens): move tokens to design/, build script to bin/ Per PR review feedback (jjmata): - tokens/ → design/tokens/ — top-level design/ namespace leaves room for future design assets (Figma exports, design docs, etc.) without cluttering the repo root. - tools/tokens/build.mjs → bin/tokens.mjs — keeps all developer-facing scripts in one place (bin/) regardless of language. Path references updated in: - bin/tokens.mjs (TOKENS / OUT / generated header) - package.json (tokens:build, tokens:check) - app/assets/tailwind/application.css (@source not directive) - app/assets/tailwind/sure-design-system.css (comment) - app/assets/tailwind/sure-design-system/_generated.css (regenerated) - design/tokens/README.md (workflow examples) bin/tokens.mjs gains a +x bit. Tailwind compile verified. * docs(tokens): normalize README paths to repo-root style Files section was mixing relative-to-README paths (`../../bin/...`) with repo-root paths (`design/tokens/...`) used elsewhere in the same README. Switching everything to repo-root style for consistency. * fix(tokens): validate {ref} placeholders against the known token set CodeRabbit caught: resolveTemplate() and refToClass() would happily emit var(--foo-bar) or bg-foo-bar for any {foo.bar} input, so a typo in design/tokens/sure.tokens.json would silently ship broken CSS. Now build() pre-computes the set of valid token paths from the walker, and resolveTemplate() / refToClass() throw a clean "[tokens] Unknown token reference ..." error when a placeholder doesn't match. Top-level catch surfaces just the message and exits 1, no Node stack trace noise. Smoke-tested both directions: - Valid JSON: builds. - {color.gray.NONEXISTENT|5%}: fails with clear message, exit 1. * docs(tokens): humanize README prose * One more refenrece to `maybe-design-system` --------- Signed-off-by: Guillem Arias Fauste Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> Co-authored-by: Juan José Mata --- .cursor/rules/ui-ux-design-guidelines.mdc | 8 +- .gitattributes | 3 + .github/copilot-instructions.md | 4 +- .junie/guidelines.md | 8 +- CLAUDE.md | 4 +- app/assets/tailwind/application.css | 7 +- .../maybe-design-system/background-utils.css | 91 --- .../maybe-design-system/border-utils.css | 92 --- .../maybe-design-system/component-utils.css | 109 ---- .../maybe-design-system/foreground-utils.css | 63 -- .../maybe-design-system/text-utils.css | 39 -- app/assets/tailwind/sure-design-system.css | 12 + .../_generated.css} | 594 +++++++++++------- .../tailwind/sure-design-system/base.css | 33 + .../sure-design-system/components.css | 148 +++++ .../tailwind/sure-design-system/prose.css | 16 + bin/setup | 4 + bin/tokens.mjs | 172 +++++ design/tokens/README.md | 101 +++ design/tokens/sure.tokens.json | 356 +++++++++++ package.json | 4 +- 21 files changed, 1234 insertions(+), 634 deletions(-) delete mode 100644 app/assets/tailwind/maybe-design-system/background-utils.css delete mode 100644 app/assets/tailwind/maybe-design-system/border-utils.css delete mode 100644 app/assets/tailwind/maybe-design-system/component-utils.css delete mode 100644 app/assets/tailwind/maybe-design-system/foreground-utils.css delete mode 100644 app/assets/tailwind/maybe-design-system/text-utils.css create mode 100644 app/assets/tailwind/sure-design-system.css rename app/assets/tailwind/{maybe-design-system.css => sure-design-system/_generated.css} (54%) create mode 100644 app/assets/tailwind/sure-design-system/base.css create mode 100644 app/assets/tailwind/sure-design-system/components.css create mode 100644 app/assets/tailwind/sure-design-system/prose.css create mode 100755 bin/tokens.mjs create mode 100644 design/tokens/README.md create mode 100644 design/tokens/sure.tokens.json diff --git a/.cursor/rules/ui-ux-design-guidelines.mdc b/.cursor/rules/ui-ux-design-guidelines.mdc index 12b9a7ee3..125fc744a 100644 --- a/.cursor/rules/ui-ux-design-guidelines.mdc +++ b/.cursor/rules/ui-ux-design-guidelines.mdc @@ -11,13 +11,13 @@ Use the rules below when: ## Rules for AI (mandatory) -The codebase uses TailwindCSS v4.x (the newest version) with a custom design system defined in [maybe-design-system.css](mdc:app/assets/tailwind/maybe-design-system.css) +The codebase uses TailwindCSS v4.x (the newest version) with a custom design system defined in [sure-design-system.css](mdc:app/assets/tailwind/sure-design-system.css) -- Always start by referencing [maybe-design-system.css](mdc:app/assets/tailwind/maybe-design-system.css) to see the base primitives, functional tokens, and component tokens we use in the codebase -- Always prefer using the functional "tokens" defined in @maybe-design-system.css when possible. +- Always start by referencing [sure-design-system.css](mdc:app/assets/tailwind/sure-design-system.css) to see the base primitives, functional tokens, and component tokens we use in the codebase +- Always prefer using the functional "tokens" defined in @sure-design-system.css when possible. - Example 1: use `text-primary` rather than `text-white` - Example 2: use `bg-container` rather than `bg-white` - Example 3: use `border border-primary` rather than `border border-gray-200` -- Never create new styles in [maybe-design-system.css](mdc:app/assets/tailwind/maybe-design-system.css) or [application.css](mdc:app/assets/tailwind/application.css) without explicitly receiving permission to do so +- Never create new styles in [sure-design-system.css](mdc:app/assets/tailwind/sure-design-system.css) or [application.css](mdc:app/assets/tailwind/application.css) without explicitly receiving permission to do so - Always generate semantic HTML diff --git a/.gitattributes b/.gitattributes index 767c681d5..b387bb10a 100644 --- a/.gitattributes +++ b/.gitattributes @@ -3,6 +3,9 @@ # Mark the database schema as having been generated. db/schema.rb linguist-generated +# Mark generated design system CSS (built from tokens/sure.tokens.json). +app/assets/tailwind/sure-design-system/_generated.css linguist-generated + # Mark any vendored files as having been vendored. vendor/* linguist-vendored config/credentials/*.yml.enc diff=rails_credentials diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index 1c098fd39..87726005e 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -86,7 +86,7 @@ Sidekiq handles asynchronous tasks: - **Stimulus Controllers**: Handle interactivity, organized alongside components - **Charts**: D3.js for financial visualizations (time series, donut, sankey) - **Styling**: Tailwind CSS v4.x with custom design system - - Design system defined in `app/assets/tailwind/maybe-design-system.css` + - Design system defined in `app/assets/tailwind/sure-design-system.css` - Always use functional tokens (e.g., `text-primary` not `text-white`) - Prefer semantic HTML elements over JS components - Use `icon` helper for icons, never `lucide_icon` directly @@ -261,7 +261,7 @@ end ## TailwindCSS Design System ### Design System Rules -- **Always reference `app/assets/tailwind/maybe-design-system.css`** for primitives and tokens +- **Always reference `app/assets/tailwind/sure-design-system.css`** for primitives and tokens - **Use functional tokens** defined in design system: - `text-primary` instead of `text-white` - `bg-container` instead of `bg-white` diff --git a/.junie/guidelines.md b/.junie/guidelines.md index 88c9eb4e9..2097634b8 100644 --- a/.junie/guidelines.md +++ b/.junie/guidelines.md @@ -470,14 +470,14 @@ Use the rules below when: ## Rules for AI (mandatory) -The codebase uses TailwindCSS v4.x (the newest version) with a custom design system defined in [maybe-design-system.css](app/assets/tailwind/maybe-design-system.css) +The codebase uses TailwindCSS v4.x (the newest version) with a custom design system defined in [sure-design-system.css](app/assets/tailwind/sure-design-system.css) -- Always start by referencing [maybe-design-system.css](app/assets/tailwind/maybe-design-system.css) to see the base primitives, functional tokens, and component tokens we use in the codebase -- Always prefer using the functional "tokens" defined in @maybe-design-system.css when possible. +- Always start by referencing [sure-design-system.css](app/assets/tailwind/sure-design-system.css) to see the base primitives, functional tokens, and component tokens we use in the codebase +- Always prefer using the functional "tokens" defined in @sure-design-system.css when possible. - Example 1: use `text-primary` rather than `text-white` - Example 2: use `bg-container` rather than `bg-white` - Example 3: use `border border-primary` rather than `border border-gray-200` -- Never create new styles in [maybe-design-system.css](app/assets/tailwind/maybe-design-system.css) or [application.css](app/assets/tailwind/application.css) without explicitly receiving permission to do so +- Never create new styles in [sure-design-system.css](app/assets/tailwind/sure-design-system.css) or [application.css](app/assets/tailwind/application.css) without explicitly receiving permission to do so - Always generate semantic HTML ``` diff --git a/CLAUDE.md b/CLAUDE.md index ddd38afd0..4fc9fcf10 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -138,7 +138,7 @@ Sidekiq handles asynchronous tasks: - **Stimulus Controllers**: Handle interactivity, organized alongside components - **Charts**: D3.js for financial visualizations (time series, donut, sankey) - **Styling**: Tailwind CSS v4.x with custom design system - - Design system defined in `app/assets/tailwind/maybe-design-system.css` + - Design system defined in `app/assets/tailwind/sure-design-system.css` - Always use functional tokens (e.g., `text-primary` not `text-white`) - Prefer semantic HTML elements over JS components - Use `icon` helper for icons, never `lucide_icon` directly @@ -222,7 +222,7 @@ Sidekiq handles asynchronous tasks: ## TailwindCSS Design System ### Design System Rules -- **Always reference `app/assets/tailwind/maybe-design-system.css`** for primitives and tokens +- **Always reference `app/assets/tailwind/sure-design-system.css`** for primitives and tokens - **Use functional tokens** defined in design system: - `text-primary` instead of `text-white` - `bg-container` instead of `bg-white` diff --git a/app/assets/tailwind/application.css b/app/assets/tailwind/application.css index a46216b6b..ad728cdb3 100644 --- a/app/assets/tailwind/application.css +++ b/app/assets/tailwind/application.css @@ -1,6 +1,11 @@ @import 'tailwindcss'; -@import "./maybe-design-system.css"; +/* Exclude design/tokens/sure.tokens.json from Tailwind's content scanner: its utility + keys (e.g. `bg-surface`) would otherwise be treated as used classes and skip + tree-shaking. Path is relative to this file. */ +@source not "../../../design/tokens"; + +@import "./sure-design-system.css"; @import "./geist-font.css"; @import "./geist-mono-font.css"; diff --git a/app/assets/tailwind/maybe-design-system/background-utils.css b/app/assets/tailwind/maybe-design-system/background-utils.css deleted file mode 100644 index f11a7621a..000000000 --- a/app/assets/tailwind/maybe-design-system/background-utils.css +++ /dev/null @@ -1,91 +0,0 @@ -@utility bg-surface { - @apply bg-gray-50; - - @variant theme-dark { - @apply bg-black; - } -} - -@utility bg-surface-hover { - @apply bg-gray-100; - - @variant theme-dark { - @apply bg-gray-800; - } -} - -@utility bg-surface-inset { - @apply bg-gray-100; - - @variant theme-dark { - @apply bg-gray-800; - } -} - -@utility bg-surface-inset-hover { - @apply bg-gray-200; - - @variant theme-dark { - @apply bg-gray-800; - } -} - -@utility bg-container { - @apply bg-white; - - @variant theme-dark { - @apply bg-gray-900; - } -} - -@utility bg-container-hover { - @apply bg-gray-50; - - @variant theme-dark { - @apply bg-gray-800; - } -} - -@utility bg-container-inset { - @apply bg-gray-50; - - @variant theme-dark { - @apply bg-gray-800; - } -} - -@utility bg-container-inset-hover { - @apply bg-gray-100; - - @variant theme-dark { - @apply bg-gray-700; - } -} - -@utility bg-inverse { - @apply bg-gray-800; - - @variant theme-dark { - @apply bg-white; - } -} - -@utility bg-inverse-hover { - @apply bg-gray-700; - - @variant theme-dark { - @apply bg-gray-100; - } -} - -@utility bg-overlay { - background-color: --alpha(var(--color-gray-100) / 50%); - - @variant theme-dark { - background-color: var(--color-alpha-black-900); - } -} - -@utility bg-loader { - @apply bg-surface-inset animate-pulse; -} diff --git a/app/assets/tailwind/maybe-design-system/border-utils.css b/app/assets/tailwind/maybe-design-system/border-utils.css deleted file mode 100644 index 8fcc1c9cd..000000000 --- a/app/assets/tailwind/maybe-design-system/border-utils.css +++ /dev/null @@ -1,92 +0,0 @@ -/* Custom shadow borders used for surfaces / containers */ -@utility shadow-border-xs { - box-shadow: var(--shadow-xs), 0px 0px 0px 1px var(--color-alpha-black-50); - - @variant theme-dark { - box-shadow: var(--shadow-xs), 0px 0px 0px 1px var(--color-alpha-white-50); - } -} - -@utility shadow-border-sm { - box-shadow: var(--shadow-sm), 0px 0px 0px 1px var(--color-alpha-black-50); - - @variant theme-dark { - box-shadow: var(--shadow-sm), 0px 0px 0px 1px var(--color-alpha-white-50); - } -} - -@utility shadow-border-md { - box-shadow: var(--shadow-md), 0px 0px 0px 1px var(--color-alpha-black-50); - - @variant theme-dark { - box-shadow: var(--shadow-md), 0px 0px 0px 1px var(--color-alpha-white-50); - } -} - -@utility shadow-border-lg { - box-shadow: var(--shadow-lg), 0px 0px 0px 1px var(--color-alpha-black-50); - - @variant theme-dark { - box-shadow: var(--shadow-lg), 0px 0px 0px 1px var(--color-alpha-white-50); - } -} - -@utility shadow-border-xl { - box-shadow: var(--shadow-xl), 0px 0px 0px 1px var(--color-alpha-black-50); - - @variant theme-dark { - box-shadow: var(--shadow-xl), 0px 0px 0px 1px var(--color-alpha-white-50); - } -} - -@utility border-primary { - @apply border-alpha-black-300; - - @variant theme-dark { - @apply border-alpha-white-400; - } -} - -@utility border-secondary { - @apply border-alpha-black-200; - - @variant theme-dark { - @apply border-alpha-white-300; - } -} - -@utility border-tertiary { - @apply border-alpha-black-100; - - @variant theme-dark { - @apply border-alpha-white-200; - } -} - -@utility border-divider { - @apply border-tertiary; -} - -@utility border-subdued { - @apply border-alpha-black-50; - - @variant theme-dark { - @apply border-alpha-white-100; - } -} - -@utility border-solid { - @apply border-black; - - @variant theme-dark { - @apply border-white; - } -} - -@utility border-destructive { - @apply border-red-500; - - @variant theme-dark { - @apply border-red-400; - } -} diff --git a/app/assets/tailwind/maybe-design-system/component-utils.css b/app/assets/tailwind/maybe-design-system/component-utils.css deleted file mode 100644 index 597b50921..000000000 --- a/app/assets/tailwind/maybe-design-system/component-utils.css +++ /dev/null @@ -1,109 +0,0 @@ -/* Button Backgrounds */ -@utility button-bg-primary { - @apply bg-gray-900; - /* Maps to fg-primary light */ - - @variant theme-dark { - @apply bg-white; - /* Maps to fg-primary dark */ - } -} - -@utility button-bg-primary-hover { - @apply bg-gray-800; - /* Maps to fg-primary-variant light */ - - @variant theme-dark { - @apply bg-gray-50; - /* Maps to fg-primary-variant dark */ - } -} - -@utility button-bg-secondary { - @apply bg-gray-50; /* Maps to fg-secondary light */ - - @variant theme-dark { - @apply bg-gray-700; /* Maps to fg-secondary dark */ - } -} - -@utility button-bg-secondary-hover { - @apply bg-gray-100; /* Maps to fg-secondary-variant light */ - - @variant theme-dark { - @apply bg-gray-600; /* Maps to fg-secondary-variant dark */ - } -} - -@utility button-bg-disabled { - @apply bg-gray-50; - - @variant theme-dark { - @apply bg-gray-700; - } -} - -@utility button-bg-destructive { - @apply bg-red-500; - - @variant theme-dark { - @apply bg-red-400; - } -} - -@utility button-bg-destructive-hover { - @apply bg-red-600; - - @variant theme-dark { - @apply bg-red-500; - } -} - -@utility button-bg-ghost-hover { - @apply bg-gray-50; - - @variant theme-dark { - @apply bg-gray-800 fg-inverse; - } -} - -@utility button-bg-outline-hover { - @apply bg-gray-100; - - @variant theme-dark { - @apply bg-gray-700; - } -} - -/* Tab Styles */ -@utility tab-item-active { - @apply bg-white; - - @variant theme-dark { - @apply bg-gray-700; - } -} - -@utility tab-item-hover { - @apply bg-gray-200; - - @variant theme-dark { - @apply bg-gray-800; - } -} - -@utility tab-bg-group { - @apply bg-gray-50; - - @variant theme-dark { - @apply bg-alpha-black-700; - } -} - -@utility bg-nav-indicator { - @apply bg-black; - - @variant theme-dark { - @apply bg-white; - } -} \ No newline at end of file diff --git a/app/assets/tailwind/maybe-design-system/foreground-utils.css b/app/assets/tailwind/maybe-design-system/foreground-utils.css deleted file mode 100644 index 5697d2e9a..000000000 --- a/app/assets/tailwind/maybe-design-system/foreground-utils.css +++ /dev/null @@ -1,63 +0,0 @@ -@utility fg-gray { - @apply text-gray-500; - - @variant theme-dark { - @apply text-gray-400; - } -} - -@utility fg-contrast { - @apply text-gray-400; - - @variant theme-dark { - @apply text-gray-500; - } -} - -@utility fg-inverse { - @apply text-white; - - @variant theme-dark { - @apply text-gray-900; - } -} - -@utility fg-primary { - @apply text-gray-900; - - @variant theme-dark { - @apply text-white; - } -} - -@utility fg-primary-variant { - @apply text-gray-800; - - @variant theme-dark { - @apply text-gray-50; - } -} - -@utility fg-secondary { - @apply text-gray-50; - - @variant theme-dark { - @apply text-gray-400; - } -} - -@utility fg-secondary-variant { - @apply text-gray-100; - - @variant theme-dark { - @apply text-gray-500; - } -} - -@utility fg-subdued { - @apply text-gray-400; - - @variant theme-dark { - @apply text-gray-500; - } -} \ No newline at end of file diff --git a/app/assets/tailwind/maybe-design-system/text-utils.css b/app/assets/tailwind/maybe-design-system/text-utils.css deleted file mode 100644 index 7aa8f2e67..000000000 --- a/app/assets/tailwind/maybe-design-system/text-utils.css +++ /dev/null @@ -1,39 +0,0 @@ -@utility text-primary { - @apply text-gray-900; - - @variant theme-dark { - @apply text-white; - } -} - -@utility text-inverse { - @apply text-white; - - @variant theme-dark { - @apply text-gray-900; - } -} - -@utility text-secondary { - @apply text-gray-500; - - @variant theme-dark { - @apply text-gray-300; - } -} - -@utility text-subdued { - @apply text-gray-400; - - @variant theme-dark { - @apply text-gray-500; - } -} - -@utility text-link { - @apply text-blue-600; - - @variant theme-dark { - @apply text-blue-500; - } -} \ No newline at end of file diff --git a/app/assets/tailwind/sure-design-system.css b/app/assets/tailwind/sure-design-system.css new file mode 100644 index 000000000..53ad19e26 --- /dev/null +++ b/app/assets/tailwind/sure-design-system.css @@ -0,0 +1,12 @@ +/* + * Sure design system entry. + * Tokens (theme, dark overrides, utilities) are generated from design/tokens/sure.tokens.json: see _generated.css. + * Element resets, components, and prose overrides remain hand-written. + */ + +@custom-variant theme-dark (&:where([data-theme=dark], [data-theme=dark] *)); + +@import './sure-design-system/_generated.css'; +@import './sure-design-system/base.css'; +@import './sure-design-system/components.css'; +@import './sure-design-system/prose.css'; diff --git a/app/assets/tailwind/maybe-design-system.css b/app/assets/tailwind/sure-design-system/_generated.css similarity index 54% rename from app/assets/tailwind/maybe-design-system.css rename to app/assets/tailwind/sure-design-system/_generated.css index e4ff87051..31d8c67a0 100644 --- a/app/assets/tailwind/maybe-design-system.css +++ b/app/assets/tailwind/sure-design-system/_generated.css @@ -1,37 +1,18 @@ -/* - This file contains all of the Figma design tokens, components, etc. that - are used globally across the app. - - One-off styling (3rd party overrides, etc.) should be done in the application.css file. -*/ - -@import './maybe-design-system/background-utils.css'; -@import './maybe-design-system/foreground-utils.css'; -@import './maybe-design-system/text-utils.css'; -@import './maybe-design-system/border-utils.css'; -@import './maybe-design-system/component-utils.css'; - -@custom-variant theme-dark (&:where([data-theme=dark], [data-theme=dark] *)); +/* + * GENERATED — do not edit by hand. + * Source: design/tokens/sure.tokens.json + * Build: npm run tokens:build + */ @theme { - /* Font families */ --font-sans: 'Geist', system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif; --font-mono: 'Geist Mono', ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace; - - /* Base colors */ --color-white: #ffffff; --color-black: #0B0B0B; --color-success: var(--color-green-600); --color-warning: var(--color-yellow-600); --color-destructive: var(--color-red-600); --color-shadow: --alpha(var(--color-black) / 6%); - - /* Colors used in Stimulus controllers with SVGs (easier to define light/dark mode here than toggle within the controllers) */ - /* See @layer base block below for dark mode overrides */ - --budget-unused-fill: var(--color-gray-200); - --budget-unallocated-fill: var(--color-gray-50); - - /* Gray scale */ --color-gray-25: #FAFAFA; --color-gray-50: #F7F7F7; --color-gray-100: #F0F0F0; @@ -46,8 +27,6 @@ --color-gray: var(--color-gray-500); --color-gray-tint-5: --alpha(var(--color-gray-500) / 5%); --color-gray-tint-10: --alpha(var(--color-gray-500) / 10%); - - /* Alpha colors */ --color-alpha-white-25: --alpha(var(--color-white) / 3%); --color-alpha-white-50: --alpha(var(--color-white) / 5%); --color-alpha-white-100: --alpha(var(--color-white) / 8%); @@ -59,7 +38,6 @@ --color-alpha-white-700: --alpha(var(--color-white) / 50%); --color-alpha-white-800: --alpha(var(--color-white) / 70%); --color-alpha-white-900: --alpha(var(--color-white) / 85%); - --color-alpha-black-25: --alpha(var(--color-black) / 3%); --color-alpha-black-50: --alpha(var(--color-black) / 5%); --color-alpha-black-100: --alpha(var(--color-black) / 8%); @@ -71,8 +49,6 @@ --color-alpha-black-700: --alpha(var(--color-black) / 50%); --color-alpha-black-800: --alpha(var(--color-black) / 70%); --color-alpha-black-900: --alpha(var(--color-black) / 85%); - - /* Red scale */ --color-red-25: #FFFBFB; --color-red-50: #FFF1F0; --color-red-100: #FFDEDB; @@ -86,8 +62,6 @@ --color-red-900: #7E0707; --color-red-tint-5: --alpha(var(--color-red-500) / 5%); --color-red-tint-10: --alpha(var(--color-red-500) / 10%); - - /* Green scale */ --color-green-25: #F6FEF9; --color-green-50: #ECFDF3; --color-green-100: #D1FADF; @@ -101,8 +75,6 @@ --color-green-900: #054F31; --color-green-tint-5: --alpha(var(--color-green-500) / 5%); --color-green-tint-10: --alpha(var(--color-green-500) / 10%); - - /* Yellow scale */ --color-yellow-25: #FFFCF5; --color-yellow-50: #FFFAEB; --color-yellow-100: #FEF0C7; @@ -116,8 +88,6 @@ --color-yellow-900: #7A2E0E; --color-yellow-tint-5: --alpha(var(--color-yellow-500) / 5%); --color-yellow-tint-10: --alpha(var(--color-yellow-500) / 10%); - - /* Cyan scale */ --color-cyan-25: #F5FEFF; --color-cyan-50: #ECFDFF; --color-cyan-100: #CFF9FE; @@ -131,8 +101,6 @@ --color-cyan-900: #155B75; --color-cyan-tint-5: --alpha(var(--color-cyan-500) / 5%); --color-cyan-tint-10: --alpha(var(--color-cyan-500) / 10%); - - /* Blue scale */ --color-blue-25: #F5FAFF; --color-blue-50: #EFF8FF; --color-blue-100: #D1E9FF; @@ -146,8 +114,6 @@ --color-blue-900: #194185; --color-blue-tint-5: --alpha(var(--color-blue-500) / 5%); --color-blue-tint-10: --alpha(var(--color-blue-500) / 10%); - - /* Indigo scale */ --color-indigo-25: #F5F8FF; --color-indigo-50: #EFF4FF; --color-indigo-100: #E0EAFF; @@ -161,8 +127,6 @@ --color-indigo-900: #2D3282; --color-indigo-tint-5: --alpha(var(--color-indigo-500) / 5%); --color-indigo-tint-10: --alpha(var(--color-indigo-500) / 10%); - - /* Violet scale */ --color-violet-25: #FBFAFF; --color-violet-50: #F5F3FF; --color-violet-100: #ECE9FE; @@ -174,8 +138,6 @@ --color-violet-700: #6927DA; --color-violet-tint-5: --alpha(var(--color-violet-500) / 5%); --color-violet-tint-10: --alpha(var(--color-violet-500) / 10%); - - /* Fuchsia scale */ --color-fuchsia-25: #FEFAFF; --color-fuchsia-50: #FDF4FF; --color-fuchsia-100: #FBE8FF; @@ -189,8 +151,6 @@ --color-fuchsia-900: #6F1877; --color-fuchsia-tint-5: --alpha(var(--color-fuchsia-500) / 5%); --color-fuchsia-tint-10: --alpha(var(--color-fuchsia-500) / 10%); - - /* Pink scale */ --color-pink-25: #FFFAFC; --color-pink-50: #FEF0F7; --color-pink-100: #FFD1E2; @@ -204,8 +164,6 @@ --color-pink-900: #840B45; --color-pink-tint-5: --alpha(var(--color-pink-500) / 5%); --color-pink-tint-10: --alpha(var(--color-pink-500) / 10%); - - /* Orange scale */ --color-orange-25: #FFF9F5; --color-orange-50: #FFF4ED; --color-orange-100: #FFE6D5; @@ -219,243 +177,427 @@ --color-orange-900: #771A0D; --color-orange-tint-5: --alpha(var(--color-orange-500) / 5%); --color-orange-tint-10: --alpha(var(--color-orange-500) / 10%); - - /* Border radius overrides */ + --budget-unused-fill: var(--color-gray-200); + --budget-unallocated-fill: var(--color-gray-50); --border-radius-md: 8px; --border-radius-lg: 10px; - --shadow-xs: 0px 1px 2px 0px --alpha(var(--color-black) / 6%); --shadow-sm: 0px 1px 6px 0px --alpha(var(--color-black) / 6%); --shadow-md: 0px 4px 8px -2px --alpha(var(--color-black) / 6%); --shadow-lg: 0px 12px 16px -4px --alpha(var(--color-black) / 6%); --shadow-xl: 0px 20px 24px -4px --alpha(var(--color-black) / 6%); - --animate-stroke-fill: stroke-fill 3s 300ms forwards; @keyframes stroke-fill { - 0% { - stroke-dashoffset: 43.9822971503; - } - - 100% { - stroke-dashoffset: 0; - } - } -} - -/* Specific override for strong tags in prose under dark mode */ -.prose:where([data-theme=dark], [data-theme=dark] *) strong { - color: theme(colors.white) !important; -} - -/* Specific override for headings in prose under dark mode */ -.prose:where([data-theme=dark], [data-theme=dark] *) h1, -.prose:where([data-theme=dark], [data-theme=dark] *) h2, -.prose:where([data-theme=dark], [data-theme=dark] *) h3, -.prose:where([data-theme=dark], [data-theme=dark] *) h4, -.prose:where([data-theme=dark], [data-theme=dark] *) h5, -.prose:where([data-theme=dark], [data-theme=dark] *) h6, -.prose:where([data-theme=dark], [data-theme=dark] *) blockquote, -.prose:where([data-theme=dark], [data-theme=dark] *) thead th { - color: theme(colors.white) !important; + 0% { stroke-dashoffset: 43.9822971503; } + 100% { stroke-dashoffset: 0; } + } } @layer base { - [data-theme="dark"] { + [data-theme="dark"] { --color-success: var(--color-green-500); --color-warning: var(--color-yellow-400); --color-destructive: var(--color-red-400); --color-shadow: --alpha(var(--color-white) / 8%); - - /* Dark mode overrides for colors used in Stimulus controllers with SVGs */ --budget-unused-fill: var(--color-gray-500); --budget-unallocated-fill: var(--color-gray-700); - --shadow-xs: 0px 1px 2px 0px --alpha(var(--color-white) / 8%); --shadow-sm: 0px 1px 6px 0px --alpha(var(--color-white) / 8%); --shadow-md: 0px 4px 8px -2px --alpha(var(--color-white) / 8%); --shadow-lg: 0px 12px 16px -4px --alpha(var(--color-white) / 8%); --shadow-xl: 0px 20px 24px -4px --alpha(var(--color-white) / 8%); } +} - button { - @apply cursor-pointer focus-visible:outline-gray-900; - } +@utility bg-surface { + @apply bg-gray-50; - hr { - @apply text-gray-200; - } - - /* We control the sizing through DialogComponent, so reset this value */ - dialog:modal { - max-width: 100dvw; - max-height: 100dvh; - } - - details>summary::-webkit-details-marker { - @apply hidden; - } - - details>summary { - @apply list-none; - } - - input[type='radio'] { - @apply border-gray-300 text-indigo-600 focus:ring-indigo-600; - /* Default light mode */ - - @variant theme-dark { - /* Dark mode radio button base and checked styles */ - @apply border-gray-600 bg-gray-700 checked:bg-blue-500 focus:ring-blue-500 focus:ring-offset-gray-800; - } + @variant theme-dark { + @apply bg-black; } } -@layer components { - /* Forms */ - .form-field { - @apply flex flex-col gap-1 relative px-3 py-2 rounded-md border bg-container border-secondary shadow-xs w-full; - @apply focus-within:border-secondary focus-within:shadow-none focus-within:ring-4 focus-within:ring-alpha-black-200; - @apply transition-all duration-300; +@utility bg-surface-hover { + @apply bg-gray-100; - @variant theme-dark { - @apply focus-within:ring-alpha-white-300; - } - - /* Add styles for multiple select within form fields */ - select[multiple] { - @apply py-2 pr-2 space-y-0.5 overflow-y-auto; - - option { - @apply py-2 rounded-md; - } - - option:checked { - @apply after:content-['\2713'] bg-container-inset after:text-gray-500 after:ml-2; - } - - option:active, - option:focus { - @apply bg-container-inset; - } - } + @variant theme-dark { + @apply bg-gray-800; } +} - /* New form field structure components */ - .form-field__header { - @apply flex items-center justify-between gap-2; +@utility bg-surface-inset { + @apply bg-gray-100; + + @variant theme-dark { + @apply bg-gray-800; } +} - .form-field__body { - @apply flex flex-col gap-1; +@utility bg-surface-inset-hover { + @apply bg-gray-200; + + @variant theme-dark { + @apply bg-gray-800; } +} - .form-field__actions { - @apply flex items-center gap-1; +@utility bg-container { + @apply bg-white; + + @variant theme-dark { + @apply bg-gray-900; } +} - .form-field__label { - @apply block text-xs text-secondary peer-disabled:text-subdued; +@utility bg-container-hover { + @apply bg-gray-50; + + @variant theme-dark { + @apply bg-gray-800; } +} - .form-field__input { - @apply text-primary border-none bg-container text-sm opacity-100 w-full p-0; - @apply focus:opacity-100 focus:outline-hidden focus:ring-0; - @apply placeholder-shown:opacity-50; - @apply disabled:text-subdued; - @apply text-ellipsis overflow-hidden whitespace-nowrap; - @apply transition-opacity duration-300; - @apply placeholder:text-subdued; +@utility bg-container-inset { + @apply bg-gray-50; - @variant theme-dark { - &::-webkit-calendar-picker-indicator { - filter: invert(1); - cursor: pointer; - } - } + @variant theme-dark { + @apply bg-gray-800; } +} - textarea.form-field__input { - @apply whitespace-normal overflow-auto; - text-overflow: clip; +@utility bg-container-inset-hover { + @apply bg-gray-100; + + @variant theme-dark { + @apply bg-gray-700; } - - select.form-field__input, - button.form-field__input { - @apply pr-10 appearance-none; - background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 20 20'%3e%3cpath stroke='%236b7280' stroke-linecap='round' stroke-linejoin='round' stroke-width='1.5' d='M6 8l4 4 4-4'/%3e%3c/svg%3e"); - background-position: right -0.15rem center; - background-repeat: no-repeat; - background-size: 1.25rem 1.25rem; - text-align: left; +} + +@utility bg-inverse { + @apply bg-gray-800; + + @variant theme-dark { + @apply bg-white; } +} - .form-field__radio { - @apply text-primary; +@utility bg-inverse-hover { + @apply bg-gray-700; + + @variant theme-dark { + @apply bg-gray-100; } +} - .form-field__submit { - @apply cursor-pointer rounded-lg bg-surface p-3 text-center text-white hover:bg-surface-hover; +@utility bg-overlay { + background-color: --alpha(var(--color-gray-100) / 50%); + + @variant theme-dark { + background-color: var(--color-alpha-black-900); } +} - /* Checkboxes */ - .checkbox { - &[type='checkbox'] { - @apply rounded-sm; - @apply transition-colors duration-300; - } +@utility bg-loader { + @apply bg-surface-inset animate-pulse; +} + +@utility fg-gray { + @apply text-gray-500; + + @variant theme-dark { + @apply text-gray-400; } +} - .checkbox--light { - &[type='checkbox'] { - @apply border-alpha-black-200 checked:bg-gray-900 checked:ring-gray-900 focus:ring-gray-900 focus-visible:ring-gray-900 checked:hover:bg-gray-300 hover:bg-gray-300; - } +@utility fg-contrast { + @apply text-gray-400; - &[type='checkbox']:disabled { - @apply cursor-not-allowed opacity-80 bg-gray-50 border-gray-200 checked:bg-gray-400 checked:ring-gray-400; - } - - @variant theme-dark { - &[type='checkbox'] { - @apply ring-gray-900 checked:text-white; - background-color: var(--color-gray-100); - } - - &[type='checkbox']:disabled { - @apply cursor-not-allowed opacity-80; - background-color: var(--color-gray-600); - } - - &[type='checkbox']:checked { - background-image: url("data:image/svg+xml,%3csvg viewBox='0 0 16 16' fill='%23808080' xmlns='http://www.w3.org/2000/svg'%3e%3cpath d='M12.207 4.793a1 1 0 010 1.414l-5 5a1 1 0 01-1.414 0l-2-2a1 1 0 011.414-1.414L6.5 9.086l4.293-4.293a1 1 0 011.414 0z'/%3e%3c/svg%3e"); - background-color: var(--color-gray-100); - } - } + @variant theme-dark { + @apply text-gray-500; } +} - .checkbox--dark { - &[type='checkbox'] { - @apply ring-gray-900 checked:text-white; - } +@utility fg-inverse { + @apply text-white; - &[type='checkbox']:disabled { - @apply cursor-not-allowed opacity-80 ring-gray-600; - } - - &[type='checkbox']:checked { - background-image: url("data:image/svg+xml,%3csvg viewBox='0 0 16 16' fill='%23111827' xmlns='http://www.w3.org/2000/svg'%3e%3cpath d='M12.207 4.793a1 1 0 010 1.414l-5 5a1 1 0 01-1.414 0l-2-2a1 1 0 011.414-1.414L6.5 9.086l4.293-4.293a1 1 0 011.414 0z'/%3e%3c/svg%3e"); - } + @variant theme-dark { + @apply text-gray-900; } +} - /* Tooltips */ - .tooltip { - @apply hidden absolute; - } +@utility fg-primary { + @apply text-gray-900; - .qrcode svg path { - fill: var(--color-black); - @variant theme-dark { - fill: var(--color-white); - } + @variant theme-dark { + @apply text-white; } -} \ No newline at end of file +} + +@utility fg-primary-variant { + @apply text-gray-800; + + @variant theme-dark { + @apply text-gray-50; + } +} + +@utility fg-secondary { + @apply text-gray-50; + + @variant theme-dark { + @apply text-gray-400; + } +} + +@utility fg-secondary-variant { + @apply text-gray-100; + + @variant theme-dark { + @apply text-gray-500; + } +} + +@utility fg-subdued { + @apply text-gray-400; + + @variant theme-dark { + @apply text-gray-500; + } +} + +@utility text-primary { + @apply text-gray-900; + + @variant theme-dark { + @apply text-white; + } +} + +@utility text-inverse { + @apply text-white; + + @variant theme-dark { + @apply text-gray-900; + } +} + +@utility text-secondary { + @apply text-gray-500; + + @variant theme-dark { + @apply text-gray-300; + } +} + +@utility text-subdued { + @apply text-gray-400; + + @variant theme-dark { + @apply text-gray-500; + } +} + +@utility text-link { + @apply text-blue-600; + + @variant theme-dark { + @apply text-blue-500; + } +} + +@utility shadow-border-xs { + box-shadow: var(--shadow-xs), 0px 0px 0px 1px var(--color-alpha-black-50); + + @variant theme-dark { + box-shadow: var(--shadow-xs), 0px 0px 0px 1px var(--color-alpha-white-50); + } +} + +@utility shadow-border-sm { + box-shadow: var(--shadow-sm), 0px 0px 0px 1px var(--color-alpha-black-50); + + @variant theme-dark { + box-shadow: var(--shadow-sm), 0px 0px 0px 1px var(--color-alpha-white-50); + } +} + +@utility shadow-border-md { + box-shadow: var(--shadow-md), 0px 0px 0px 1px var(--color-alpha-black-50); + + @variant theme-dark { + box-shadow: var(--shadow-md), 0px 0px 0px 1px var(--color-alpha-white-50); + } +} + +@utility shadow-border-lg { + box-shadow: var(--shadow-lg), 0px 0px 0px 1px var(--color-alpha-black-50); + + @variant theme-dark { + box-shadow: var(--shadow-lg), 0px 0px 0px 1px var(--color-alpha-white-50); + } +} + +@utility shadow-border-xl { + box-shadow: var(--shadow-xl), 0px 0px 0px 1px var(--color-alpha-black-50); + + @variant theme-dark { + box-shadow: var(--shadow-xl), 0px 0px 0px 1px var(--color-alpha-white-50); + } +} + +@utility border-primary { + @apply border-alpha-black-300; + + @variant theme-dark { + @apply border-alpha-white-400; + } +} + +@utility border-secondary { + @apply border-alpha-black-200; + + @variant theme-dark { + @apply border-alpha-white-300; + } +} + +@utility border-tertiary { + @apply border-alpha-black-100; + + @variant theme-dark { + @apply border-alpha-white-200; + } +} + +@utility border-divider { + @apply border-tertiary; +} + +@utility border-subdued { + @apply border-alpha-black-50; + + @variant theme-dark { + @apply border-alpha-white-100; + } +} + +@utility border-solid { + @apply border-black; + + @variant theme-dark { + @apply border-white; + } +} + +@utility border-destructive { + @apply border-red-500; + + @variant theme-dark { + @apply border-red-400; + } +} + +@utility button-bg-primary { + @apply bg-gray-900; + + @variant theme-dark { + @apply bg-white; + } +} + +@utility button-bg-primary-hover { + @apply bg-gray-800; + + @variant theme-dark { + @apply bg-gray-50; + } +} + +@utility button-bg-secondary { + @apply bg-gray-50; + + @variant theme-dark { + @apply bg-gray-700; + } +} + +@utility button-bg-secondary-hover { + @apply bg-gray-100; + + @variant theme-dark { + @apply bg-gray-600; + } +} + +@utility button-bg-disabled { + @apply bg-gray-50; + + @variant theme-dark { + @apply bg-gray-700; + } +} + +@utility button-bg-destructive { + @apply bg-red-500; + + @variant theme-dark { + @apply bg-red-400; + } +} + +@utility button-bg-destructive-hover { + @apply bg-red-600; + + @variant theme-dark { + @apply bg-red-500; + } +} + +@utility button-bg-ghost-hover { + @apply bg-gray-50; + + @variant theme-dark { + @apply bg-gray-800 fg-inverse; + } +} + +@utility button-bg-outline-hover { + @apply bg-gray-100; + + @variant theme-dark { + @apply bg-gray-700; + } +} + +@utility tab-item-active { + @apply bg-white; + + @variant theme-dark { + @apply bg-gray-700; + } +} + +@utility tab-item-hover { + @apply bg-gray-200; + + @variant theme-dark { + @apply bg-gray-800; + } +} + +@utility tab-bg-group { + @apply bg-gray-50; + + @variant theme-dark { + @apply bg-alpha-black-700; + } +} + +@utility bg-nav-indicator { + @apply bg-black; + + @variant theme-dark { + @apply bg-white; + } +} diff --git a/app/assets/tailwind/sure-design-system/base.css b/app/assets/tailwind/sure-design-system/base.css new file mode 100644 index 000000000..991cfc4ef --- /dev/null +++ b/app/assets/tailwind/sure-design-system/base.css @@ -0,0 +1,33 @@ +@layer base { + button { + @apply cursor-pointer focus-visible:outline-gray-900; + } + + hr { + @apply text-gray-200; + } + + /* We control the sizing through DialogComponent, so reset this value */ + dialog:modal { + max-width: 100dvw; + max-height: 100dvh; + } + + details>summary::-webkit-details-marker { + @apply hidden; + } + + details>summary { + @apply list-none; + } + + input[type='radio'] { + @apply border-gray-300 text-indigo-600 focus:ring-indigo-600; + /* Default light mode */ + + @variant theme-dark { + /* Dark mode radio button base and checked styles */ + @apply border-gray-600 bg-gray-700 checked:bg-blue-500 focus:ring-blue-500 focus:ring-offset-gray-800; + } + } +} diff --git a/app/assets/tailwind/sure-design-system/components.css b/app/assets/tailwind/sure-design-system/components.css new file mode 100644 index 000000000..8a4c08b98 --- /dev/null +++ b/app/assets/tailwind/sure-design-system/components.css @@ -0,0 +1,148 @@ +@layer components { + /* Forms */ + .form-field { + @apply flex flex-col gap-1 relative px-3 py-2 rounded-md border bg-container border-secondary shadow-xs w-full; + @apply focus-within:border-secondary focus-within:shadow-none focus-within:ring-4 focus-within:ring-alpha-black-200; + @apply transition-all duration-300; + + @variant theme-dark { + @apply focus-within:ring-alpha-white-300; + } + + /* Add styles for multiple select within form fields */ + select[multiple] { + @apply py-2 pr-2 space-y-0.5 overflow-y-auto; + + option { + @apply py-2 rounded-md; + } + + option:checked { + @apply after:content-['\2713'] bg-container-inset after:text-gray-500 after:ml-2; + } + + option:active, + option:focus { + @apply bg-container-inset; + } + } + } + + /* New form field structure components */ + .form-field__header { + @apply flex items-center justify-between gap-2; + } + + .form-field__body { + @apply flex flex-col gap-1; + } + + .form-field__actions { + @apply flex items-center gap-1; + } + + .form-field__label { + @apply block text-xs text-secondary peer-disabled:text-subdued; + } + + .form-field__input { + @apply text-primary border-none bg-container text-sm opacity-100 w-full p-0; + @apply focus:opacity-100 focus:outline-hidden focus:ring-0; + @apply placeholder-shown:opacity-50; + @apply disabled:text-subdued; + @apply text-ellipsis overflow-hidden whitespace-nowrap; + @apply transition-opacity duration-300; + @apply placeholder:text-subdued; + + @variant theme-dark { + &::-webkit-calendar-picker-indicator { + filter: invert(1); + cursor: pointer; + } + } + } + + textarea.form-field__input { + @apply whitespace-normal overflow-auto; + text-overflow: clip; + } + + select.form-field__input, + button.form-field__input { + @apply pr-10 appearance-none; + background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 20 20'%3e%3cpath stroke='%236b7280' stroke-linecap='round' stroke-linejoin='round' stroke-width='1.5' d='M6 8l4 4 4-4'/%3e%3c/svg%3e"); + background-position: right -0.15rem center; + background-repeat: no-repeat; + background-size: 1.25rem 1.25rem; + text-align: left; + } + + .form-field__radio { + @apply text-primary; + } + + .form-field__submit { + @apply cursor-pointer rounded-lg bg-surface p-3 text-center text-white hover:bg-surface-hover; + } + + /* Checkboxes */ + .checkbox { + &[type='checkbox'] { + @apply rounded-sm; + @apply transition-colors duration-300; + } + } + + .checkbox--light { + &[type='checkbox'] { + @apply border-alpha-black-200 checked:bg-gray-900 checked:ring-gray-900 focus:ring-gray-900 focus-visible:ring-gray-900 checked:hover:bg-gray-300 hover:bg-gray-300; + } + + &[type='checkbox']:disabled { + @apply cursor-not-allowed opacity-80 bg-gray-50 border-gray-200 checked:bg-gray-400 checked:ring-gray-400; + } + + @variant theme-dark { + &[type='checkbox'] { + @apply ring-gray-900 checked:text-white; + background-color: var(--color-gray-100); + } + + &[type='checkbox']:disabled { + @apply cursor-not-allowed opacity-80; + background-color: var(--color-gray-600); + } + + &[type='checkbox']:checked { + background-image: url("data:image/svg+xml,%3csvg viewBox='0 0 16 16' fill='%23808080' xmlns='http://www.w3.org/2000/svg'%3e%3cpath d='M12.207 4.793a1 1 0 010 1.414l-5 5a1 1 0 01-1.414 0l-2-2a1 1 0 011.414-1.414L6.5 9.086l4.293-4.293a1 1 0 011.414 0z'/%3e%3c/svg%3e"); + background-color: var(--color-gray-100); + } + } + } + + .checkbox--dark { + &[type='checkbox'] { + @apply ring-gray-900 checked:text-white; + } + + &[type='checkbox']:disabled { + @apply cursor-not-allowed opacity-80 ring-gray-600; + } + + &[type='checkbox']:checked { + background-image: url("data:image/svg+xml,%3csvg viewBox='0 0 16 16' fill='%23111827' xmlns='http://www.w3.org/2000/svg'%3e%3cpath d='M12.207 4.793a1 1 0 010 1.414l-5 5a1 1 0 01-1.414 0l-2-2a1 1 0 011.414-1.414L6.5 9.086l4.293-4.293a1 1 0 011.414 0z'/%3e%3c/svg%3e"); + } + } + + /* Tooltips */ + .tooltip { + @apply hidden absolute; + } + + .qrcode svg path { + fill: var(--color-black); + @variant theme-dark { + fill: var(--color-white); + } + } +} diff --git a/app/assets/tailwind/sure-design-system/prose.css b/app/assets/tailwind/sure-design-system/prose.css new file mode 100644 index 000000000..85b7892c0 --- /dev/null +++ b/app/assets/tailwind/sure-design-system/prose.css @@ -0,0 +1,16 @@ +/* Specific override for strong tags in prose under dark mode */ +.prose:where([data-theme=dark], [data-theme=dark] *) strong { + color: theme(colors.white) !important; +} + +/* Specific override for headings in prose under dark mode */ +.prose:where([data-theme=dark], [data-theme=dark] *) h1, +.prose:where([data-theme=dark], [data-theme=dark] *) h2, +.prose:where([data-theme=dark], [data-theme=dark] *) h3, +.prose:where([data-theme=dark], [data-theme=dark] *) h4, +.prose:where([data-theme=dark], [data-theme=dark] *) h5, +.prose:where([data-theme=dark], [data-theme=dark] *) h6, +.prose:where([data-theme=dark], [data-theme=dark] *) blockquote, +.prose:where([data-theme=dark], [data-theme=dark] *) thead th { + color: theme(colors.white) !important; +} diff --git a/bin/setup b/bin/setup index 3cd5a9d78..83098e248 100755 --- a/bin/setup +++ b/bin/setup @@ -17,6 +17,10 @@ FileUtils.chdir APP_ROOT do system! "gem install bundler --conservative" system("bundle check") || system!("bundle install") + puts "\n== Building design tokens ==" + system! "npm install" + system! "npm run tokens:build" + # puts "\n== Copying sample files ==" # unless File.exist?("config/database.yml") # FileUtils.cp "config/database.yml.sample", "config/database.yml" diff --git a/bin/tokens.mjs b/bin/tokens.mjs new file mode 100755 index 000000000..7fe2f8377 --- /dev/null +++ b/bin/tokens.mjs @@ -0,0 +1,172 @@ +#!/usr/bin/env node +// Sure design tokens build. +// Reads design/tokens/sure.tokens.json (W3C DTCG-flavored), emits one Tailwind v4 CSS file. + +import { readFileSync, writeFileSync } from "node:fs"; +import { resolve, dirname } from "node:path"; +import { fileURLToPath } from "node:url"; + +const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), ".."); +const TOKENS = resolve(ROOT, "design/tokens/sure.tokens.json"); +const OUT = resolve(ROOT, "app/assets/tailwind/sure-design-system/_generated.css"); + +const HEADER = `/* + * GENERATED — do not edit by hand. + * Source: design/tokens/sure.tokens.json + * Build: npm run tokens:build + */ +`; + +// Single inline keyframe; not worth its own JSON token. +const KEYFRAMES = ` @keyframes stroke-fill { + 0% { stroke-dashoffset: 43.9822971503; } + 100% { stroke-dashoffset: 0; } + }`; + +// Yield [path, node] for every token leaf (object with $value or $type === "utility"). +function* walk(node, path = []) { + if (!node || typeof node !== "object") return; + if ("$value" in node || node.$type === "utility") { + yield [path, node]; + if (!node.$value || typeof node.$value !== "object") return; + } + for (const [k, v] of Object.entries(node)) { + if (k.startsWith("$")) continue; + yield* walk(v, [...path, k]); + } +} + +// Path → CSS variable name. Trailing `DEFAULT` segment is dropped (Tailwind convention). +function varName(path) { + const cleaned = path[path.length - 1] === "DEFAULT" ? path.slice(0, -1) : path; + return "--" + cleaned.join("-"); +} + +// Set of valid token paths (e.g. "color.gray.50", "utility.border-tertiary"). +// Populated once at the start of build(); referenced by resolveTemplate() and +// refToClass() so a typo'd `{ref}` fails the build instead of emitting broken +// CSS or a dangling utility class. +let VALID_PATHS = null; + +function assertKnownRef(ref, source) { + if (VALID_PATHS && !VALID_PATHS.has(ref)) { + throw new Error( + `[tokens] Unknown token reference \`${source}\` (resolved path: \`${ref}\`). ` + + `Add it to design/tokens/sure.tokens.json or fix the typo.` + ); + } +} + +// Resolve template strings: +// {a.b} → var(--a-b) +// {a.b|N%} → --alpha(var(--a-b) / N%) +function resolveTemplate(s) { + if (typeof s !== "string") return s; + return s.replace(/\{([^|}]+)(?:\|([^}]+))?\}/g, (whole, ref, alpha) => { + assertKnownRef(ref, whole); + const cssVar = "--" + ref.split(".").join("-"); + return alpha ? `--alpha(var(${cssVar}) / ${alpha})` : `var(${cssVar})`; + }); +} + +// {color.gray.50} or {utility.border-tertiary} → Tailwind utility class name with the given prefix. +// Drops a leading `color.` segment (since Tailwind colors are referenced as `bg-gray-50`, not `bg-color-gray-50`). +function refToClass(refStr, prefix) { + const inner = refStr.replace(/^\{|\}$/g, ""); + assertKnownRef(inner, refStr); + if (inner.startsWith("utility.")) return inner.slice("utility.".length); + const parts = inner.split("."); + if (parts[0] === "color") parts.shift(); + return prefix + "-" + parts.join("-"); +} + +// Utility @apply argument. If value is a raw class string (no `{}`), pass through. +// If value is a `{ref}`, resolve to a Tailwind class via the given prefix. +function utilityClasses(value, prefix) { + if (typeof value !== "string") return ""; + if (!value.includes("{")) return value; + return refToClass(value, prefix); +} + +function build() { + const tokens = JSON.parse(readFileSync(TOKENS, "utf8")); + + // Pre-compute the set of valid token paths so refs can be validated as we go. + VALID_PATHS = new Set(); + for (const [path] of walk(tokens)) { + VALID_PATHS.add(path.join(".")); + } + + const themeLines = []; + const darkLines = []; + const utilityBlocks = []; + + for (const [path, node] of walk(tokens)) { + if (path[0] === "utility") { + const name = path.slice(1).join("-"); + const ext = node.$extensions || {}; + + if (ext["sure.compose"]) { + utilityBlocks.push(`@utility ${name} {\n @apply ${ext["sure.compose"].join(" ")};\n}`); + continue; + } + + const prefix = ext["sure.utility"]?.prefix; + const raw = ext["sure.utility"]?.raw; + const dark = ext["sure.dark"]; + + const lightLine = raw + ? `${raw}: ${resolveTemplate(node.$value)};` + : `@apply ${utilityClasses(node.$value, prefix)};`; + + let block = `@utility ${name} {\n ${lightLine}`; + if (dark) { + const darkLine = raw + ? `${raw}: ${resolveTemplate(dark)};` + : `@apply ${utilityClasses(dark, prefix)};`; + block += `\n\n @variant theme-dark {\n ${darkLine}\n }`; + } + block += `\n}`; + utilityBlocks.push(block); + continue; + } + + const name = varName(path); + themeLines.push(` ${name}: ${resolveTemplate(node.$value)};`); + + const dark = node.$extensions?.["sure.dark"]; + if (dark !== undefined) { + darkLines.push(` ${name}: ${resolveTemplate(dark)};`); + } + } + + const css = `${HEADER} +@theme { +${themeLines.join("\n")} + +${KEYFRAMES} +} + +@layer base { + [data-theme="dark"] { +${darkLines.join("\n")} + } +} + +${utilityBlocks.join("\n\n")} +`; + + writeFileSync(OUT, css); + console.log(`tokens → ${OUT.replace(ROOT + "/", "")} (${themeLines.length} primitives, ${darkLines.length} dark overrides, ${utilityBlocks.length} utilities)`); +} + +try { + build(); +} catch (err) { + // Token errors are user-facing; the stack trace is noise. + if (err.message?.startsWith("[tokens]")) { + console.error(err.message); + process.exit(1); + } + throw err; +} diff --git a/design/tokens/README.md b/design/tokens/README.md new file mode 100644 index 000000000..5fdf80c8c --- /dev/null +++ b/design/tokens/README.md @@ -0,0 +1,101 @@ +# Sure design tokens + +This is where the design system actually lives. Tailwind reads from here, and any external tooling (Figma Tokens Studio, AI design tools, anything that shows up later) is meant to read the same JSON. + +## Files + +- `design/tokens/sure.tokens.json`: every token, hand-edited. +- `bin/tokens.mjs`: plain Node script. Compiles the JSON into Tailwind v4 CSS. +- `app/assets/tailwind/sure-design-system/_generated.css`: the build output. Generated, do not edit by hand. + +## Workflow + +```bash +# Edit a token: +$EDITOR design/tokens/sure.tokens.json + +# Regenerate the CSS: +npm run tokens:build + +# Commit both files together: +git add design/tokens/sure.tokens.json app/assets/tailwind/sure-design-system/_generated.css +``` + +`bin/setup` runs the build automatically on a fresh checkout. + +## Schema + +The file uses the [W3C DTCG token format](https://design-tokens.github.io/community-group/format/): `$value`, `$type`, `$description`, `$extensions`. Tokens cross-reference via `{path.to.token}` placeholders. + +```jsonc +{ + "color": { + "white": { "$value": "#ffffff", "$type": "color" }, + "gray": { + "500": { "$value": "#737373", "$type": "color" } + }, + "success": { + "$value": "{color.green.600}", + "$type": "color", + "$extensions": { "sure.dark": "{color.green.500}" } + } + } +} +``` + +### Top-level groups + +| Key | Purpose | +|-----|---------| +| `font` | font-family stacks | +| `color` | base colors, semantic aliases (success, warning, destructive, shadow), full-scale ladders, alpha ladders | +| `budget` | budget-chart fills (need their own dark variants because Stimulus controllers reference them) | +| `border.radius` | corner radii | +| `shadow` | drop shadows, both light and dark variants | +| `animate` | named animations | +| `utility` | Tailwind `@utility` blocks: semantic surfaces, foregrounds, borders, button backgrounds, etc. | + +### Custom `$extensions.sure.*` + +| Extension | Where | What it does | +|-----------|-------|--------------| +| `sure.dark` | any token | Dark-mode override value. Same template syntax as `$value`. | +| `sure.alpha` | reserved | Currently unused; alpha is expressed inline via `{ref\|N%}`. Reserved for structured alpha if it's ever needed. | +| `sure.utility.prefix` | `utility.*` only | The Tailwind utility family (`bg`, `text`, `border`). Tells the build which `@apply` class to emit. | +| `sure.utility.raw` | `utility.*` only | A CSS property name (`background-color`, `box-shadow`, etc.) when the utility emits raw CSS instead of `@apply`. | +| `sure.compose` | `utility.*` only | Array of class names to `@apply`. For example, `bg-loader` is `["bg-surface-inset", "animate-pulse"]`. | + +### Template strings + +Anywhere a `$value` is a string: + +- `{path.to.token}` resolves to `var(--path-to-token)` in the generated CSS. +- `{path.to.token|N%}` resolves to `--alpha(var(--path-to-token) / N%)` (Tailwind v4 alpha syntax). + +The same syntax appears inside composite values like `shadow.xs.$value`: `"0px 1px 2px 0px {color.black|6%}"`. + +### Adding a new token + +1. Pick the right top-level group. +2. Add the `$value` (raw or `{ref}`) and `$type`. +3. If it should change in dark mode, add `$extensions.sure.dark`. +4. If it's a utility, add `$extensions.sure.utility.prefix` (or `raw`, or `compose`). +5. Run `npm run tokens:build`. +6. Look at the diff in `_generated.css` and confirm it's what you expected. +7. Commit both files. + +### Edge cases the build script handles + +- `color.gray.DEFAULT`: the `DEFAULT` segment is dropped in the CSS variable name (`--color-gray`, not `--color-gray-DEFAULT`). DTCG convention; matches Tailwind. +- `utility.border-divider`: the value is a plain class string (`border-tertiary`) instead of a `{ref}`. The build treats values without `{}` as raw `@apply` arguments. +- `utility.bg-overlay`: uses `sure.utility.raw: "background-color"` because it needs alpha rendering instead of `@apply`. +- `utility.bg-loader`: uses `sure.compose` to apply two utilities together (`bg-surface-inset animate-pulse`). +- `utility.button-bg-ghost-hover`: its dark value is a multi-class string (`bg-gray-800 fg-inverse`), not a single ref. The build accepts both forms. + +## Consumers + +- Rails / Tailwind: via the generated CSS, automatically. +- Lookbook reference page: `/design-system/inspect/design_tokens/*` reads `sure.tokens.json` at request time. +- External tools (Figma Tokens Studio, AI design tools, etc.): point them at this file. + +If a consumer wants a different shape, transform the JSON in their tooling rather than editing the source here. diff --git a/design/tokens/sure.tokens.json b/design/tokens/sure.tokens.json new file mode 100644 index 000000000..61ee4d906 --- /dev/null +++ b/design/tokens/sure.tokens.json @@ -0,0 +1,356 @@ +{ + "$schema": "https://design-tokens.github.io/community-group/format/", + "$description": "Sure design tokens. Single source of truth. Hand-edit; run `npm run tokens:build` to regenerate CSS. Template syntax in $value strings: `{path.to.token}` resolves to `var(--path-to-token)`; `{path|N%}` becomes `--alpha(var(--path) / N%)`. Utility tokens whose value lacks `{}` are treated as raw Tailwind class lists for @apply.", + + "font": { + "sans": { + "$value": "'Geist', system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif", + "$type": "fontFamily" + }, + "mono": { + "$value": "'Geist Mono', ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace", + "$type": "fontFamily" + } + }, + + "color": { + "white": { "$value": "#ffffff", "$type": "color" }, + "black": { "$value": "#0B0B0B", "$type": "color" }, + + "success": { "$value": "{color.green.600}", "$type": "color", "$extensions": { "sure.dark": "{color.green.500}" } }, + "warning": { "$value": "{color.yellow.600}", "$type": "color", "$extensions": { "sure.dark": "{color.yellow.400}" } }, + "destructive": { "$value": "{color.red.600}", "$type": "color", "$extensions": { "sure.dark": "{color.red.400}" } }, + "shadow": { "$value": "{color.black|6%}", "$type": "color", "$extensions": { "sure.dark": "{color.white|8%}" } }, + + "gray": { + "25": { "$value": "#FAFAFA", "$type": "color" }, + "50": { "$value": "#F7F7F7", "$type": "color" }, + "100": { "$value": "#F0F0F0", "$type": "color" }, + "200": { "$value": "#E7E7E7", "$type": "color" }, + "300": { "$value": "#CFCFCF", "$type": "color" }, + "400": { "$value": "#9E9E9E", "$type": "color" }, + "500": { "$value": "#737373", "$type": "color" }, + "600": { "$value": "#5C5C5C", "$type": "color" }, + "700": { "$value": "#363636", "$type": "color" }, + "800": { "$value": "#242424", "$type": "color" }, + "900": { "$value": "#171717", "$type": "color" }, + "DEFAULT": { "$value": "{color.gray.500}", "$type": "color" }, + "tint-5": { "$value": "{color.gray.500|5%}", "$type": "color" }, + "tint-10": { "$value": "{color.gray.500|10%}", "$type": "color" } + }, + + "alpha-white": { + "25": { "$value": "{color.white|3%}", "$type": "color" }, + "50": { "$value": "{color.white|5%}", "$type": "color" }, + "100": { "$value": "{color.white|8%}", "$type": "color" }, + "200": { "$value": "{color.white|10%}", "$type": "color" }, + "300": { "$value": "{color.white|15%}", "$type": "color" }, + "400": { "$value": "{color.white|20%}", "$type": "color" }, + "500": { "$value": "{color.white|30%}", "$type": "color" }, + "600": { "$value": "{color.white|40%}", "$type": "color" }, + "700": { "$value": "{color.white|50%}", "$type": "color" }, + "800": { "$value": "{color.white|70%}", "$type": "color" }, + "900": { "$value": "{color.white|85%}", "$type": "color" } + }, + + "alpha-black": { + "25": { "$value": "{color.black|3%}", "$type": "color" }, + "50": { "$value": "{color.black|5%}", "$type": "color" }, + "100": { "$value": "{color.black|8%}", "$type": "color" }, + "200": { "$value": "{color.black|10%}", "$type": "color" }, + "300": { "$value": "{color.black|15%}", "$type": "color" }, + "400": { "$value": "{color.black|20%}", "$type": "color" }, + "500": { "$value": "{color.black|30%}", "$type": "color" }, + "600": { "$value": "{color.black|40%}", "$type": "color" }, + "700": { "$value": "{color.black|50%}", "$type": "color" }, + "800": { "$value": "{color.black|70%}", "$type": "color" }, + "900": { "$value": "{color.black|85%}", "$type": "color" } + }, + + "red": { + "25": { "$value": "#FFFBFB", "$type": "color" }, + "50": { "$value": "#FFF1F0", "$type": "color" }, + "100": { "$value": "#FFDEDB", "$type": "color" }, + "200": { "$value": "#FEB9B3", "$type": "color" }, + "300": { "$value": "#F88C86", "$type": "color" }, + "400": { "$value": "#ED4E4E", "$type": "color" }, + "500": { "$value": "#F13636", "$type": "color" }, + "600": { "$value": "#EC2222", "$type": "color" }, + "700": { "$value": "#C91313", "$type": "color" }, + "800": { "$value": "#A40E0E", "$type": "color" }, + "900": { "$value": "#7E0707", "$type": "color" }, + "tint-5": { "$value": "{color.red.500|5%}", "$type": "color" }, + "tint-10": { "$value": "{color.red.500|10%}", "$type": "color" } + }, + + "green": { + "25": { "$value": "#F6FEF9", "$type": "color" }, + "50": { "$value": "#ECFDF3", "$type": "color" }, + "100": { "$value": "#D1FADF", "$type": "color" }, + "200": { "$value": "#A6F4C5", "$type": "color" }, + "300": { "$value": "#6CE9A6", "$type": "color" }, + "400": { "$value": "#32D583", "$type": "color" }, + "500": { "$value": "#12B76A", "$type": "color" }, + "600": { "$value": "#10A861", "$type": "color" }, + "700": { "$value": "#078C52", "$type": "color" }, + "800": { "$value": "#05603A", "$type": "color" }, + "900": { "$value": "#054F31", "$type": "color" }, + "tint-5": { "$value": "{color.green.500|5%}", "$type": "color" }, + "tint-10": { "$value": "{color.green.500|10%}", "$type": "color" } + }, + + "yellow": { + "25": { "$value": "#FFFCF5", "$type": "color" }, + "50": { "$value": "#FFFAEB", "$type": "color" }, + "100": { "$value": "#FEF0C7", "$type": "color" }, + "200": { "$value": "#FEDF89", "$type": "color" }, + "300": { "$value": "#FEC84B", "$type": "color" }, + "400": { "$value": "#FDB022", "$type": "color" }, + "500": { "$value": "#F79009", "$type": "color" }, + "600": { "$value": "#DC6803", "$type": "color" }, + "700": { "$value": "#B54708", "$type": "color" }, + "800": { "$value": "#93370D", "$type": "color" }, + "900": { "$value": "#7A2E0E", "$type": "color" }, + "tint-5": { "$value": "{color.yellow.500|5%}", "$type": "color" }, + "tint-10": { "$value": "{color.yellow.500|10%}", "$type": "color" } + }, + + "cyan": { + "25": { "$value": "#F5FEFF", "$type": "color" }, + "50": { "$value": "#ECFDFF", "$type": "color" }, + "100": { "$value": "#CFF9FE", "$type": "color" }, + "200": { "$value": "#A5F0FC", "$type": "color" }, + "300": { "$value": "#67E3F9", "$type": "color" }, + "400": { "$value": "#22CCEE", "$type": "color" }, + "500": { "$value": "#06AED4", "$type": "color" }, + "600": { "$value": "#088AB2", "$type": "color" }, + "700": { "$value": "#0E7090", "$type": "color" }, + "800": { "$value": "#155B75", "$type": "color" }, + "900": { "$value": "#155B75", "$type": "color" }, + "tint-5": { "$value": "{color.cyan.500|5%}", "$type": "color" }, + "tint-10": { "$value": "{color.cyan.500|10%}", "$type": "color" } + }, + + "blue": { + "25": { "$value": "#F5FAFF", "$type": "color" }, + "50": { "$value": "#EFF8FF", "$type": "color" }, + "100": { "$value": "#D1E9FF", "$type": "color" }, + "200": { "$value": "#B2DDFF", "$type": "color" }, + "300": { "$value": "#84CAFF", "$type": "color" }, + "400": { "$value": "#53B1FD", "$type": "color" }, + "500": { "$value": "#2E90FA", "$type": "color" }, + "600": { "$value": "#1570EF", "$type": "color" }, + "700": { "$value": "#175CD3", "$type": "color" }, + "800": { "$value": "#1849A9", "$type": "color" }, + "900": { "$value": "#194185", "$type": "color" }, + "tint-5": { "$value": "{color.blue.500|5%}", "$type": "color" }, + "tint-10": { "$value": "{color.blue.500|10%}", "$type": "color" } + }, + + "indigo": { + "25": { "$value": "#F5F8FF", "$type": "color" }, + "50": { "$value": "#EFF4FF", "$type": "color" }, + "100": { "$value": "#E0EAFF", "$type": "color" }, + "200": { "$value": "#C7D7FE", "$type": "color" }, + "300": { "$value": "#A4BCFD", "$type": "color" }, + "400": { "$value": "#8098F9", "$type": "color" }, + "500": { "$value": "#6172F3", "$type": "color" }, + "600": { "$value": "#444CE7", "$type": "color" }, + "700": { "$value": "#3538CD", "$type": "color" }, + "800": { "$value": "#2D31A6", "$type": "color" }, + "900": { "$value": "#2D3282", "$type": "color" }, + "tint-5": { "$value": "{color.indigo.500|5%}", "$type": "color" }, + "tint-10": { "$value": "{color.indigo.500|10%}", "$type": "color" } + }, + + "violet": { + "25": { "$value": "#FBFAFF", "$type": "color" }, + "50": { "$value": "#F5F3FF", "$type": "color" }, + "100": { "$value": "#ECE9FE", "$type": "color" }, + "200": { "$value": "#DDD6FE", "$type": "color" }, + "300": { "$value": "#C3B5FD", "$type": "color" }, + "400": { "$value": "#A48AFB", "$type": "color" }, + "500": { "$value": "#875BF7", "$type": "color" }, + "600": { "$value": "#7839EE", "$type": "color" }, + "700": { "$value": "#6927DA", "$type": "color" }, + "tint-5": { "$value": "{color.violet.500|5%}", "$type": "color" }, + "tint-10": { "$value": "{color.violet.500|10%}", "$type": "color" } + }, + + "fuchsia": { + "25": { "$value": "#FEFAFF", "$type": "color" }, + "50": { "$value": "#FDF4FF", "$type": "color" }, + "100": { "$value": "#FBE8FF", "$type": "color" }, + "200": { "$value": "#F6D0FE", "$type": "color" }, + "300": { "$value": "#EEAAFD", "$type": "color" }, + "400": { "$value": "#E478FA", "$type": "color" }, + "500": { "$value": "#D444F1", "$type": "color" }, + "600": { "$value": "#BA24D5", "$type": "color" }, + "700": { "$value": "#9F1AB1", "$type": "color" }, + "800": { "$value": "#821890", "$type": "color" }, + "900": { "$value": "#6F1877", "$type": "color" }, + "tint-5": { "$value": "{color.fuchsia.500|5%}", "$type": "color" }, + "tint-10": { "$value": "{color.fuchsia.500|10%}", "$type": "color" } + }, + + "pink": { + "25": { "$value": "#FFFAFC", "$type": "color" }, + "50": { "$value": "#FEF0F7", "$type": "color" }, + "100": { "$value": "#FFD1E2", "$type": "color" }, + "200": { "$value": "#FFB1CE", "$type": "color" }, + "300": { "$value": "#FD8FBA", "$type": "color" }, + "400": { "$value": "#F86BA7", "$type": "color" }, + "500": { "$value": "#F23E94", "$type": "color" }, + "600": { "$value": "#D5327F", "$type": "color" }, + "700": { "$value": "#BA256B", "$type": "color" }, + "800": { "$value": "#9E1958", "$type": "color" }, + "900": { "$value": "#840B45", "$type": "color" }, + "tint-5": { "$value": "{color.pink.500|5%}", "$type": "color" }, + "tint-10": { "$value": "{color.pink.500|10%}", "$type": "color" } + }, + + "orange": { + "25": { "$value": "#FFF9F5", "$type": "color" }, + "50": { "$value": "#FFF4ED", "$type": "color" }, + "100": { "$value": "#FFE6D5", "$type": "color" }, + "200": { "$value": "#FFD6AE", "$type": "color" }, + "300": { "$value": "#FF9C66", "$type": "color" }, + "400": { "$value": "#FF692E", "$type": "color" }, + "500": { "$value": "#FF4405", "$type": "color" }, + "600": { "$value": "#E62E05", "$type": "color" }, + "700": { "$value": "#BC1B06", "$type": "color" }, + "800": { "$value": "#97180C", "$type": "color" }, + "900": { "$value": "#771A0D", "$type": "color" }, + "tint-5": { "$value": "{color.orange.500|5%}", "$type": "color" }, + "tint-10": { "$value": "{color.orange.500|10%}", "$type": "color" } + } + }, + + "budget": { + "unused-fill": { "$value": "{color.gray.200}", "$type": "color", "$extensions": { "sure.dark": "{color.gray.500}" } }, + "unallocated-fill": { "$value": "{color.gray.50}", "$type": "color", "$extensions": { "sure.dark": "{color.gray.700}" } } + }, + + "border": { + "radius": { + "md": { "$value": "8px", "$type": "dimension" }, + "lg": { "$value": "10px", "$type": "dimension" } + } + }, + + "shadow": { + "xs": { "$value": "0px 1px 2px 0px {color.black|6%}", "$type": "shadow", "$extensions": { "sure.dark": "0px 1px 2px 0px {color.white|8%}" } }, + "sm": { "$value": "0px 1px 6px 0px {color.black|6%}", "$type": "shadow", "$extensions": { "sure.dark": "0px 1px 6px 0px {color.white|8%}" } }, + "md": { "$value": "0px 4px 8px -2px {color.black|6%}", "$type": "shadow", "$extensions": { "sure.dark": "0px 4px 8px -2px {color.white|8%}" } }, + "lg": { "$value": "0px 12px 16px -4px {color.black|6%}","$type": "shadow", "$extensions": { "sure.dark": "0px 12px 16px -4px {color.white|8%}" } }, + "xl": { "$value": "0px 20px 24px -4px {color.black|6%}","$type": "shadow", "$extensions": { "sure.dark": "0px 20px 24px -4px {color.white|8%}" } } + }, + + "animate": { + "stroke-fill": { "$value": "stroke-fill 3s 300ms forwards", "$type": "transition" } + }, + + "utility": { + "bg-surface": { "$type": "utility", "$value": "{color.gray.50}", "$extensions": { "sure.utility": { "prefix": "bg" }, "sure.dark": "{color.black}" } }, + "bg-surface-hover": { "$type": "utility", "$value": "{color.gray.100}", "$extensions": { "sure.utility": { "prefix": "bg" }, "sure.dark": "{color.gray.800}" } }, + "bg-surface-inset": { "$type": "utility", "$value": "{color.gray.100}", "$extensions": { "sure.utility": { "prefix": "bg" }, "sure.dark": "{color.gray.800}" } }, + "bg-surface-inset-hover": { "$type": "utility", "$value": "{color.gray.200}", "$extensions": { "sure.utility": { "prefix": "bg" }, "sure.dark": "{color.gray.800}" } }, + "bg-container": { "$type": "utility", "$value": "{color.white}", "$extensions": { "sure.utility": { "prefix": "bg" }, "sure.dark": "{color.gray.900}" } }, + "bg-container-hover": { "$type": "utility", "$value": "{color.gray.50}", "$extensions": { "sure.utility": { "prefix": "bg" }, "sure.dark": "{color.gray.800}" } }, + "bg-container-inset": { "$type": "utility", "$value": "{color.gray.50}", "$extensions": { "sure.utility": { "prefix": "bg" }, "sure.dark": "{color.gray.800}" } }, + "bg-container-inset-hover":{ "$type": "utility","$value": "{color.gray.100}", "$extensions": { "sure.utility": { "prefix": "bg" }, "sure.dark": "{color.gray.700}" } }, + "bg-inverse": { "$type": "utility", "$value": "{color.gray.800}", "$extensions": { "sure.utility": { "prefix": "bg" }, "sure.dark": "{color.white}" } }, + "bg-inverse-hover": { "$type": "utility", "$value": "{color.gray.700}", "$extensions": { "sure.utility": { "prefix": "bg" }, "sure.dark": "{color.gray.100}" } }, + "bg-overlay": { + "$type": "utility", + "$value": "{color.gray.100|50%}", + "$extensions": { + "sure.utility": { "raw": "background-color" }, + "sure.dark": "{color.alpha-black.900}" + } + }, + "bg-loader": { + "$type": "utility", + "$extensions": { "sure.compose": ["bg-surface-inset", "animate-pulse"] } + }, + + "fg-gray": { "$type": "utility", "$value": "{color.gray.500}", "$extensions": { "sure.utility": { "prefix": "text" }, "sure.dark": "{color.gray.400}" } }, + "fg-contrast": { "$type": "utility", "$value": "{color.gray.400}", "$extensions": { "sure.utility": { "prefix": "text" }, "sure.dark": "{color.gray.500}" } }, + "fg-inverse": { "$type": "utility", "$value": "{color.white}", "$extensions": { "sure.utility": { "prefix": "text" }, "sure.dark": "{color.gray.900}" } }, + "fg-primary": { "$type": "utility", "$value": "{color.gray.900}", "$extensions": { "sure.utility": { "prefix": "text" }, "sure.dark": "{color.white}" } }, + "fg-primary-variant": { "$type": "utility", "$value": "{color.gray.800}", "$extensions": { "sure.utility": { "prefix": "text" }, "sure.dark": "{color.gray.50}" } }, + "fg-secondary": { "$type": "utility", "$value": "{color.gray.50}", "$extensions": { "sure.utility": { "prefix": "text" }, "sure.dark": "{color.gray.400}" } }, + "fg-secondary-variant": { "$type": "utility", "$value": "{color.gray.100}", "$extensions": { "sure.utility": { "prefix": "text" }, "sure.dark": "{color.gray.500}" } }, + "fg-subdued": { "$type": "utility", "$value": "{color.gray.400}", "$extensions": { "sure.utility": { "prefix": "text" }, "sure.dark": "{color.gray.500}" } }, + + "text-primary": { "$type": "utility", "$value": "{color.gray.900}", "$extensions": { "sure.utility": { "prefix": "text" }, "sure.dark": "{color.white}" } }, + "text-inverse": { "$type": "utility", "$value": "{color.white}", "$extensions": { "sure.utility": { "prefix": "text" }, "sure.dark": "{color.gray.900}" } }, + "text-secondary": { "$type": "utility", "$value": "{color.gray.500}", "$extensions": { "sure.utility": { "prefix": "text" }, "sure.dark": "{color.gray.300}" } }, + "text-subdued": { "$type": "utility", "$value": "{color.gray.400}", "$extensions": { "sure.utility": { "prefix": "text" }, "sure.dark": "{color.gray.500}" } }, + "text-link": { "$type": "utility", "$value": "{color.blue.600}", "$extensions": { "sure.utility": { "prefix": "text" }, "sure.dark": "{color.blue.500}" } }, + + "shadow-border-xs": { + "$type": "utility", + "$value": "{shadow.xs}, 0px 0px 0px 1px {color.alpha-black.50}", + "$extensions": { + "sure.utility": { "raw": "box-shadow" }, + "sure.dark": "{shadow.xs}, 0px 0px 0px 1px {color.alpha-white.50}" + } + }, + "shadow-border-sm": { + "$type": "utility", + "$value": "{shadow.sm}, 0px 0px 0px 1px {color.alpha-black.50}", + "$extensions": { + "sure.utility": { "raw": "box-shadow" }, + "sure.dark": "{shadow.sm}, 0px 0px 0px 1px {color.alpha-white.50}" + } + }, + "shadow-border-md": { + "$type": "utility", + "$value": "{shadow.md}, 0px 0px 0px 1px {color.alpha-black.50}", + "$extensions": { + "sure.utility": { "raw": "box-shadow" }, + "sure.dark": "{shadow.md}, 0px 0px 0px 1px {color.alpha-white.50}" + } + }, + "shadow-border-lg": { + "$type": "utility", + "$value": "{shadow.lg}, 0px 0px 0px 1px {color.alpha-black.50}", + "$extensions": { + "sure.utility": { "raw": "box-shadow" }, + "sure.dark": "{shadow.lg}, 0px 0px 0px 1px {color.alpha-white.50}" + } + }, + "shadow-border-xl": { + "$type": "utility", + "$value": "{shadow.xl}, 0px 0px 0px 1px {color.alpha-black.50}", + "$extensions": { + "sure.utility": { "raw": "box-shadow" }, + "sure.dark": "{shadow.xl}, 0px 0px 0px 1px {color.alpha-white.50}" + } + }, + + "border-primary": { "$type": "utility", "$value": "{color.alpha-black.300}", "$extensions": { "sure.utility": { "prefix": "border" }, "sure.dark": "{color.alpha-white.400}" } }, + "border-secondary": { "$type": "utility", "$value": "{color.alpha-black.200}", "$extensions": { "sure.utility": { "prefix": "border" }, "sure.dark": "{color.alpha-white.300}" } }, + "border-tertiary": { "$type": "utility", "$value": "{color.alpha-black.100}", "$extensions": { "sure.utility": { "prefix": "border" }, "sure.dark": "{color.alpha-white.200}" } }, + "border-divider": { "$type": "utility", "$value": "border-tertiary" }, + "border-subdued": { "$type": "utility", "$value": "{color.alpha-black.50}", "$extensions": { "sure.utility": { "prefix": "border" }, "sure.dark": "{color.alpha-white.100}" } }, + "border-solid": { "$type": "utility", "$value": "{color.black}", "$extensions": { "sure.utility": { "prefix": "border" }, "sure.dark": "{color.white}" } }, + "border-destructive": { "$type": "utility", "$value": "{color.red.500}", "$extensions": { "sure.utility": { "prefix": "border" }, "sure.dark": "{color.red.400}" } }, + + "button-bg-primary": { "$type": "utility", "$value": "{color.gray.900}", "$extensions": { "sure.utility": { "prefix": "bg" }, "sure.dark": "{color.white}" } }, + "button-bg-primary-hover": { "$type": "utility", "$value": "{color.gray.800}", "$extensions": { "sure.utility": { "prefix": "bg" }, "sure.dark": "{color.gray.50}" } }, + "button-bg-secondary": { "$type": "utility", "$value": "{color.gray.50}", "$extensions": { "sure.utility": { "prefix": "bg" }, "sure.dark": "{color.gray.700}" } }, + "button-bg-secondary-hover": { "$type": "utility", "$value": "{color.gray.100}", "$extensions": { "sure.utility": { "prefix": "bg" }, "sure.dark": "{color.gray.600}" } }, + "button-bg-disabled": { "$type": "utility", "$value": "{color.gray.50}", "$extensions": { "sure.utility": { "prefix": "bg" }, "sure.dark": "{color.gray.700}" } }, + "button-bg-destructive": { "$type": "utility", "$value": "{color.red.500}", "$extensions": { "sure.utility": { "prefix": "bg" }, "sure.dark": "{color.red.400}" } }, + "button-bg-destructive-hover": { "$type": "utility", "$value": "{color.red.600}", "$extensions": { "sure.utility": { "prefix": "bg" }, "sure.dark": "{color.red.500}" } }, + "button-bg-ghost-hover": { "$type": "utility", "$value": "{color.gray.50}", "$extensions": { "sure.utility": { "prefix": "bg" }, "sure.dark": "bg-gray-800 fg-inverse" } }, + "button-bg-outline-hover": { "$type": "utility", "$value": "{color.gray.100}", "$extensions": { "sure.utility": { "prefix": "bg" }, "sure.dark": "{color.gray.700}" } }, + + "tab-item-active": { "$type": "utility", "$value": "{color.white}", "$extensions": { "sure.utility": { "prefix": "bg" }, "sure.dark": "{color.gray.700}" } }, + "tab-item-hover": { "$type": "utility", "$value": "{color.gray.200}", "$extensions": { "sure.utility": { "prefix": "bg" }, "sure.dark": "{color.gray.800}" } }, + "tab-bg-group": { "$type": "utility", "$value": "{color.gray.50}", "$extensions": { "sure.utility": { "prefix": "bg" }, "sure.dark": "{color.alpha-black.700}" } }, + "bg-nav-indicator": { "$type": "utility", "$value": "{color.black}", "$extensions": { "sure.utility": { "prefix": "bg" }, "sure.dark": "{color.white}" } } + } +} diff --git a/package.json b/package.json index c3ec2a6d4..8dbeeb8bf 100644 --- a/package.json +++ b/package.json @@ -11,7 +11,9 @@ "lint": "biome lint", "lint:fix": "biome lint --write", "format:check": "biome format", - "format": "biome format --write" + "format": "biome format --write", + "tokens:build": "node bin/tokens.mjs", + "tokens:check": "node bin/tokens.mjs && git diff --quiet -- app/assets/tailwind/sure-design-system/_generated.css" }, "author": "", "license": "ISC"