diff --git a/README.md b/README.md index c5c57705..2876ca4d 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@
A cascade-layer CSS framework you never compile.
- 747 design tokens, automatic dark mode, fluid type & spacing — no build step, no Node, no JavaScript.
+ 741 design tokens, automatic dark mode, fluid type & spacing — no build step, no Node, no JavaScript.
@@ -67,7 +67,7 @@ Every letter of SLASHED is a design commitment, and each one is checkable agains
## What's inside
-- **747 design tokens** (`--sf-*`) covering color, typography, spacing, layout, borders, shadows, motion, and z-index — catalogued in a machine-readable [API index](docs/api-index.json).
+- **741 design tokens** (`--sf-*`) covering color, typography, spacing, layout, borders, shadows, motion, and z-index — catalogued in a machine-readable [API index](docs/api-index.json).
- **Layout primitives** — `.sf-container`, `.sf-stack`, `.sf-grid`, `.sf-cluster`, `.sf-sidebar`, `.sf-switcher`, `.sf-cover`, `.sf-center`, `.sf-frame`, `.sf-reel`, `.sf-bento`, and more ([docs/layout.md](docs/layout.md)).
- **Macros** — recipes like `.sf-prose`, `.sf-flow`, `.sf-truncate`, `.sf-aspect`, `.sf-scroll-shadow`, `.sf-surface` ([docs/macros.md](docs/macros.md)).
- **State classes** — `.sf-is-*` classes mapped to ARIA semantics ([docs/states.md](docs/states.md)).
diff --git a/badges/badge-optimal.json b/badges/badge-optimal.json
index 31ef54a3..99f96683 100644
--- a/badges/badge-optimal.json
+++ b/badges/badge-optimal.json
@@ -1,7 +1,7 @@
{
"schemaVersion": 1,
"label": "optimal",
- "message": "19.6 kB gzip",
+ "message": "19.4 kB gzip",
"color": "brightgreen",
"namedLogo": "css3"
}
diff --git a/configurator/src/components/panels/EffectsPanel.svelte b/configurator/src/components/panels/EffectsPanel.svelte
index 58d5487c..808b867d 100644
--- a/configurator/src/components/panels/EffectsPanel.svelte
+++ b/configurator/src/components/panels/EffectsPanel.svelte
@@ -28,7 +28,6 @@
let blur = $derived(parseNum(overrides["--sf-blur"], 12, "px"));
let opacityMuted = $derived(parseNum(overrides["--sf-opacity-muted"], 0.5));
let opacityDisabled = $derived(parseNum(overrides["--sf-opacity-disabled"], 0.45));
- let pendingOpacity = $derived(parseNum(overrides["--sf-state-pending-opacity"], 0.7));
let scrollbarThumb = $derived(overrides["--sf-scrollbar-thumb"] ?? "");
let scrollbarTrack = $derived(overrides["--sf-scrollbar-track"] ?? "");
@@ -84,19 +83,11 @@
onChange={(v) => onSet("--sf-opacity-disabled", String(v))}
onReset={() => onReset("--sf-opacity-disabled")}
/>
- Design tokens 7
}
function openNav() {
- nav.classList.add('sf-is-open');
- overlay.classList.add('sf-is-open');
+ nav.classList.add('is-open');
+ overlay.classList.add('is-open');
navToggle.setAttribute('aria-expanded', 'true');
navToggle.setAttribute('aria-label', 'Close navigation');
document.body.style.overflow = 'hidden';
@@ -5335,8 +5269,8 @@
Design tokens 7
}
function closeNav() {
- nav.classList.remove('sf-is-open');
- overlay.classList.remove('sf-is-open');
+ nav.classList.remove('is-open');
+ overlay.classList.remove('is-open');
navToggle.setAttribute('aria-expanded', 'false');
navToggle.setAttribute('aria-label', 'Open navigation');
document.body.style.overflow = '';
@@ -5349,7 +5283,7 @@
Design tokens 7
}
navToggle.addEventListener('click', () => {
- nav.classList.contains('sf-is-open') ? closeNav() : openNav();
+ nav.classList.contains('is-open') ? closeNav() : openNav();
});
overlay.addEventListener('click', closeNav);
@@ -5361,7 +5295,7 @@
Design tokens 7
});
document.addEventListener('keydown', e => {
- if (e.key === 'Escape' && nav.classList.contains('sf-is-open')) closeNav();
+ if (e.key === 'Escape' && nav.classList.contains('is-open')) closeNav();
});
// Replay an animation preset on click by re-triggering its animation.
diff --git a/dist/css-custom-data.json b/dist/css-custom-data.json
index eba9a848..33cc860a 100644
--- a/dist/css-custom-data.json
+++ b/dist/css-custom-data.json
@@ -1752,8 +1752,8 @@
"syntax": "*"
},
{
- "name": "--sf-color-text--secondary",
- "description": "De-emphasised secondary text (captions, labels, supporting copy).\n\nDefault: `light-dark( oklch(from var(--sf-color-neutral-source-light) clamp(0.15, calc(l - 0.25 - var(--sf-contrast-bias)), 0.45) c h), oklch(from var(--sf-color-neutral) clamp(0.55, calc(l + 0.1 + var(--sf-contrast-bias)), 0.90) c h) )`",
+ "name": "--sf-color-text--subtle",
+ "description": "De-emphasised, lower-emphasis text (captions, labels, supporting copy). Second tier below --sf-color-text; sits between it and --sf-color-text--muted.\n\nDefault: `light-dark( oklch(from var(--sf-color-neutral-source-light) clamp(0.15, calc(l - 0.25 - var(--sf-contrast-bias)), 0.45) c h), oklch(from var(--sf-color-neutral) clamp(0.55, calc(l + 0.1 + var(--sf-contrast-bias)), 0.90) c h) )`",
"syntax": "*"
},
{
@@ -1861,11 +1861,6 @@
"description": "Inner padding for the cover layout.\n\nDefault: `var(--sf-section-pad)`",
"syntax": "*"
},
- {
- "name": "--sf-current-font-weight",
- "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)",
@@ -2556,26 +2551,6 @@
"description": "Margin between the imposter overlay and the viewport edges.\n\nDefault: `var(--sf-space-m)`",
"syntax": "*"
},
- {
- "name": "--sf-is-active",
- "description": "Integer state flag for the active state. Set to 1 (via JS or .sf-is-active) to drive branching calc() expressions. 0 = default (inactive).\n\nDefault: `0`\n\n(advanced token)",
- "syntax": "
/
(or element with list-style applied). |
| `.sf-marker--action` | PUBLIC | utility | LIST MARKER COLOUR (.sf-marker--*) | Colours list ::markers with the action colour. |
| `.sf-marker--primary` | PUBLIC | utility | LIST MARKER COLOUR (.sf-marker--*) | Colours list ::markers with the primary brand colour. |
@@ -1161,4 +1143,5 @@ and a short description. The machine-readable companion (with all columns) is
| `.sf-text-s` | PUBLIC | utility | TEXT-SIZE UTILITIES (.sf-text-2xs | Applies the s text-size role: font-size, line-height, font-weight, letter-spacing, and measure (max-inline-size) in one class. |
| `.sf-text-xl` | PUBLIC | utility | TEXT-SIZE UTILITIES (.sf-text-2xs | Applies the xl text-size role: font-size, line-height, font-weight, letter-spacing, and measure (max-inline-size) in one class. |
| `.sf-text-xs` | PUBLIC | utility | TEXT-SIZE UTILITIES (.sf-text-2xs | Applies the xs text-size role: font-size, line-height, font-weight, letter-spacing, and measure (max-inline-size) in one class. |
+| `.sf-visible` | PUBLIC | utility | VISIBILITY HELPERS (.sf-invisible / .sf-visible) | Visibility helper (optional/utilities.css) — forces the element to be visible (visibility: visible). Use to un-hide an element that inherited visibility: hidden from a parent. |
diff --git a/docs/architecture.md b/docs/architecture.md
index 6db9a1f0..b2573695 100644
--- a/docs/architecture.md
+++ b/docs/architecture.md
@@ -180,10 +180,14 @@ not carry a broad component library. Every value goes through `var()`. Requires
`.sf-overflow-fade`, `.sf-no-tap-highlight`. Tokens for these classes
live in `core/tokens.macros.css`.
-**slashed.utilities** — single-purpose helpers. SLASHED ships no
-utility classes in 0.x; the layer slot is reserved for the future.
+**slashed.utilities** — single-purpose helpers. SLASHED is BEM-first and
+ships only a curated subset in 0.x (heading, text-size, hover-transform,
+list-reset, marker-colour, alternate-selection, sticky, and visibility —
+see `optional/utilities.css`'s header comment for the current active set);
+most definitions in the file are staged (commented out) behind that same
+curation call.
-**slashed.states** — `.sf-is-*` markers. Exclusive prefix — utilities never use it. `.sf-is-current` exposes `--sf-current-font-weight` (defaults to `--sf-font-weight-bold`) for consumers to override without specificity battles.
+**slashed.states** — `.sf-is-*` markers. Exclusive prefix — utilities never use it (visibility helpers that don't encode a runtime condition, like `.sf-invisible`/`.sf-visible`, live in `slashed.utilities` instead — see [states.md](states.md) § "Prefer native state").
**slashed.themes** — token reassignments only. Lives in `core/themes.css`. Holds `@media (prefers-color-scheme: dark)` and the `[data-theme="light|dark"]` selectors that flip `color-scheme` and `--sf-is-dark`. Sits above `slashed.{states, utilities, components}` so theme overrides cannot be beaten by an equal-specificity component or utility rule. Consumers can extend this layer with `forced-colors` swaps, brand-palette scopes, or any other token-only reassignment (see `optional/customize-example.css`).
@@ -202,7 +206,7 @@ utility classes in 0.x; the layer slot is reserved for the future.
## Tokens
- Colors: `oklch()` with relative color syntax for derived values; `oklch(from …)` for semantic alpha variants (ghost/subtle/muted) in core; `color-mix(in oklab)` for numeric tints/shades in the optional palette; the optional palette's numeric alpha variants (-a5/-a10/-a30/-a50/-a80) are gated behind `@supports (color: oklch(from red l c h))`
-- `@property` registration for 20 source colours (10 `-source-light` + 10 `-source-dark`, 6 brand + 4 status) and 5 interaction-state integers (`--sf-is-dark`, `--sf-is-active`, `--sf-is-current`, `--sf-is-pressed`, `--sf-is-open`) — enables animation and typed `initial` reset
+- `@property` registration for 20 source colours (10 `-source-light` + 10 `-source-dark`, 6 brand + 4 status) and the `--sf-is-dark` mode-flag integer — enables animation and typed `initial` reset
- Sizing: `clamp(min, preferred, max)` — no bare viewport units in tokens
- Aliases: semantic tokens always reference palette tokens via `var()` — never literals
- Component tokens: always `var(--sf-*)` — never literals
diff --git a/docs/classes.md b/docs/classes.md
index 67de1232..ba5e1ea9 100644
--- a/docs/classes.md
+++ b/docs/classes.md
@@ -3,7 +3,7 @@
> **Generated** from source by `scripts/gen-class-reference.js` —
> run `npm run docs:classes` to refresh. Do not edit by hand.
-**287 .sf-classes, 28 .sf-is-classes.** Every `.sf-*` layout/macro class and `.sf-is-*` state class.
+**289 .sf-classes, 15 .sf-is-classes.** Every `.sf-*` layout/macro class and `.sf-is-*` state class.
See [architecture.md](architecture.md) for layer order and naming conventions.
## Layout primitives (`core/layout.css`)
@@ -218,37 +218,24 @@ See [architecture.md](architecture.md) for layer order and naming conventions.
## State classes (`core/states.css`)
-28 classes.
+15 classes.
| Class |
|---|
-| `.sf-is-active` |
-| `.sf-is-busy` |
-| `.sf-is-collapsed` |
-| `.sf-is-current` |
-| `.sf-is-danger` |
| `.sf-is-disabled` |
| `.sf-is-draggable` |
| `.sf-is-dragging` |
| `.sf-is-drop-target` |
| `.sf-is-empty` |
| `.sf-is-error` |
-| `.sf-is-expanded` |
-| `.sf-is-hidden` |
| `.sf-is-highlighted` |
| `.sf-is-info` |
| `.sf-is-invalid` |
-| `.sf-is-invisible` |
| `.sf-is-loading` |
-| `.sf-is-open` |
-| `.sf-is-pending` |
-| `.sf-is-pressed` |
-| `.sf-is-readonly` |
| `.sf-is-selected` |
| `.sf-is-skeleton` |
| `.sf-is-success` |
| `.sf-is-valid` |
-| `.sf-is-visible` |
| `.sf-is-warning` |
## Accessibility (`core/accessibility.css`)
@@ -360,7 +347,7 @@ See [architecture.md](architecture.md) for layer order and naming conventions.
## Utilities (`optional/utilities.css`)
-32 classes.
+34 classes.
| Class |
|---|
@@ -376,6 +363,7 @@ See [architecture.md](architecture.md) for layer order and naming conventions.
| `.sf-hover-sink` |
| `.sf-hover-slide-end` |
| `.sf-hover-slide-start` |
+| `.sf-invisible` |
| `.sf-list-none` |
| `.sf-marker--action` |
| `.sf-marker--primary` |
@@ -396,4 +384,5 @@ See [architecture.md](architecture.md) for layer order and naming conventions.
| `.sf-text-s` |
| `.sf-text-xl` |
| `.sf-text-xs` |
+| `.sf-visible` |
diff --git a/docs/llm-guide.md b/docs/llm-guide.md
index f3d42edd..e2409cb2 100644
--- a/docs/llm-guide.md
+++ b/docs/llm-guide.md
@@ -1,6 +1,6 @@
# Slashed Framework — LLM Reference Guide
-> Version: **0.7.25** · Tokens: **747** · Prefix: `--sf-`
+> Version: **0.7.25** · Tokens: **741** · Prefix: `--sf-`
---
@@ -53,7 +53,7 @@ slashed/
│ ├── base.css # HTML typography, links, img/video
│ ├── layout.css # Primitive classes: stack, cluster, grid, sidebar…
│ ├── macros.css # Recipe classes: prose, surface, flow, scrim…
-│ ├── states.css # State classes: .sf-is-active, .sf-is-current…
+│ ├── states.css # State classes: .sf-is-disabled, .sf-is-loading…
│ ├── accessibility.css # Focus, skip links, screen-reader utilities
│ ├── motion.css # @keyframes for animations + view-transition
│ └── print.css # Print styles
@@ -218,7 +218,7 @@ Auto-switch between light and dark modes. These are the tokens you reference in
```css
--sf-color-text /* Primary body text */
--sf-color-text--muted /* Captions, metadata, helper labels */
---sf-color-text--secondary /* Between muted and primary */
+--sf-color-text--subtle /* Between muted and primary */
--sf-color-text--placeholder /* Form field placeholders */
--sf-color-text--disabled /* Disabled text */
--sf-color-text--inverse /* Light text on dark bg, and vice versa */
@@ -469,9 +469,6 @@ Status families (success/warning/info/danger) have **no numeric scale** — use
--sf-font-weight-display: var(--sf-font-weight-bold) /* hero */
--sf-font-weight-interactive: var(--sf-font-weight-semibold) /* buttons, nav */
--sf-font-weight-strong: var(--sf-font-weight-bold) /* */
-
-/* State token */
---sf-current-font-weight: var(--sf-font-weight-bold) /* .sf-is-current in nav */
```
### 6.4 Text sizes (fluid, generative)
@@ -1071,30 +1068,18 @@ Ready-made `animation` values — keyframe + duration + easing + fill-mode.
--sf-caret-color: var(--sf-color-action)
--sf-touch-target: 2.75rem /* 44px — WCAG 2.5.5, fixed independent of --sf-size-* */
--sf-opacity-disabled: 0.45
---sf-state-pending-opacity: 0.7 /* async operations */
--sf-opacity-muted: 0.5
--sf-blur: 12px /* frosted glass backdrop-filter */
```
-### 9.9 State flags (PUBLIC-ADVANCED — Style Queries)
-
-Used by `.sf-is-active`, `.sf-is-current`, etc. in `core/states.css`. Allow components to react to ancestor states via CSS Style Queries.
-
-```css
---sf-is-active: 0 /* 1 = active element (.sf-is-active) */
---sf-is-current: 0 /* 1 = current page (.sf-is-current in nav) */
---sf-is-pressed: 0 /* 1 = pressed */
---sf-is-open: 0 /* 1 = open (accordion, dropdown) */
-```
-
-### 9.10 Form field tokens
+### 9.9 Form field tokens
```css
--sf-field-required-marker: " *" /* required field marker */
--sf-link-external-marker: " ↗" /* marker for .sf-link-external */
/* Scoped override hooks — set per-field/form to override global borders/text */
---sf-field-border-color /* set by validation states (error/success/warning/info/danger); also set natively by :user-invalid/:user-valid within .sf-live-validate (opt-in — see below) */
+--sf-field-border-color /* set by validation states (error/success/warning/info); also set natively by :user-invalid/:user-valid within .sf-live-validate (opt-in — see below) */
--sf-field-text-color /* set by validation states for text color feedback */
```
diff --git a/docs/macros.md b/docs/macros.md
index c4503c0b..f8874ab2 100644
--- a/docs/macros.md
+++ b/docs/macros.md
@@ -230,7 +230,7 @@ target a specific edge or axis:
Suppresses the WebKit/Android grey tap-highlight overlay on
interactive elements where it conflicts with the framework's own
-`.sf-is-active` / hover treatment.
+`:active` / hover treatment.
```html
…
@@ -379,7 +379,7 @@ contract (shown here seeded from a custom foreground/background pair):
--sf-color-link: oklch(from var(--my-fg) l calc(c + 0.08) h);
--sf-color-link--hover: var(--my-fg);
--sf-color-link--underline: oklch(from var(--my-fg) l c h / 0.5);
- --sf-color-text--secondary: oklch(from var(--my-fg) l c h / 0.70);
+ --sf-color-text--subtle: oklch(from var(--my-fg) l c h / 0.70);
--sf-color-text--placeholder: oklch(from var(--my-fg) l c h / 0.45);
--sf-color-text--disabled: oklch(from var(--my-fg) l c h / 0.30);
--sf-color-border: oklch(from var(--my-fg) l c h / 0.20);
diff --git a/docs/migration.md b/docs/migration.md
index 9594d29d..895ba496 100644
--- a/docs/migration.md
+++ b/docs/migration.md
@@ -3,6 +3,82 @@
Mapping concepts from popular CSS frameworks to SLASHED, plus intra-project
upgrade notes.
+## SLASHED 0.7.25 → Unreleased
+
+### `--sf-color-text--secondary` renamed to `--sf-color-text--subtle` (breaking)
+
+The name collided conceptually with the brand-palette `secondary` role
+(`--sf-color-secondary`) and with the token's own `--sf-color-text--on-secondary`
+sibling, reading as "text in the secondary brand colour" when it actually means
+de-emphasised, lower-emphasis body text (captions, labels, supporting copy).
+`--subtle` pairs cleanly with the existing `--muted` tier and carries no
+brand-role ambiguity. Value, derivation, and role are unchanged — only the name
+moved.
+
+**What changed for you:** replace `var(--sf-color-text--secondary)` with
+`var(--sf-color-text--subtle)` anywhere you reference it directly. No
+compatibility alias is provided — see "State-class API reduction" below for
+why: for a pre-1.0 framework, an alias here would have perpetuated the exact
+naming collision that motivated the rename.
+
+### State-class API reduction (breaking)
+
+A full audit of every `.sf-is-*` class held each one to the bar "does a native
+CSS mechanism already cover this, and is there a real consumer" instead of
+"was this class documented." Eleven classes and six tokens didn't clear it and
+are removed; two classes were relocated, not cut.
+
+**Removed — duplicated a native mechanism, no distinct behaviour of their own:**
+
+| Removed class | Native replacement |
+|---|---|
+| `.sf-is-hidden` | `[hidden]` (`core/reset.css` already hardens it to the same `display: none !important`) |
+| `.sf-is-readonly` | `:read-only` (the class also incorrectly blocked text selection a real read-only field should allow) |
+| `.sf-is-busy` | `[aria-busy="true"]` (a single `cursor: progress` with no distinct visual) |
+
+**Removed — speculative `--sf-is-*` custom-property flags with zero consumers**
+anywhere in this codebase or in any of the comparable frameworks surveyed
+(Open Props, Pico.css, Bulma, Automatic.css ship nothing equivalent):
+`.sf-is-active`, `.sf-is-open`, `.sf-is-collapsed`, `.sf-is-expanded`,
+`.sf-is-pressed`, `.sf-is-current`. Style off the ARIA state you already need
+to set for accessibility instead: `[aria-current]`, `[aria-expanded]`,
+`[aria-selected]`, `[aria-pressed]`.
+
+**Removed — for other reasons:**
+- `.sf-is-danger` — identical implementation to `.sf-is-invalid`/`.sf-is-error`
+ (both only ever wrote `--sf-field-*`), so it never actually worked as the
+ "destructive-action button" case its own docs described, and visual variants
+ belong in your own component CSS per this same guide's "component/modifier
+ framework" section below.
+- `.sf-is-pending` — two lines (`opacity`, `cursor: progress`) cheap to
+ hand-roll, with `.sf-is-loading` already covering the common "mask with a
+ spinner" case.
+
+**Relocated, not cut** — single-property helpers with no runtime condition of
+their own (unlike `.sf-is-selected`, which paints something), matching
+Bulma's precedent of treating visibility as a helper, not a state:
+
+| Old name | New name | New location |
+|---|---|---|
+| `.sf-is-invisible` | `.sf-invisible` | `optional/utilities.css` |
+| `.sf-is-visible` | `.sf-visible` | `optional/utilities.css` |
+
+**Tokens removed as a consequence** — each lost its only consumer:
+`--sf-current-font-weight` (`.sf-is-current`'s font-weight hook),
+`--sf-state-pending-opacity` (`.sf-is-pending`'s dimming), and the
+`--sf-is-active`/`--sf-is-current`/`--sf-is-pressed`/`--sf-is-open`
+`@property` registrations (the removed flag mechanism above).
+
+**What changed for you:** if you toggle any of the eleven removed classes from
+your own JS, they're now inert — no CSS reacts to them. Swap to the native
+replacement in the first table, or to the matching ARIA attribute for the
+speculative-flag group, or hand-roll the one-line effect yourself for
+`.sf-is-danger`/`.sf-is-pending`. If you use `.sf-is-invisible`/`.sf-is-visible`,
+rename the class in your markup to `.sf-invisible`/`.sf-visible` and make sure
+you're on a bundle that includes `optional/utilities.css` (the `full` bundle,
+or your own custom build — the `optimal` bundle does not include it). Full
+rationale for every class: [states.md § "Prefer native state"](states.md).
+
## SLASHED 0.7.8 → 0.8.0
### `.sf-btn` size scale honoured on touch; blanket 44px floor no longer applies to buttons (breaking)
diff --git a/docs/ref-allowlist.json b/docs/ref-allowlist.json
index bfbc226d..03d148c5 100644
--- a/docs/ref-allowlist.json
+++ b/docs/ref-allowlist.json
@@ -25,5 +25,38 @@
"sf-alert": "Example of a component SLASHED deliberately does NOT ship.",
"sf-avatar": "Example of a component SLASHED deliberately does NOT ship.",
"sf-modal": "Example of a component SLASHED deliberately does NOT ship."
+ },
+ "docs/states.md": {
+ "sf-is-hidden": "Removed — duplicated [hidden] (core/reset.css already hardens it identically). Named in the 'Prefer native state' section as the precedent for the guidance.",
+ "sf-is-readonly": "Removed — duplicated :read-only and incorrectly blocked text selection. Named in the 'Prefer native state' section as a removed example.",
+ "sf-is-busy": "Removed — a single cursor:progress declaration with no distinct visual; [aria-busy=\"true\"] alone covers it. Named in the 'Prefer native state' section as a removed example.",
+ "sf-is-active": "Removed — speculative --sf-is-* custom-property flag with zero consumers. Named in the 'Prefer native state' section's removal rationale.",
+ "sf-is-open": "Removed — same speculative-flag rationale as sf-is-active. Named in the 'Prefer native state' section's removal rationale.",
+ "sf-is-collapsed": "Removed — same speculative-flag rationale as sf-is-active. Named in the 'Prefer native state' section's removal rationale.",
+ "sf-is-expanded": "Removed — pure vocabulary duplicate of sf-is-open. Named in the 'Prefer native state' section's removal rationale.",
+ "sf-is-pressed": "Removed — became a functional duplicate of sf-is-selected once its inert flag was cut. Named in the 'Prefer native state' section's removal rationale.",
+ "sf-is-current": "Removed — for consistency with the other speculative-flag removals. Named in the 'Prefer native state' section's removal rationale.",
+ "sf-is-danger": "Removed — identical implementation to sf-is-invalid/sf-is-error, never worked outside a form field. Named in the 'Prefer native state' section's removal rationale.",
+ "sf-is-pending": "Removed — two lines cheap to hand-roll, sf-is-loading covers the common case. Named in the 'Prefer native state' section's removal rationale.",
+ "sf-is-invisible": "Renamed and relocated to optional/utilities.css as .sf-invisible. Named in the 'Prefer native state' section's removal rationale (historical name, pre-relocation).",
+ "sf-is-visible": "Renamed and relocated to optional/utilities.css as .sf-visible. Named in the 'Prefer native state' section's removal rationale (historical name, pre-relocation)."
+ },
+ "docs/api-audit-checklist.md": {
+ "sf-is-hidden": "Removed 2026-07-21 (round 1) — recorded in the audit checklist as the resolved verdict for this class.",
+ "sf-is-readonly": "Removed 2026-07-21 (round 1) — recorded in the audit checklist as the resolved verdict for this class.",
+ "sf-is-busy": "Removed 2026-07-21 (round 1) — recorded in the audit checklist as the resolved verdict for this class.",
+ "sf-is-active": "Removed 2026-07-21 (round 2) — recorded in the audit checklist as the resolved verdict for this class.",
+ "sf-is-open": "Removed 2026-07-21 (round 2) — recorded in the audit checklist as the resolved verdict for this class.",
+ "sf-is-collapsed": "Removed 2026-07-21 (round 2) — recorded in the audit checklist as the resolved verdict for this class.",
+ "sf-is-expanded": "Removed 2026-07-21 (round 2) — recorded in the audit checklist as the resolved verdict for this class.",
+ "sf-is-pressed": "Removed 2026-07-21 (round 2) — recorded in the audit checklist as the resolved verdict for this class.",
+ "sf-is-current": "Removed 2026-07-21 (round 2) — recorded in the audit checklist as the resolved verdict for this class.",
+ "sf-is-danger": "Removed 2026-07-21 (round 2) — recorded in the audit checklist as the resolved verdict for this class.",
+ "sf-is-pending": "Removed 2026-07-21 (round 2) — recorded in the audit checklist as the resolved verdict for this class.",
+ "sf-is-invisible": "Historical name — recorded in the audit checklist before its rename/relocation to .sf-invisible.",
+ "sf-is-visible": "Historical name — recorded in the audit checklist before its rename/relocation to .sf-visible.",
+ "--sf-current-font-weight": "Removed 2026-07-21 (round 2) — orphaned when its only consumer (.sf-is-current) was removed. Recorded in the audit checklist.",
+ "--sf-state-pending-opacity": "Removed 2026-07-21 (round 2) — orphaned when its only consumer (.sf-is-pending) was removed. Recorded in the audit checklist.",
+ "--sf-is-active": "Removed 2026-07-21 (round 2) — @property registration for the removed .sf-is-active flag mechanism. Recorded in the audit checklist."
}
}
diff --git a/docs/registry.json b/docs/registry.json
index 001e76c0..5a5ad77c 100644
--- a/docs/registry.json
+++ b/docs/registry.json
@@ -19,9 +19,9 @@
"optional/utilities.css"
],
"counts": {
- "tokens": 747,
- "sf_classes": 287,
- "is_classes": 28
+ "tokens": 741,
+ "sf_classes": 289,
+ "is_classes": 15
}
},
"tokens": [
@@ -375,7 +375,7 @@
"--sf-color-text--on-tertiary",
"--sf-color-text--on-warning",
"--sf-color-text--placeholder",
- "--sf-color-text--secondary",
+ "--sf-color-text--subtle",
"--sf-color-warning",
"--sf-color-warning-muted",
"--sf-color-warning-source-dark",
@@ -397,7 +397,6 @@
"--sf-contrast-threshold",
"--sf-cover-min-height",
"--sf-cover-padding",
- "--sf-current-font-weight",
"--sf-density",
"--sf-display-l-line-height",
"--sf-display-m-line-height",
@@ -536,11 +535,7 @@
"--sf-icon-xl",
"--sf-icon-xs",
"--sf-imposter-margin",
- "--sf-is-active",
- "--sf-is-current",
"--sf-is-dark",
- "--sf-is-open",
- "--sf-is-pressed",
"--sf-leading-normal",
"--sf-leading-relaxed",
"--sf-leading-snug",
@@ -668,7 +663,6 @@
"--sf-space-xs",
"--sf-stack-gap",
"--sf-stagger-step",
- "--sf-state-pending-opacity",
"--sf-sticky-offset",
"--sf-sticky-offset-desktop",
"--sf-sticky-offset-mobile",
@@ -957,6 +951,7 @@
"sf-imposter",
"sf-imposter--contain",
"sf-imposter--fixed",
+ "sf-invisible",
"sf-line-clamp-2",
"sf-line-clamp-3",
"sf-line-clamp-N",
@@ -1060,36 +1055,24 @@
"sf-theme-dark",
"sf-theme-light",
"sf-theme-transition",
- "sf-truncate"
+ "sf-truncate",
+ "sf-visible"
],
"is_classes": [
- "sf-is-active",
- "sf-is-busy",
- "sf-is-collapsed",
- "sf-is-current",
- "sf-is-danger",
"sf-is-disabled",
"sf-is-draggable",
"sf-is-dragging",
"sf-is-drop-target",
"sf-is-empty",
"sf-is-error",
- "sf-is-expanded",
- "sf-is-hidden",
"sf-is-highlighted",
"sf-is-info",
"sf-is-invalid",
- "sf-is-invisible",
"sf-is-loading",
- "sf-is-open",
- "sf-is-pending",
- "sf-is-pressed",
- "sf-is-readonly",
"sf-is-selected",
"sf-is-skeleton",
"sf-is-success",
"sf-is-valid",
- "sf-is-visible",
"sf-is-warning"
],
"unprefixed_classes": [
diff --git a/docs/roadmap.md b/docs/roadmap.md
index b855b8b7..e1837ef0 100644
--- a/docs/roadmap.md
+++ b/docs/roadmap.md
@@ -224,7 +224,7 @@ not backlog. They should not be re-proposed or picked up by accident:
- **Runtime JavaScript in the framework core** — the core ships zero runtime JS and
will keep doing so. Viewport-triggered "reveal on scroll / on-visible" behaviour
that depends on an `IntersectionObserver` is out of scope for `core/`; the supported
- surfaces are the CSS-only `.sf-is-visible` state hook and scroll-timeline `view()`
+ surfaces are the CSS-only `.sf-visible` utility hook (optional/utilities.css) and scroll-timeline `view()`
entrance animations. Any observer-driven JS belongs to host/integration layers,
never to the framework bundle.
- **A consumer-facing preprocessor layer (mixins / functions)** — SLASHED is authored
diff --git a/docs/states.md b/docs/states.md
index 3ab4ede1..3c96c5f5 100644
--- a/docs/states.md
+++ b/docs/states.md
@@ -8,6 +8,72 @@ visuals on top.
All states are exercised live in the [demo](/demo/).
+## Prefer native state
+
+A `.sf-is-*` class is a fallback for when the platform has no state mechanism
+of its own — not the default reach. Before reaching for one, check whether a
+native pseudo-class, attribute, or ARIA state already gets you there:
+
+| Instead of | Reach for | Why |
+|---|---|---|
+| a hidden-state class | `[hidden]` | native, and already hardened to `display: none !important` in `core/reset.css` |
+| a disabled-state class | `:disabled` / `[disabled]` | native on every real form control and `