refactor(design-system): single-source design tokens via DTCG JSON (#1604)

* 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 <gariasf@proton.me>

* 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 <gariasf@proton.me>
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
Co-authored-by: Juan José Mata <jjmata@jjmata.com>
This commit is contained in:
Guillem Arias Fauste
2026-05-01 14:46:33 +02:00
committed by GitHub
parent 2cff2065eb
commit e250d266e8
21 changed files with 1234 additions and 634 deletions

View File

@@ -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

3
.gitattributes vendored
View File

@@ -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

View File

@@ -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`

View File

@@ -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
```

View File

@@ -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`

View File

@@ -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";

View File

@@ -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;
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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';

View File

@@ -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;
}
}
}
@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;
}
}

View File

@@ -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;
}
}
}

View File

@@ -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);
}
}
}

View File

@@ -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;
}

View File

@@ -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"

172
bin/tokens.mjs Executable file
View File

@@ -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;
}

101
design/tokens/README.md Normal file
View File

@@ -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.

View File

@@ -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}" } }
}
}

View File

@@ -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"