Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
## Unreleased

### Features
- **tokens:** add `--sf-density` (PUBLIC-ADVANCED, default `1`) — the compact ↔ comfortable dial for interactive control **geometry**. It scales the `--sf-size-*` rung ladder (24·32·40·48·56px at `1`); below `1` packs controls tighter for dashboards / data tables / power-user tools, above `1` loosens them. A deliberate design or user-preference choice, **not** viewport-fluid (control height must not shrink on small screens where touch targets need to grow), so the size ladder — previously static literals scaled by no knob — now moves only by this one dial. Orthogonal to `--sf-space-scale` (whitespace) and `--sf-section-scale` (section rhythm); combine them for a fully compact UI. The `--sf-touch-target` accessibility floor stays independent, so a small value can't pull native controls under the WCAG target. Wired through the configurator (spacing domain) and the LLM guide's global-multipliers section.
- **motion:** add `.sf-stagger` — put it on a parent and every direct child gets an incrementing `animation-delay`, so a group of time-based entrance animations (`.sf-fade-in` / `.sf-slide-in-*`) plays in sequence. Works on any number of children with no manual per-item index: where `sibling-index()` is supported the ramp is unbounded, otherwise it falls back to an 8-step `:nth-child` ramp (covering a 4-column grid's first two rows) that plateaus. One knob, `--sf-stagger-step` (default `75ms`), retunes the whole sequence; each child's delay is `index × --sf-stagger-step × --sf-motion-scale`. Choreography only — a child without its own animation just carries an inert delay, so animating only some children needs no opt-out on the rest.
- **tokens:** the `.sf-hover-*` transform utilities now read magnitude knobs instead of hard-coded values — `--sf-hover-grow-scale` (1.05), `--sf-hover-shrink-scale` (0.95), `--sf-hover-lift` (0.25em, float/sink), `--sf-hover-slide` (0.5em, slide-start/end) — so each effect's strength is one global override.

Expand Down
49 changes: 35 additions & 14 deletions configurator/src/data/api-index.generated.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,21 +2,21 @@
"_sync": {
"generatedBy": "configurator/scripts/sync-api.mjs",
"source": "docs/api-index.json",
"tokensHash": "5624c486ab4d",
"tokensHash": "2733b25b05b9",
"bundles": [
"full",
"optimal"
],
"counts": {
"tokens": 755,
"tokens": 756,
"byTier": {
"PUBLIC": 695,
"PUBLIC-ADVANCED": 59,
"PUBLIC-ADVANCED": 60,
"INTERNAL": 1
},
"byCategory": {
"Layout tokens": 53,
"Core tokens": 622,
"Core tokens": 623,
"Macro tokens": 34,
"Component tokens": 46
}
Expand Down Expand Up @@ -7855,6 +7855,27 @@
"optimal"
]
},
{
"name": "--sf-density",
"tier": "PUBLIC-ADVANCED",
"role": "knob",
"namespace": "density",
"category": "Core tokens",
"group": "Density",
"description": "Compact ↔ comfortable dial for interactive control geometry (the --sf-size-* rung ladder). Default 1; below 1 packs controls tighter (dashboards, data tables), above 1 loosens them. A deliberate design/user choice, not a viewport response — orthogonal to --sf-space-scale (whitespace) and --sf-section-scale (section rhythm). The --sf-touch-target accessibility floor is independent, so a small value can't shrink native controls below the WCAG target. Like the other global multipliers it is a :root dial (the size ladder is computed at :root and inherits, so nested overrides don't retroactively rescale it).",
"note": "Compact ↔ comfortable dial for interactive control geometry (the --sf-size-* rung ladder). Default 1; below 1 packs controls tighter (dashboards, data tables), above 1 loosens them. A deliberate design/user choice, not a viewport response — orthogonal to --sf-space-scale (whitespace) and --sf-section-scale (section rhythm). The --sf-touch-target accessibility floor is independent, so a small value can't shrink native controls below the WCAG target. Like the other global multipliers it is a :root dial (the size ladder is computed at :root and inherits, so nested overrides don't retroactively rescale it).",
"value": "1",
"aliasOf": null,
"registered": false,
"syntax": null,
"fallbackOnly": false,
"optional": false,
"layer": "slashed.tokens",
"bundles": [
"full",
"optimal"
]
},
{
"name": "--sf-display-l-line-height",
"tier": "PUBLIC",
Expand Down Expand Up @@ -13168,13 +13189,13 @@
{
"name": "--sf-size-l",
"tier": "PUBLIC",
"role": "knob",
"role": "consumption",
"namespace": "size",
"category": "Core tokens",
"group": "UI sizes",
"description": "Fixed sizes for interactive components (buttons, inputs, chips) at S / M / L.",
"note": "Large UI component height (~48px). Touch-friendly interactive elements.",
"value": "3rem",
"value": "calc(3rem * var(--sf-density))",
"aliasOf": null,
"registered": false,
"syntax": null,
Expand All @@ -13189,13 +13210,13 @@
{
"name": "--sf-size-m",
"tier": "PUBLIC",
"role": "knob",
"role": "consumption",
"namespace": "size",
"category": "Core tokens",
"group": "UI sizes",
"description": "Fixed sizes for interactive components (buttons, inputs, chips) at S / M / L.",
"note": "~40px UI component height (scale rung `m`). The default `.sf-btn` min-height.",
"value": "2.5rem",
"value": "calc(2.5rem * var(--sf-density))",
"aliasOf": null,
"registered": false,
"syntax": null,
Expand All @@ -13210,13 +13231,13 @@
{
"name": "--sf-size-s",
"tier": "PUBLIC",
"role": "knob",
"role": "consumption",
"namespace": "size",
"category": "Core tokens",
"group": "UI sizes",
"description": "Fixed sizes for interactive components (buttons, inputs, chips) at S / M / L.",
"note": "Small UI component height (~32px). Compact buttons and input variants.",
"value": "2rem",
"value": "calc(2rem * var(--sf-density))",
"aliasOf": null,
"registered": false,
"syntax": null,
Expand All @@ -13231,13 +13252,13 @@
{
"name": "--sf-size-xl",
"tier": "PUBLIC",
"role": "knob",
"role": "consumption",
"namespace": "size",
"category": "Core tokens",
"group": "UI sizes",
"description": "Fixed sizes for interactive components (buttons, inputs, chips) at S / M / L.",
"note": "Extra-large interactive component height (~56px). For oversized or prominently touch-friendly controls.",
"value": "3.5rem",
"value": "calc(3.5rem * var(--sf-density))",
"aliasOf": null,
"registered": false,
"syntax": null,
Expand All @@ -13252,13 +13273,13 @@
{
"name": "--sf-size-xs",
"tier": "PUBLIC",
"role": "knob",
"role": "consumption",
"namespace": "size",
"category": "Core tokens",
"group": "UI sizes",
"description": "Fixed sizes for interactive components (buttons, inputs, chips) at S / M / L.",
"note": "Extra-small interactive component height (~24px). For very compact inline elements and micro-controls.",
"value": "1.5rem",
"value": "calc(1.5rem * var(--sf-density))",
"aliasOf": null,
"registered": false,
"syntax": null,
Expand Down
2 changes: 1 addition & 1 deletion configurator/src/data/domain-patterns.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
"body-em", "body-text", "heading-text", "display-l-", "display-m-",
"-h1-", "-h2-", "-h3-", "-h4-", "-h5-", "-h6-",
"link-", "optical-sizing", "fluid-width", "fluid-min-vw", "fluid-max-vw"],
"spacing": ["space", "section", "gutter", "content-gap", "component-pad"],
"spacing": ["space", "section", "gutter", "content-gap", "component-pad", "density"],
"layout": ["container", "layout", "grid", "breakpoint", "measure", "column",
"header-height", "touch-target", "bento", "cluster",
"cover-min", "equal-min", "equal-rule", "frame", "ratio-", "reel", "safe-", "sidebar",
Expand Down
6 changes: 5 additions & 1 deletion configurator/src/data/token-registry.generated.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"_meta": {
"generatedBy": "scripts/gen-token-registry.js",
"nextId": 797
"nextId": 798
},
"tokens": [
{
Expand Down Expand Up @@ -3233,6 +3233,10 @@
{
"id": 796,
"name": "--sf-stagger-step"
},
{
"id": 797,
"name": "--sf-density"
}
]
}
38 changes: 29 additions & 9 deletions core/tokens.css
Original file line number Diff line number Diff line change
Expand Up @@ -1175,18 +1175,38 @@
--sf-space-3xl: calc(clamp(calc(var(--sf-space-base-min) * pow(var(--sf-space-ratio-min), 4) * 1rem), calc((var(--sf-space-base-max) * pow(var(--sf-space-ratio-max), 4) - var(--sf-space-base-min) * pow(var(--sf-space-ratio-min), 4)) / (var(--sf-fluid-max-vw) - var(--sf-fluid-min-vw)) * (var(--sf-fluid-width) - var(--sf-fluid-min-vw) * 1rem) + var(--sf-space-base-min) * pow(var(--sf-space-ratio-min), 4) * 1rem), calc(var(--sf-space-base-max) * pow(var(--sf-space-ratio-max), 4) * 1rem)) * var(--sf-space-scale));
--sf-space-4xl: calc(clamp(calc(var(--sf-space-base-min) * pow(var(--sf-space-ratio-min), 5) * 1rem), calc((var(--sf-space-base-max) * pow(var(--sf-space-ratio-max), 5) - var(--sf-space-base-min) * pow(var(--sf-space-ratio-min), 5)) / (var(--sf-fluid-max-vw) - var(--sf-fluid-min-vw)) * (var(--sf-fluid-width) - var(--sf-fluid-min-vw) * 1rem) + var(--sf-space-base-min) * pow(var(--sf-space-ratio-min), 5) * 1rem), calc(var(--sf-space-base-max) * pow(var(--sf-space-ratio-max), 5) * 1rem)) * var(--sf-space-scale));

/* ----------------------------------------------------------
Density — the compact ↔ comfortable dial for interactive
control GEOMETRY. 1 = default; <1 packs controls tighter
(dashboards, data tables, power-user tools), >1 loosens them.
It is a discrete design/product choice (or a user preference),
NOT a viewport response — control height must never shrink on
small screens where touch targets need to be larger, so unlike
the fluid text and space scales the size ladder stays non-fluid
and is scaled only by this deliberate knob. Orthogonal to
--sf-space-scale (overall whitespace) and --sf-section-scale
(macro section rhythm); combine them for a fully compact UI.
Accessibility is unaffected: the coarse-pointer floor lives on
--sf-touch-target (core/accessibility.css), never on a rung, so
a small --sf-density can't shrink native controls below the
WCAG target. Like the other global multipliers it is a :root
dial — the size ladder is computed at :root and inherits, so a
nested override does not retroactively rescale it (same
computed-value rule as --sf-space-scale et al.). */
Comment on lines +1192 to +1195

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Nested override claim contradicts CSS specification

The comment (and matching prose in docs/llm-guide.md and docs/token-annotations.json) states that a nested --sf-density override "does not retroactively rescale" the --sf-size-* ladder. This is incorrect. CSS custom property values are token streams that are substituted lazily at used-value time in each element's own cascade context — the same mechanism used by --sf-space-scale inline in the space tokens. A descendant element with --sf-density: 0.8 set on it inherits the token stream calc(2.5rem * var(--sf-density)) for --sf-size-m; when CSS resolves var(--sf-size-m) for that element, var(--sf-density) is looked up in that element's context → 0.8, giving 2rem. The behaviour the PR says was "verified" (no rescale) only holds when reading the size from the parent element after setting density on a child — the child and its descendants do see the rescaled value. Documenting this as strictly :root-only misinforms developers who would benefit from component-level density control, which actually works by design.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I checked this carefully before pushing the "does not rescale" claim, and re-verified it again just now — the documented behavior is correct, and this comment describes a plausible-sounding but incorrect mental model of custom-property substitution.

The key detail: var() substitution inside a custom property's own value happens at the element where that custom property is declared (or wins the cascade), not lazily re-evaluated per consuming descendant. --sf-size-m: calc(2.5rem * var(--sf-density)) is declared once, at :root. Chromium resolves var(--sf-density) there (→ 1) and the already-substituted token stream (calc(2.5rem * 1)) is what inherits down — not the original var(--sf-density) reference. A descendant overriding --sf-density does change its own getComputedStyle(...).getPropertyValue('--sf-density'), but --sf-size-m on that descendant still reads calc(2.5rem * 1), because it was never re-declared in that scope.

Verified two ways just now:

  1. Minimal isolated case (no SLASHED CSS at all):
    :root { --k: 1; --derived: calc(10px * var(--k)); }
    .child { --k: 5; }
    <div class="child" style="width:var(--derived)"> → computed width is 10px, not 50px.
  2. Same result reproduced against the actual built dist/slashed.full.css with --sf-density on a scoped <aside>.

This isn't a novel claim either — it's the exact same phenomenon core/layout.css already documents for --sf-fluid-width (see the .sf-fluid-cq comment): "the --sf-text-* / --sf-space-* tokens are computed at :root, so their var(--sf-fluid-width) is substituted there... overriding --sf-fluid-width lower in the tree can't retroactively rewrite an already-computed inherited value." --sf-density has the identical resolution shape, so the identical caveat applies.

If someone genuinely wants scoped density, the fix mirrors .sf-fluid-cq: re-declare the --sf-size-* ladder (not just --sf-density) on the scoping element's children. That's a real, addressable feature gap, but documenting today's behavior as "already works by design" would be inaccurate and would mislead people who try <aside style="--sf-density: 0.8"> expecting it to work. Keeping the current wording.


Generated by Claude Code

--sf-density: 1;

/* ----------------------------------------------------------
UI sizes — a purely geometric rung ladder (+0.5rem / 8px per
step: 24 · 32 · 40 · 48 · 56 px). These are free design knobs:
nothing external constrains them, so overriding them just
rescales component heights. Accessibility floors live on their
own tokens (--sf-touch-target), never on a rung — see below.
step: 24 · 32 · 40 · 48 · 56 px at density 1). These are free
design knobs, scaled by --sf-density; overriding a rung just
rescales that component height. Accessibility floors live on
their own tokens (--sf-touch-target), never on a rung.
---------------------------------------------------------- */
--sf-size-xs: 1.5rem;
--sf-size-s: 2rem;
--sf-size-m: 2.5rem;
--sf-size-l: 3rem;
--sf-size-xl: 3.5rem;
--sf-size-xs: calc(1.5rem * var(--sf-density));
--sf-size-s: calc(2rem * var(--sf-density));
--sf-size-m: calc(2.5rem * var(--sf-density));
--sf-size-l: calc(3rem * var(--sf-density));
--sf-size-xl: calc(3.5rem * var(--sf-density));

/* ----------------------------------------------------------
Containers
Expand Down
6 changes: 4 additions & 2 deletions demos/full-api-demo-with-overrides.html

Large diffs are not rendered by default.

6 changes: 4 additions & 2 deletions demos/full-api-demo.html

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions demos/generate.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -204,6 +204,7 @@ const CURATED = {
'--sf-text-scale': '1.6',
'--sf-text-display-scale': '1.6',
'--sf-motion-scale': '1.6',
'--sf-density': '1.6',
};

const LEN_UNIT = /^(-?[\d.]+)(px|rem|em|ch|vw|vh|dvh|svh|lvh|vmin|vmax|pt|cm|mm|ex|cap|lh|q)$/i;
Expand Down
12 changes: 4 additions & 8 deletions demos/ultimate-override.css
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
Perturbs every CONFIGURABLE (role: "knob") token to a valid, visibly-distinct
value so full-api-demo.html can prove each one is wired end to end.

Coverage: 242 of 270 knob tokens overridden.
Coverage: 238 of 266 knob tokens overridden.
28 knobs are intentionally NOT perturbed (structural /
meta / sentinel) — listed with reasons at the bottom of this file.
============================================================================ */
Expand Down Expand Up @@ -123,6 +123,9 @@
/* cover */
--sf-cover-min-height: 160dvh; /* was: 100dvh */

/* density */
--sf-density: 1.6; /* was: 1 */

/* display */
--sf-display-l-line-height: 1.2; /* was: 1 */
--sf-display-m-line-height: 1.25; /* was: 1.05 */
Expand Down Expand Up @@ -325,13 +328,6 @@
--sf-sidebar-min-width: 35%; /* was: 50% */
--sf-sidebar-width: 28.8rem; /* was: 18rem */

/* size */
--sf-size-l: 4.8rem; /* was: 3rem */
--sf-size-m: 4rem; /* was: 2.5rem */
--sf-size-s: 3.2rem; /* was: 2rem */
--sf-size-xl: 5.6rem; /* was: 3.5rem */
--sf-size-xs: 2.4rem; /* was: 1.5rem */

/* space */
--sf-space-base-max: 3; /* was: 2 */
--sf-space-base-min: 1.4; /* was: 1 */
Expand Down
15 changes: 10 additions & 5 deletions dist/css-custom-data.json
Original file line number Diff line number Diff line change
Expand Up @@ -1876,6 +1876,11 @@
"description": "Inherits the current font-weight from context. Useful as a reset.\n\nDefault: `var(--sf-font-weight-bold)`",
"syntax": "*"
},
{
"name": "--sf-density",
"description": "Compact ↔ comfortable dial for interactive control geometry (the --sf-size-* rung ladder). Default 1; below 1 packs controls tighter (dashboards, data tables), above 1 loosens them. A deliberate design/user choice, not a viewport response — orthogonal to --sf-space-scale (whitespace) and --sf-section-scale (section rhythm). The --sf-touch-target accessibility floor is independent, so a small value can't shrink native controls below the WCAG target. Like the other global multipliers it is a :root dial (the size ladder is computed at :root and inherits, so nested overrides don't retroactively rescale it).\n\nDefault: `1`\n\n(advanced token)",
"syntax": "*"
},
{
"name": "--sf-display-l-line-height",
"description": "Line height for large display text (--sf-text-display-l). Very tight to avoid excessive gaps at oversized sizes.\n\nDefault: `1`",
Expand Down Expand Up @@ -3138,27 +3143,27 @@
},
{
"name": "--sf-size-l",
"description": "Large UI component height (~48px). Touch-friendly interactive elements.\n\nDefault: `3rem`",
"description": "Large UI component height (~48px). Touch-friendly interactive elements.\n\nDefault: `calc(3rem * var(--sf-density))`",
"syntax": "*"
},
{
"name": "--sf-size-m",
"description": "~40px UI component height (scale rung `m`). The default `.sf-btn` min-height.\n\nDefault: `2.5rem`",
"description": "~40px UI component height (scale rung `m`). The default `.sf-btn` min-height.\n\nDefault: `calc(2.5rem * var(--sf-density))`",
"syntax": "*"
},
{
"name": "--sf-size-s",
"description": "Small UI component height (~32px). Compact buttons and input variants.\n\nDefault: `2rem`",
"description": "Small UI component height (~32px). Compact buttons and input variants.\n\nDefault: `calc(2rem * var(--sf-density))`",
"syntax": "*"
},
{
"name": "--sf-size-xl",
"description": "Extra-large interactive component height (~56px). For oversized or prominently touch-friendly controls.\n\nDefault: `3.5rem`",
"description": "Extra-large interactive component height (~56px). For oversized or prominently touch-friendly controls.\n\nDefault: `calc(3.5rem * var(--sf-density))`",
"syntax": "*"
},
{
"name": "--sf-size-xs",
"description": "Extra-small interactive component height (~24px). For very compact inline elements and micro-controls.\n\nDefault: `1.5rem`",
"description": "Extra-small interactive component height (~24px). For very compact inline elements and micro-controls.\n\nDefault: `calc(1.5rem * var(--sf-density))`",
"syntax": "*"
},
{
Expand Down
Loading