From a105e39058675d2e942bd064317c69045c190f58 Mon Sep 17 00:00:00 2001 From: Kiro Agent <244629292+kiro-agent@users.noreply.github.com> Date: Wed, 27 May 2026 09:47:01 +0000 Subject: [PATCH 1/4] feat: apply PR #118 audit-fixes-pre-freeze changes to main - Rename .focus-parent to .sf-focus-parent (prefixed canonical name) - Add .sf-icon--2xl modifier and --sf-icon-2xl: 4em token - Create docs/motion.md documenting the motion system - Expand optional/theme-example.css to maximal reference version - Update docs/registry.json with new classes, token, and unprefixed_classes - Update forms.css token header (radius-full, border-width-3, action-hover) - Add Naming exceptions and Flat bundles sections to architecture.md - Update all docs to reference sf-focus-parent consistently Co-authored-by: Jack Granatowski --- core/accessibility.css | 4 +- core/layout.css | 11 +- core/tokens.css | 13 +- docs/architecture.md | 36 +++++- docs/browser-support.md | 2 +- docs/demo.html | 6 +- docs/layout.md | 9 +- docs/migration.md | 4 +- docs/motion.md | 168 +++++++++++++++++++++++++ docs/registry.json | 23 +++- docs/states.md | 6 +- optional/forms.css | 6 +- optional/theme-example.css | 250 +++++++++++++++++++++++++++++++++---- tests/layers.spec.js | 2 +- 14 files changed, 484 insertions(+), 56 deletions(-) create mode 100644 docs/motion.md diff --git a/core/accessibility.css b/core/accessibility.css index 89c3a030..4d9e662e 100644 --- a/core/accessibility.css +++ b/core/accessibility.css @@ -161,6 +161,7 @@ top: var(--sf-space-m); } + /* ---------------------------------------------------------- Focus-parent — an a11y *pattern*, not a state. Lives in the accessibility layer (was previously in states.css; moved in @@ -168,8 +169,9 @@ and survive third-party resets at the same priority as the focus-visible ring above. Cannot be hardened with !important because the outline shorthand is consumer-customisable. + Canonical name: .sf-focus-parent (prefixed). ---------------------------------------------------------- */ - .focus-parent:focus-within { + .sf-focus-parent:focus-within { outline: var(--sf-focus-ring-width) var(--sf-focus-ring-style, solid) var(--sf-color-border--focus); outline-offset: var(--sf-focus-ring-offset); } diff --git a/core/layout.css b/core/layout.css index be8879a8..1956fa7d 100644 --- a/core/layout.css +++ b/core/layout.css @@ -270,11 +270,12 @@ vertical-align: text-bottom; fill: currentcolor; } - .sf-icon--xs { --sf-icon-size: var(--sf-icon-xs); } - .sf-icon--s { --sf-icon-size: var(--sf-icon-s); } - .sf-icon--m { --sf-icon-size: var(--sf-icon-m); } - .sf-icon--l { --sf-icon-size: var(--sf-icon-l); } - .sf-icon--xl { --sf-icon-size: var(--sf-icon-xl); } + .sf-icon--xs { --sf-icon-size: var(--sf-icon-xs); } + .sf-icon--s { --sf-icon-size: var(--sf-icon-s); } + .sf-icon--m { --sf-icon-size: var(--sf-icon-m); } + .sf-icon--l { --sf-icon-size: var(--sf-icon-l); } + .sf-icon--xl { --sf-icon-size: var(--sf-icon-xl); } + .sf-icon--2xl { --sf-icon-size: var(--sf-icon-2xl); } /* Boxed variant — wraps the icon in a padded, bordered, optionally coloured frame. Adds inline-/block-size on top of the naked icon diff --git a/core/tokens.css b/core/tokens.css index f2a7f2f6..beffb46f 100644 --- a/core/tokens.css +++ b/core/tokens.css @@ -561,14 +561,15 @@ --sf-icon-scale knob by design. clamp() is intentionally absent here: making icons fluid INDEPENDENTLY of context would break the relationship to surrounding text. - Capped at --xl (3em ≈ 48px on default text); above that the + Capped at --2xl (4em ≈ 64px on default text); above that the element ceases to be an icon and becomes an illustration. ---------------------------------------------------------- */ - --sf-icon-xs: 0.875em; - --sf-icon-s: 1em; - --sf-icon-m: 1.5em; - --sf-icon-l: 2em; - --sf-icon-xl: 3em; + --sf-icon-xs: 0.875em; + --sf-icon-s: 1em; + --sf-icon-m: 1.5em; + --sf-icon-l: 2em; + --sf-icon-xl: 3em; + --sf-icon-2xl: 4em; /* ---------------------------------------------------------- diff --git a/docs/architecture.md b/docs/architecture.md index 5072c14b..da6b7992 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -98,7 +98,7 @@ decision tree to use when adding a new class. NO → step 3 3. Is it an a11y pattern or a11y utility? - (sr-only, focus-parent, clickable-parent…) + (sr-only, sf-focus-parent, clickable-parent…) YES → core/accessibility.css (slashed.accessibility) NO → step 4 @@ -213,7 +213,7 @@ Transition tokens live in `core/tokens.css`: `@property` color interpolation is demonstrated by `.sf-color-pulse` which animates `--sf-color-primary-light` lightness via `sf-color-pulse` keyframes — proving that registered custom properties interpolate smoothly in oklch. -**slashed.accessibility** — `:focus-visible`, `.sr-only`, `.skip-link`, reduced-motion resets, plus the a11y patterns `.focus-parent` (relocated from `slashed.states` in v0.3.0) and `.sf-clickable-parent` (added in v0.3.0). High in the stack to override motion without relying solely on `!important`. Selective `!important` used only where override is a genuine accessibility barrier (focus ring, reduced motion, sr-only). `.sr-only` uses `overflow: clip` (modern consensus — avoids creating a new scroll container unlike the legacy `overflow: hidden`). `.visually-hidden` is shipped as a synonym of `.sr-only` for teams that prefer the WHATWG naming convention. +**slashed.accessibility** — `:focus-visible`, `.sr-only`, `.skip-link`, reduced-motion resets, plus the a11y patterns `.sf-focus-parent` (v0.3.0) and `.sf-clickable-parent` (added in v0.3.0). High in the stack to override motion without relying solely on `!important`. Selective `!important` used only where override is a genuine accessibility barrier (focus ring, reduced motion, sr-only). `.sr-only` uses `overflow: clip` (modern consensus — avoids creating a new scroll container unlike the legacy `overflow: hidden`). `.visually-hidden` is shipped as a synonym of `.sr-only` for teams that prefer the WHATWG naming convention. **slashed.print** — `@media print` only. Contains `@page` rule consuming `--sf-print-*` tokens. Authored colour is preserved by default; consumers opt into ink-on-paper via `.print-no-color` or force colour via `.print-color-exact`. `!important` is reserved for selectors whose semantics require defeating consumer CSS: the hide-list (`nav, aside, button, input, select, textarea, dialog, [popover], .no-print`), `details > summary`, and the two opt-in colour classes. @@ -271,6 +271,25 @@ Print helpers use the `.print-*` prefix: `.print-only` (show only on paper), `.no-print` (hide on paper), `.print-color-exact` (force colour), and `.print-no-color` (force ink-saving flatten). See the **slashed.print** layer. +### Naming exceptions + +A small number of classes are intentionally unprefixed (no `sf-` or `is-`). +They are short, universally understood terms where a prefix would add noise +without reducing collision risk in practice. + +| Class | Layer | Rationale | +|---|---|---| +| `.sr-only` | accessibility | Industry-standard screen-reader name | +| `.sr-only-focusable` | accessibility | Companion to `.sr-only` | +| `.visually-hidden` | accessibility | WHATWG synonym of `.sr-only` | +| `.skip-link` | accessibility | Common a11y pattern name | +| `.no-motion` | accessibility | Reads as a behaviour toggle | +| `.no-print` | print | Reads as a behaviour toggle | +| `.print-only` | print | Reads as a behaviour toggle | +| `.print-color-exact` | print | Self-documenting intent | +| `.print-no-color` | print | Self-documenting intent | +| `.theme-transition` | themes | Scoped opt-in helper | + --- ## Specificity @@ -381,6 +400,14 @@ CSS is emitted). `utilities.css` ships as an empty stub. Consumers can also build à la carte: `essential` (or raw `core/`) plus hand-picked optional files. +### Flat bundles + +For consumers who want a single file with no decisions, `dist/slashed.full.css` +includes everything (core + all optional files that contain active rules). +Because cascade layers govern specificity, concatenation order inside a flat +bundle is irrelevant -- `core/layers.css` fixes the priority order once at the +top. + --- ## Known intentional tradeoffs @@ -405,6 +432,11 @@ These behaviors are deliberate. Documented here so they aren't mistaken for bugs tints and base *into* text for shades, so it is surface-relative, not a perceptual lightness ramp. `base-600` can be lighter than `base-400`. This is intentional and differs from the conventional monotonic lightness ramp. +- **Palette token values are not part of the public API.** The *names* + (`--sf-color-primary-light`, `--sf-color-action-hover`, etc.) are stable and + covered by SemVer. The *computed colour values* they resolve to may shift + between minor releases as derivation formulas are refined. Pin your own + overrides to the 6 source tokens if you need colour stability. --- diff --git a/docs/browser-support.md b/docs/browser-support.md index 102ca1d8..baa6fc96 100644 --- a/docs/browser-support.md +++ b/docs/browser-support.md @@ -25,7 +25,7 @@ The effective floor is the **maximum** of these per engine: Firefox 128 |---|---| | Cascade layers (`@layer`) | foundational — the whole architecture; no fallback possible | | Container queries (`@container`) | all responsive primitives; baseline 2023 | -| `:has()` | required-field marker, `.focus-parent` | +| `:has()` | required-field marker, `.sf-focus-parent` | | `text-wrap: balance/pretty` | headings / paragraphs (degrades to normal) | | `interpolate-size` | wrapped in `@supports`; progressive enhancement only | | `@starting-style`, view transitions | opt-in, `@supports`-gated where used | diff --git a/docs/demo.html b/docs/demo.html index 37fba8f1..8b90ce56 100644 --- a/docs/demo.html +++ b/docs/demo.html @@ -1408,8 +1408,8 @@

Interactivity

.is-pressed .is-focused -
- .focus-parent (focus the input → parent rings) +
+ .sf-focus-parent (focus the input → parent rings)
@@ -2540,7 +2540,7 @@

.sf-clickable-parent Automatic mode — one link, no extra attributes. Click anywhere on the card to navigate; the button remains independently clickable. Tab to the card to see the built-in card-level focus ring - (no separate focus-parent recipe needed). + (no separate sf-focus-parent recipe needed).

Click anywhere on this card to navigate

diff --git a/docs/layout.md b/docs/layout.md index 394ba205..ea7b66fe 100644 --- a/docs/layout.md +++ b/docs/layout.md @@ -36,13 +36,10 @@ All primitives are demoed in [`demo.html`](demo.html). | `.sf-icon` | em-based inline icon sizing; `--xs … --xl` | `--sf-icon-*` | | `.sf-icon--boxed` | padded, bordered icon frame (content-box sizing) | `--sf-icon-box-pad`, `--sf-icon-box-radius`, `--sf-icon-box-bg`, `--sf-icon-box-border` | -## Size-modifier scale — `xs..2xl` everywhere (one exception) +## Size-modifier scale -- `xs..2xl` everywhere Every size-aware primitive supports the canonical range -`--xs --s --m --l --xl --2xl` with one documented exception: `.sf-icon` is -capped at `--xl` (3em ≈ 48px on default text). Above that the element ceases -to be an icon and becomes an illustration — the cap is semantic, not -arbitrary. +`--xs --s --m --l --xl --2xl`. One rule: **xs..2xl wszedzie.** | primitive | what the suffix scales | `--xs` | `--s` | `--m` *(default)* | `--l` | `--xl` | `--2xl` | |---|---|:-:|:-:|:-:|:-:|:-:|:-:| @@ -50,7 +47,7 @@ arbitrary. | `.sf-cluster` | inline-axis gap | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | | `.sf-grid` | min column width | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | | `.sf-section` | block padding | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | -| `.sf-icon` | font-size | ✓ | ✓ | ✓ | ✓ | ✓ | — | +| `.sf-icon` | font-size | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | Three suffixes scale different physical dimensions. `.sf-grid--xs` reads as "narrow column tier" (denser layouts), not "small gap" — gap on `.sf-grid` is diff --git a/docs/migration.md b/docs/migration.md index f396ce15..7ec438b0 100644 --- a/docs/migration.md +++ b/docs/migration.md @@ -16,7 +16,7 @@ live in changes. Most consumers won't notice. | # | Class / file | Was (0.2.x) | Now (0.3.0) | |---|---|---|---| | 1 | `.sf-prose`, `.sf-not-prose` | `slashed.layout` (in `core/layout.css`) | `slashed.macros` (in `core/macros.css`) | -| 2 | `.focus-parent` | `slashed.states` (in `core/states.css`) | `slashed.accessibility` (in `core/accessibility.css`) | +| 2 | `.sf-focus-parent` | `slashed.states` (in `core/states.css`) | `slashed.accessibility` (in `core/accessibility.css`) | | 3 | new layer | — | `slashed.macros` between `components` and `utilities` | ### Are these breaking changes? @@ -34,7 +34,7 @@ You only see a difference if your CSS targeted these classes from The fix is to move the override into `slashed.overrides` (the documented escape hatch, last in the cascade) — that's been the recommended path all along. -- A `@layer slashed.states { .focus-parent { … } }` override stops +- A `@layer slashed.states { .sf-focus-parent { … } }` override stops winning over the framework's rule. Same fix: move it to `slashed.overrides`. diff --git a/docs/motion.md b/docs/motion.md new file mode 100644 index 00000000..c5fde899 --- /dev/null +++ b/docs/motion.md @@ -0,0 +1,168 @@ +# Motion + +SLASHED ships animation primitives in the `slashed.motion` layer +(`core/motion.css`). Everything is gated behind +`@media (prefers-reduced-motion: no-preference)` -- users who prefer +reduced motion see no animation from the framework. + +All animations are demoed in [`demo.html`](demo.html). + +## Design principles + +1. **Token-driven.** Every duration, easing, and named animation is a + custom property -- override per-instance without new CSS. +2. **Opt-in classes.** Nothing animates until you add a class (or + consume a transition token in your own rule). +3. **Reduced-motion first.** The entire layer is wrapped in + `@media (prefers-reduced-motion: no-preference)`. Remove the class + or set `.no-motion` on an ancestor to suppress individually. +4. **Composable.** Entrance classes can be combined with delay tokens. + +## Transition tokens + +Declared in `core/tokens.css` (layer `slashed.tokens`). Apply them in +your own components: + +```css +.card { transition: var(--sf-transition-colors); } +``` + +| Token | Properties | Duration | Easing | +|---|---|---|---| +| `--sf-transition-all` | all | normal | ease-out | +| `--sf-transition-colors` | color, background-color, border-color, text-decoration-color, fill, stroke | normal | ease-out | +| `--sf-transition-transform` | transform | normal | ease-out | +| `--sf-transition-opacity` | opacity | normal | ease-out | +| `--sf-transition-shadow` | box-shadow | normal | ease-out | +| `--sf-transition-fast` | all | fast | ease-out | +| `--sf-transition-slow` | all | slow | ease-out | +| `--sf-transition-enter` | all | normal | ease-out | +| `--sf-transition-exit` | all | fast | ease-out | + +## Duration tokens + +| Token | Default value | +|---|---| +| `--sf-duration-none` | `0ms` | +| `--sf-duration-instant` | `50ms` | +| `--sf-duration-fast` | `150ms` | +| `--sf-duration-normal` | `300ms` | +| `--sf-duration-slow` | `500ms` | +| `--sf-duration-slower` | `800ms` | + +## Easing tokens + +| Token | Value | +|---|---| +| `--sf-ease-linear` | `linear` | +| `--sf-ease-in` | `cubic-bezier(0.4, 0, 1, 1)` | +| `--sf-ease-out` | `cubic-bezier(0, 0, 0.2, 1)` | +| `--sf-ease-in-out` | `cubic-bezier(0.4, 0, 0.2, 1)` | +| `--sf-ease-bounce` | `cubic-bezier(0.34, 1.56, 0.64, 1)` | +| `--sf-ease-elastic` | `cubic-bezier(0.68, -0.6, 0.32, 1.6)` | +| `--sf-ease-overshoot` | `cubic-bezier(0.22, 1.3, 0.36, 1)` | +| `--sf-ease-spring` | `linear(...)` (spring approximation) | + +## Animation classes + +### Entrance animations + +Apply one of these classes to trigger a one-shot entrance animation. +Combine with a `--sf-animation-delay-*` token for staggered sequences. + +| Class | Effect | +|---|---| +| `.sf-entrance--fade` | fade in | +| `.sf-entrance--fade-up` | fade in + slide up | +| `.sf-entrance--fade-down` | fade in + slide down | +| `.sf-entrance--fade-left` | fade in + slide from left | +| `.sf-entrance--fade-right` | fade in + slide from right | +| `.sf-entrance--scale-up` | fade in + scale from 95% | + +### Looping animations + +| Class | Effect | +|---|---| +| `.sf-fade-in` | fade in (fill forwards) | +| `.sf-fade-out` | fade out (fill forwards) | +| `.sf-scale-up` | scale from 0 to 1 | +| `.sf-scale-down` | scale from 1 to 0 | +| `.sf-slide-in-up` | slide from below | +| `.sf-slide-in-down` | slide from above | +| `.sf-slide-in-left` | slide from left | +| `.sf-slide-in-right` | slide from right | + +### Special + +| Class | Effect | +|---|---| +| `.sf-color-pulse` | animates `--sf-color-primary-light` lightness via `@property` interpolation in oklch | + +## Keyframes + +All keyframe names use the `sf-` prefix: + +- `sf-fade-in`, `sf-fade-out` +- `sf-scale-up`, `sf-scale-down` +- `sf-slide-in-up`, `sf-slide-in-down`, `sf-slide-in-left`, `sf-slide-in-right` +- `sf-float`, `sf-ping`, `sf-spin`, `sf-shimmer`, `sf-blink` +- `sf-color-pulse` + +## Stagger delays + +Use the delay tokens to stagger children: + +```html +
First
+
Second
+
Third
+``` + +| Token | Value | +|---|---| +| `--sf-animation-delay-1` | `50ms` | +| `--sf-animation-delay-2` | `100ms` | +| `--sf-animation-delay-3` | `150ms` | +| `--sf-animation-delay-4` | `200ms` | +| `--sf-animation-delay-5` | `250ms` | + +## Motion scale + +`--sf-motion-scale` (default `1`) multiplies every duration in the +motion layer. Set it to `0` to kill all framework animation globally, +or to `1.5` to slow everything down for debugging. + +```css +:root { --sf-motion-scale: 0; } /* kill all motion */ +``` + +## Reduced motion + +The entire `slashed.motion` layer is wrapped in: + +```css +@media (prefers-reduced-motion: no-preference) { … } +``` + +Additionally, `core/accessibility.css` sets a hard override: + +```css +@media (prefers-reduced-motion: reduce) { + *, *::before, *::after { + animation-duration: 0.01ms !important; + animation-iteration-count: 1 !important; + transition-duration: 0.01ms !important; + scroll-behavior: auto !important; + } +} +``` + +The `.no-motion` class provides per-element suppression independent of +the OS preference. + +## Theme transitions + +For smooth colour transitions when toggling light/dark mode, use the +`.theme-transition` helper class (defined in `optional/theme-example.css`). +Add it to `` during the toggle, remove it once the transition +completes. See [theming.md](theming.md) for the full pattern. diff --git a/docs/registry.json b/docs/registry.json index 244ba51a..101774db 100644 --- a/docs/registry.json +++ b/docs/registry.json @@ -13,12 +13,14 @@ "core/states.css", "core/accessibility.css", "core/motion.css", + "core/print.css", "optional/forms.css", - "optional/components.css" + "optional/components.css", + "optional/theme-example.css" ], "counts": { - "tokens": 622, - "sf_classes": 156, + "tokens": 623, + "sf_classes": 158, "is_classes": 40 } }, @@ -480,6 +482,7 @@ "--sf-heading-color", "--sf-heading-font-family", "--sf-heading-text-wrap", + "--sf-icon-2xl", "--sf-icon-box-bg", "--sf-icon-box-border", "--sf-icon-box-pad", @@ -700,6 +703,7 @@ "sf-fade-in", "sf-fade-out", "sf-flow", + "sf-focus-parent", "sf-frame", "sf-frame--3-2", "sf-frame--4-3", @@ -735,6 +739,7 @@ "sf-grid-4", "sf-grid-6", "sf-icon", + "sf-icon--2xl", "sf-icon--boxed", "sf-icon--l", "sf-icon--m", @@ -845,5 +850,17 @@ "is-valid", "is-visible", "is-warning" + ], + "unprefixed_classes": [ + "no-motion", + "no-print", + "print-color-exact", + "print-no-color", + "print-only", + "skip-link", + "sr-only", + "sr-only-focusable", + "theme-transition", + "visually-hidden" ] } diff --git a/docs/states.md b/docs/states.md index e50c30da..e8e93861 100644 --- a/docs/states.md +++ b/docs/states.md @@ -11,8 +11,8 @@ All states are exercised in [`demo.html`](demo.html). ## Reference > **Layer note:** Most state classes live in `core/states.css` (layer -> `slashed.states`). Three accessibility-related entries (`.no-motion`, -> `.sr-only-focusable`, `.focus-parent`) moved to `core/accessibility.css` +> `slashed.states`). Two accessibility-related entries (`.no-motion`, +> `.sr-only-focusable`) moved to `core/accessibility.css` > (layer `slashed.accessibility`) in v0.3.0 for better layering priority. | Class | Use when | ARIA / pairing | Layer | @@ -48,7 +48,7 @@ All states are exercised in [`demo.html`](demo.html). | `.sr-only-focusable` | hidden until focused (skip-link pattern) | — | accessibility | | `.no-motion` | kill all animation/transition on this subtree | `prefers-reduced-motion` equivalent | accessibility | -`.focus-parent` (no `is-`/`sf-` prefix) is a helper: a container with it rings +`.sf-focus-parent` is a helper: a container with it rings when any descendant has keyboard focus (`:focus-within`). It lives in `core/accessibility.css` (layer `slashed.accessibility`). diff --git a/optional/forms.css b/optional/forms.css index d1a423cb..d4afbf6e 100644 --- a/optional/forms.css +++ b/optional/forms.css @@ -15,11 +15,13 @@ full control via BEM components, skip this file entirely. TOKENS CONSUMED: - --sf-radius-m, --sf-border-width-1, --sf-color-border, + --sf-radius-m, --sf-radius-full, --sf-border-width-1, + --sf-border-width-3, --sf-color-border, --sf-color-border--focus, --sf-color-border--subtle, --sf-color-surface, --sf-color-text, --sf-color-text--placeholder, --sf-color-text--disabled, --sf-color-text--on-action, - --sf-color-bg--disabled, --sf-color-action, --sf-color-error, + --sf-color-bg--disabled, --sf-color-action, --sf-color-action-hover, + --sf-color-error, --sf-field-border-color, --sf-opacity-disabled, --sf-space-xs, --sf-space-s, --sf-space-m, --sf-text-m, --sf-leading-normal, --sf-font-weight-heading, diff --git a/optional/theme-example.css b/optional/theme-example.css index 4c8075d8..386b55b6 100644 --- a/optional/theme-example.css +++ b/optional/theme-example.css @@ -2,47 +2,114 @@ SLASHED — optional/theme-example.css (COPY & CUSTOMISE) @layer slashed.themes - A worked example of the "rebrand in 6 tokens" workflow. This file - is NOT part of any bundle — copy it into your project, rename it, - and edit the values. Everything else (dark mode, surfaces, text, - status tints, on-colours) auto-derives from these source tokens. + Maximal reference theme. This file demonstrates EVERY theming + mechanism the framework supports — it is intentionally verbose. + Copy it, delete what you don't need, and rename it. + + NOT part of any bundle — this is a docs/reference file only. + + Sections: + 1. Quick rebrand (6 source tokens) + 2. Full dark-mode override (optional) + 3. Multi-brand scoping + 4. Component-level token overrides (commented examples) + 5. Smooth theme-transition helper + 6. Full token reference (commented out) See README "Customising tokens" and docs/theming.md. ============================================================ */ @layer slashed.themes { - /* ---- 1. Quick rebrand: override the 6 -light source tokens ---- + /* =========================================================== + 1. QUICK REBRAND — override the 6 -light source tokens + =========================================================== Dark mode auto-derives via relative colour syntax. Any valid CSS - colour works (hex, oklch, hsl, …). */ + colour works (hex, oklch, hsl, ...). These six tokens propagate + to every derived colour in the system. */ + :root { --sf-color-primary-light: oklch(0.55 0.18 280); /* brand / links */ - --sf-color-secondary-light: oklch(0.30 0.04 280); /* muted brand */ - --sf-color-tertiary-light: oklch(0.62 0.15 200); /* accent */ - --sf-color-action-light: oklch(0.62 0.16 150); /* primary CTA */ - --sf-color-neutral-light: oklch(0.45 0.01 280); /* greys / text base */ - --sf-color-base-light: oklch(0.99 0.004 280); /* page surface */ + --sf-color-secondary-light: oklch(0.30 0.04 280); /* muted brand */ + --sf-color-tertiary-light: oklch(0.62 0.15 200); /* accent */ + --sf-color-action-light: oklch(0.62 0.16 150); /* primary CTA */ + --sf-color-neutral-light: oklch(0.45 0.01 280); /* greys / text base */ + --sf-color-base-light: oklch(0.99 0.004 280); /* page surface */ } - /* ---- 2. Optional: take full control of dark mode ---- - Only needed when the auto-derived dark value isn't what you want. */ + /* =========================================================== + 2. FULL DARK-MODE OVERRIDE (optional) + =========================================================== + Only needed when the auto-derived dark value isn't what you want. + Most brands work fine with auto-derivation — only override if the + computed dark-mode colour is off. */ + :root { - --sf-color-primary-dark: oklch(0.78 0.16 280); + --sf-color-primary-dark: oklch(0.78 0.16 280); + /* --sf-color-secondary-dark: oklch(…); */ + /* --sf-color-tertiary-dark: oklch(…); */ + /* --sf-color-action-dark: oklch(…); */ + /* --sf-color-neutral-dark: oklch(…); */ + /* --sf-color-base-dark: oklch(…); */ } - /* ---- 3. Multi-brand: scope a different palette to a subtree ---- + /* =========================================================== + 3. MULTI-BRAND SCOPING + =========================================================== Nest [data-brand] (or any selector) and re-declare the 6 tokens. - The architecture allows unlimited scoped palettes. */ + The architecture allows unlimited scoped palettes — downstream + colours re-derive automatically within the subtree. */ + [data-brand="sunset"] { - --sf-color-primary-light: oklch(0.62 0.20 35); - --sf-color-action-light: oklch(0.70 0.18 60); + --sf-color-primary-light: oklch(0.62 0.20 35); + --sf-color-secondary-light: oklch(0.50 0.12 30); + --sf-color-tertiary-light: oklch(0.55 0.18 60); + --sf-color-action-light: oklch(0.70 0.18 60); + --sf-color-neutral-light: oklch(0.40 0.02 35); + --sf-color-base-light: oklch(0.98 0.005 35); + } + + [data-brand="ocean"] { + --sf-color-primary-light: oklch(0.50 0.14 230); + --sf-color-secondary-light: oklch(0.40 0.08 230); + --sf-color-tertiary-light: oklch(0.58 0.12 180); + --sf-color-action-light: oklch(0.55 0.16 200); + --sf-color-neutral-light: oklch(0.42 0.01 230); + --sf-color-base-light: oklch(0.98 0.003 230); } - /* ---- 4. Optional: smooth colour change when toggling themes ---- + /* =========================================================== + 4. COMPONENT-LEVEL TOKEN OVERRIDES (examples) + =========================================================== + When components ship, you can fine-tune their tokens here + without touching their source. Uncomment and adjust as needed. */ + + /* :root { + --sf-button-radius: var(--sf-radius-full); + --sf-button-font-weight: var(--sf-font-weight-semibold); + --sf-card-radius: var(--sf-radius-l); + --sf-card-shadow: var(--sf-shadow-m); + --sf-badge-radius: var(--sf-radius-full); + --sf-avatar-radius: var(--sf-radius-full); + } */ + + /* =========================================================== + 5. SMOOTH THEME-TRANSITION HELPER + =========================================================== Opt-in only — transitioning every colour globally is a perf footgun, so it's scoped to a class you add to while a - toggle is in flight (remove it afterwards). Pairs with the - ::view-transition(root) animation already shipped in motion.css. */ + toggle is in flight (remove it afterwards via setTimeout or + transitionend). Pairs with the ::view-transition(root) + animation already shipped in motion.css. + + Usage: + document.documentElement.classList.add('theme-transition'); + document.documentElement.dataset.theme = newTheme; + setTimeout(() => { + document.documentElement.classList.remove('theme-transition'); + }, 350); + */ + .theme-transition, .theme-transition * { transition: @@ -51,4 +118,145 @@ border-color var(--sf-duration-normal) var(--sf-ease-out); } + /* =========================================================== + 6. FULL TOKEN REFERENCE (commented out) + =========================================================== + Every overridable token in the system, grouped by concern. + Uncomment individual lines to override. Values shown are the + defaults — change them to your brand values. + + NOTE: Palette token VALUES are not part of the public API (they + may shift between minors). The NAMES are stable and SemVer'd. + */ + + /* :root { */ + + /* --- Brand sources (the 6 master tokens) --- + --sf-color-primary-light: oklch(0.55 0.22 265); + --sf-color-secondary-light: oklch(0.30 0.04 265); + --sf-color-tertiary-light: oklch(0.62 0.15 145); + --sf-color-action-light: oklch(0.55 0.22 265); + --sf-color-neutral-light: oklch(0.45 0.01 265); + --sf-color-base-light: oklch(0.985 0.004 265); + */ + + /* --- Typography --- + --sf-body-font-family: system-ui, sans-serif; + --sf-heading-font-family: inherit; + --sf-font-mono: ui-monospace, monospace; + --sf-body-font-size: 100%; + --sf-body-line-height: 1.6; + --sf-body-font-weight: 400; + --sf-body-strong-weight: 700; + --sf-heading-text-wrap: balance; + --sf-body-text-wrap: pretty; + --sf-text-scale: 1; + */ + + /* --- Text sizes (fluid clamp) --- + --sf-text-2xs: clamp(...); + --sf-text-xs: clamp(...); + --sf-text-s: clamp(...); + --sf-text-m: clamp(...); + --sf-text-l: clamp(...); + --sf-text-xl: clamp(...); + --sf-text-2xl: clamp(...); + --sf-text-3xl: clamp(...); + --sf-text-4xl: clamp(...); + */ + + /* --- Spacing (fluid clamp * scale) --- + --sf-space-scale: 1; + --sf-space-gutter: var(--sf-space-l); + */ + + /* --- Radii --- + --sf-radius-none: 0; + --sf-radius-xs: 0.125rem; + --sf-radius-s: 0.25rem; + --sf-radius-m: 0.5rem; + --sf-radius-l: 0.75rem; + --sf-radius-xl: 1rem; + --sf-radius-2xl: 1.25rem; + --sf-radius-3xl: 1.5rem; + --sf-radius-4xl: 2rem; + --sf-radius-full: 9999px; + --sf-radius-pill: 9999px; + --sf-radius-scale: 1; + */ + + /* --- Shadows --- + --sf-shadow-color: oklch(0.25 0 0 / 0.1); + --sf-shadow-strength: 1; + --sf-shadow-xs: ...; + --sf-shadow-s: ...; + --sf-shadow-m: ...; + --sf-shadow-l: ...; + --sf-shadow-xl: ...; + --sf-shadow-2xl: ...; + --sf-shadow-inner: ...; + --sf-shadow-glow: ...; + */ + + /* --- Borders --- + --sf-border-width-1: 1px; + --sf-border-width-2: 2px; + --sf-border-width-3: 3px; + --sf-border-width-4: 4px; + --sf-border-style: solid; + --sf-border-style-strong: solid; + --sf-border-style-soft: solid; + --sf-border-style-dotted: dotted; + */ + + /* --- Motion --- + --sf-duration-fast: 150ms; + --sf-duration-normal: 300ms; + --sf-duration-slow: 500ms; + --sf-motion-scale: 1; + */ + + /* --- Layout --- + --sf-container-default: 75rem; + --sf-container-narrow: 55rem; + --sf-container-wide: 90rem; + --sf-container-prose: 65ch; + --sf-header-height: 4rem; + */ + + /* --- Focus ring --- + --sf-focus-ring-width: 3px; + --sf-focus-ring-style: solid; + --sf-focus-ring-offset: 2px; + --sf-focus-ring-color: var(--sf-color-border--focus); + */ + + /* --- Z-index scale --- + --sf-z-below: -1; + --sf-z-base: 0; + --sf-z-low: 10; + --sf-z-raised: 20; + --sf-z-mid: 50; + --sf-z-high: 100; + --sf-z-top: 500; + --sf-z-max: 9999; + */ + + /* --- Icon --- + --sf-icon-xs: 0.875em; + --sf-icon-s: 1em; + --sf-icon-m: 1.5em; + --sf-icon-l: 2em; + --sf-icon-xl: 3em; + --sf-icon-2xl: 4em; + */ + + /* --- Print --- + --sf-print-page-size: A4; + --sf-print-page-margin: 2cm; + --sf-print-base-size: 12pt; + */ + + /* } */ + } diff --git a/tests/layers.spec.js b/tests/layers.spec.js index 5fdd1cc7..aca130e8 100644 --- a/tests/layers.spec.js +++ b/tests/layers.spec.js @@ -105,7 +105,7 @@ test.describe('Layer ordering', () => { test('slashed.accessibility wins over slashed.motion at equal specificity', async ({ page }) => { // a11y rules are placed near the end of the cascade so they can // recover focus rings, sr-only positioning, and the relocated - // .focus-parent / .sf-clickable-parent patterns. + // .sf-focus-parent / .sf-clickable-parent patterns. await page.setContent(`