Files
sure/app/assets/tailwind/sure-design-system/components.css
Guillem Arias Fauste e250d266e8 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>
2026-05-01 14:46:33 +02:00

149 lines
4.3 KiB
CSS

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