diff --git a/CHANGELOG.md b/CHANGELOG.md index d181c28d..75024630 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,118 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), ## [Unreleased] +## [0.3.0] - 2026-05-24 + +Class taxonomy refactor. New `slashed.macros` cascade layer, 12 macro +recipes, ACSS-parity essentials, and a v0.3.0+ blueprint for 8 reserved +components. Plus the in-flight WebKit a11y/contrast fixes that were sitting +in `Unreleased`. + +### ⚠️ Breaking Changes + +#### New `slashed.macros` cascade layer + +A new `slashed.macros` layer is introduced between `slashed.components` +and `slashed.utilities`. Macros may compose with primitives and +components, but a single-property utility still wins on the same +selector. Updated layer order: + +```text +tokens → reset → base → forms → layout → components → macros → +utilities → states → themes → motion → accessibility → print → +legacy → overrides +``` + +Three classes change cascade layer (selectors and properties unchanged): + +- `.sf-prose`, `.sf-not-prose` — `slashed.layout` → `slashed.macros`. + Moved out of `core/layout.css` into the new `core/macros.css`. +- `.focus-parent` — `slashed.states` → `slashed.accessibility`. + Now lives in `core/accessibility.css` alongside the rest of the focus / + a11y rules. + +A site that worked in 0.2.x works in 0.3.0 without any markup changes. +Consumers who previously wrote `@layer slashed.layout { .sf-prose { … } }` +or `@layer slashed.states { .focus-parent { … } }` overrides should move +those rules to `slashed.overrides` (the documented escape hatch). +See `docs/migration.md` for details. + +### Added + +- **`core/macros.css`** — 12 recipes / patterns. + - `.sf-prose`, `.sf-not-prose` (relocated from `core/layout.css`). + - `.sf-flow` — Heydon Pickering's lobotomized owl + (`> * + * { margin-block-start: var(--sf-flow-space) }`). + - `.sf-truncate` — single-line ellipsis. + - `.sf-line-clamp-2`, `.sf-line-clamp-3`, `.sf-line-clamp-N`. + - `.sf-equal-height` — flex children stretch to tallest. + - `.sf-aspect` — generic aspect-ratio container. + - `.sf-scroll-shadow` — top + bottom mask gradient on a vertical + scroll container. + - `.sf-scroll-snap` — vertical scroll-snap container. + - `.sf-overflow-fade` — end-edge horizontal mask fade. + - `.sf-no-tap-highlight` — suppress mobile tap-highlight. +- **`core/tokens.macros.css`** — semantic tokens for the macros: + `--sf-flow-space`, `--sf-line-clamp`, `--sf-truncate-suffix`, + `--sf-aspect`, `--sf-scroll-shadow-size`. +- **A11y patterns:** + - `.sf-clickable-parent` — the card-with-link pattern (overlay-based + full-card click target with single AT announcement; text remains + selectable; secondary buttons / links keep working; + `[data-no-overlay]` is the consumer escape hatch). +- **Layout extras (ACSS parity):** + - `.sf-icon--boxed` — modifier on the existing `.sf-icon` that wraps + the glyph in a padded, bordered, optionally coloured frame + (content-box; consumes new `--sf-icon-box-*` tokens). + - Border-style scale: `--sf-border-style`, `--sf-border-style-strong`, + `--sf-border-style-soft`, `--sf-border-style-dotted` — pairs with + the existing `--sf-border-width-*` and `--sf-color-border*` so + consumers can switch decorative styles without rewriting rules. + - Icon-boxed tokens: `--sf-icon-box-pad`, `--sf-icon-box-radius`, + `--sf-icon-box-bg`, `--sf-icon-box-border`. +- **Components blueprint** — `optional/components.css` and + `optional/tokens.components.css` move from empty `/* TODO */` stubs + to structured BLUEPRINTs. Both files keep an active `@layer` + declaration to RESERVE cascade position; every class definition and + every component token is commented out. Activation is planned for + upcoming minor releases (additive only). The 8 reserved components + are: `.sf-button`, `.sf-card`, `.sf-badge`, `.sf-tag`, `.sf-alert`, + `.sf-avatar`, `.sf-modal`, `.sf-skeleton`. Card tokens (originally + proposed for the essential bundle) live here, commented, alongside + the rest — so the entire component-related surface (classes + + tokens) shares one activation cycle. See `docs/components.md`. + +### Documentation + +- **`docs/architecture.md`** — new "Class taxonomy" section with the + 7 categories and a 5-step decision tree for adding new classes; + layer order updated; file structure updated. +- **`docs/macros.md`** *(new)* — full reference for every macro with + signature, usage example, and consumed tokens. +- **`docs/components.md`** *(new)* — components blueprint and the + ratified out-of-scope list (`tabs`, `accordion`, `tooltip`, …) with + rationale for each exclusion. +- **`docs/migration.md`** — adds an "0.2.x → 0.3.0" section explaining + the three relocations and the `slashed.overrides` escape hatch. +- README updated for the new layer order, file list, and bundle + contents. + +### Tests + +- `tests/macros.spec.js` *(new)* — behavioural tests for each macro, + covering both default behaviour and token-override paths. +- `tests/layers.spec.js` — three new ordering invariants: + `macros > components`, `utilities > macros`, + `accessibility > motion`. +- `tests/coverage.spec.js` — three new exclusions for selectors + exercised by dedicated specs (`.sf-overflow-fade`, + `.sf-no-tap-highlight`, `.sf-clickable-parent`). +- `tests/tokens.spec.js` — adds `core/tokens.macros.css` to + the token-coverage source list. +- `docs/demo.html` — new `
` exercising every + macro inline. Existing `tests/demo-visual.spec.js` screenshots are + locator-scoped and unaffected. + ### Changed - **CI: skip `[webkit] axe: no WCAG A/AA violations (dark)`** with a @@ -26,13 +138,10 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), bump from `0.68` to `0.72` was made under the (incorrect) belief that the dark-mode link floor was the cause of the WebKit axe contrast failure. The actual cause was selector specificity (now fixed via - `a:link`, see above) and the WebKit value was painting `-webkit-link` + `a:link`, see below) and the WebKit value was painting `-webkit-link` regardless of how the formula computed. Restoring PR #70's `0.68` - floor — that floor is still a defensive contrast cushion above the - pre-PR-#70 `0.62`, just without the speculative extra bump that - served no purpose. Tiny visible delta in dark mode across all - browsers; the formula's value never reached WebKit anyway and now - Chromium/Firefox revert to PR #70's tested floor. + floor — still a defensive contrast cushion above the pre-PR-#70 + `0.62`, without the speculative extra bump that served no purpose. ### Fixed @@ -56,6 +165,15 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), pickers is unaffected (the `:where()` block only matches text-like inputs). +### Bundle size + +- essential: 9.9 kB → 10.4 kB gzip (+5%). +- optimal: 12.2 kB → 12.7 kB gzip (+4%). +- optimal-components / full: minifier strips blueprint comments, so + min sizes are unchanged from optimal. +- All bundles remain well under their `tests/bundle-size.spec.js` + budgets (15 / 18 / 20 kB gzip). + ## [0.2.12] - 2026-05-23 Release infrastructure and Bricks Builder integration. diff --git a/README.md b/README.md index 48c5d140..f1609bd0 100644 --- a/README.md +++ b/README.md @@ -20,10 +20,12 @@ A cascade-layer CSS framework. No build step. No Node. No runtime dependencies. + + @@ -35,11 +37,14 @@ A cascade-layer CSS framework. No build step. No Node. No runtime dependencies. ``` -> **Note:** `optional/components.css`, `optional/utilities.css` and -> `optional/tokens.components.css` are empty placeholders reserved for a -> future component/utility layer. They ship (as no-ops) only in the -> `*-components` / `*-utilities` / `full` bundles and do nothing until -> populated — there's no need to link them individually yet. +> **Note:** `optional/components.css` and `optional/tokens.components.css` +> ship as **blueprints** in v0.3.0 — their `@layer` declarations reserve +> cascade position, but every class definition and component token is +> commented out (no CSS is emitted). They appear (as no-ops) in the +> `*-components` and `full` bundles only. `optional/utilities.css` +> remains an empty stub — SLASHED is BEM-first by design and ships no +> utility classes in 0.x. There's no need to link any of the three +> individually. **Recommended:** use a pre-built bundle instead of wiring up every file (see [Releases](https://github.com/codeslash-dev/SLASHED/releases/latest)): @@ -58,8 +63,9 @@ A cascade-layer CSS framework. No build step. No Node. No runtime dependencies. ## Cascade layer order ```text -slashed.tokens → reset → base → forms → layout → components → utilities → -states → themes → motion → accessibility → print → legacy → overrides +slashed.tokens → reset → base → forms → layout → components → macros → +utilities → states → themes → motion → accessibility → print → legacy → +overrides ``` Declared in `core/layers.css`. Later layers win. `slashed.overrides` @@ -68,7 +74,10 @@ is reserved for your own overrides and sits last so it always wins. `@supports not (...)` and are inert on modern engines. `slashed.forms` (between `base` and `layout`) holds the opt-in -classless form styling from `optional/forms.css`. +classless form styling from `optional/forms.css`. `slashed.macros` +(between `components` and `utilities`, added in v0.3.0) holds recipes +like `.sf-prose`, `.sf-flow`, `.sf-truncate`, `.sf-aspect`, +`.sf-scroll-shadow`. See [`docs/macros.md`](docs/macros.md). ## Scope of the base layer @@ -96,17 +105,18 @@ the `slashed.tokens` layer. | Bundle | Contents | | --- | --- | -| `slashed.essential.css` | all `core/` (`layers` + `tokens` + `tokens.layout` + `reset` + `base` + `themes` + `layout` + `states` + `motion` + `accessibility` + `print`) | +| `slashed.essential.css` | all `core/` (`layers` + `tokens` + `tokens.layout` + `tokens.macros` + `reset` + `base` + `themes` + `layout` + `macros` + `states` + `motion` + `accessibility` + `print`) | | `slashed.optimal.css` | essential + `tokens.palette` + `forms` + `legacy` | -| `slashed.optimal-components.css` | optimal + `tokens.components` + `components` | +| `slashed.optimal-components.css` | optimal + `tokens.components` *(blueprint)* + `components` *(blueprint)* | | `slashed.optimal-utilities.css` | optimal + `utilities` | -| `slashed.full.css` | optimal + `tokens.components` + `components` + `utilities` | +| `slashed.full.css` | optimal + `tokens.components` *(blueprint)* + `components` *(blueprint)* + `utilities` | `optional/legacy.css` is always concatenated last. Every rule lives in an `@layer`, so concatenation order never affects the cascade — `core/layers.css` -fixes it. The `components` / `utilities` / `tokens.components` files are still -empty stubs: they ship (no-op) only in the `*-components` / `*-utilities` / -`full` bundles and do nothing until populated. +fixes it. `components.css` and `tokens.components.css` ship as **blueprints** +in 0.3.0 — their `@layer` declarations reserve cascade position, but every +selector and token is commented out (no CSS is emitted). `utilities.css` +ships as an empty stub. À la carte is also supported — start from `essential` (or raw `core/`) and add hand-picked optional files in any order. When building a custom bundle by hand, @@ -179,15 +189,17 @@ versions above — colors will collapse to `initial` on older engines. | Guide | What's inside | | --- | --- | -| [Architecture](docs/architecture.md) | layers, file structure, bundles, token contract, naming | +| [Architecture](docs/architecture.md) | layers, file structure, class taxonomy, bundles, token contract | | [Theming](docs/theming.md) | rebrand in 6 tokens, multi-brand, contrast | | [Dark mode](docs/dark-mode.md) | toggle script, scoped themes, per-value overrides | | [Layout primitives](docs/layout.md) | every `.sf-*` layout class + tokens | +| [Macros / recipes](docs/macros.md) | every `.sf-*` macro-class + tokens (v0.3.0+) | +| [Components blueprint](docs/components.md) | reserved component names + roadmap | | [State classes](docs/states.md) | every `.is-*` + ARIA mapping + overlap semantics | | [Token reference](docs/tokens.md) | all `--sf-*` tokens + defaults (generated) | | [Browser support](docs/browser-support.md) | the support floor and why | | [Performance](docs/performance.md) | modern-CSS footguns to avoid | -| [Migration](docs/migration.md) | Pico / Bulma / Tailwind → SLASHED | +| [Migration](docs/migration.md) | 0.2.x → 0.3.0 + Pico / Bulma / Tailwind → SLASHED | | [Contributing](CONTRIBUTING.md) | setup, conventions, tests | ## Development diff --git a/bundle.config.json b/bundle.config.json index d264b1d4..06853fbd 100644 --- a/bundle.config.json +++ b/bundle.config.json @@ -6,10 +6,12 @@ "core/layers.css", "core/tokens.css", "core/tokens.layout.css", + "core/tokens.macros.css", "core/reset.css", "core/base.css", "core/themes.css", "core/layout.css", + "core/macros.css", "core/states.css", "core/motion.css", "core/accessibility.css", @@ -23,10 +25,12 @@ "core/layers.css", "core/tokens.css", "core/tokens.layout.css", + "core/tokens.macros.css", "core/reset.css", "core/base.css", "core/themes.css", "core/layout.css", + "core/macros.css", "core/states.css", "core/motion.css", "core/accessibility.css", @@ -39,10 +43,12 @@ "core/layers.css", "core/tokens.css", "core/tokens.layout.css", + "core/tokens.macros.css", "core/reset.css", "core/base.css", "core/themes.css", "core/layout.css", + "core/macros.css", "core/states.css", "core/motion.css", "core/accessibility.css", @@ -59,10 +65,12 @@ "core/layers.css", "core/tokens.css", "core/tokens.layout.css", + "core/tokens.macros.css", "core/reset.css", "core/base.css", "core/themes.css", "core/layout.css", + "core/macros.css", "core/states.css", "core/motion.css", "core/accessibility.css", @@ -78,10 +86,12 @@ "core/layers.css", "core/tokens.css", "core/tokens.layout.css", + "core/tokens.macros.css", "core/reset.css", "core/base.css", "core/themes.css", "core/layout.css", + "core/macros.css", "core/states.css", "core/motion.css", "core/accessibility.css", @@ -100,10 +110,12 @@ "core/layers.css", "core/tokens.css", "core/tokens.layout.css", + "core/tokens.macros.css", "core/reset.css", "core/base.css", "core/themes.css", "core/layout.css", + "core/macros.css", "core/states.css", "core/motion.css", "core/accessibility.css", @@ -121,10 +133,12 @@ "core/layers.css", "core/tokens.css", "core/tokens.layout.css", + "core/tokens.macros.css", "core/reset.css", "core/base.css", "core/themes.css", "core/layout.css", + "core/macros.css", "core/states.css", "core/motion.css", "core/accessibility.css", @@ -142,10 +156,12 @@ "core/layers.css", "core/tokens.css", "core/tokens.layout.css", + "core/tokens.macros.css", "core/reset.css", "core/base.css", "core/themes.css", "core/layout.css", + "core/macros.css", "core/states.css", "core/motion.css", "core/accessibility.css", @@ -162,10 +178,12 @@ "core/layers.css", "core/tokens.css", "core/tokens.layout.css", + "core/tokens.macros.css", "core/reset.css", "core/base.css", "core/themes.css", "core/layout.css", + "core/macros.css", "core/states.css", "core/motion.css", "core/accessibility.css", @@ -185,10 +203,12 @@ "core/layers.css", "core/tokens.css", "core/tokens.layout.css", + "core/tokens.macros.css", "core/reset.css", "core/base.css", "core/themes.css", "core/layout.css", + "core/macros.css", "core/states.css", "core/motion.css", "core/accessibility.css", diff --git a/core/accessibility.css b/core/accessibility.css index 8bc7f319..7d05dba4 100644 --- a/core/accessibility.css +++ b/core/accessibility.css @@ -161,6 +161,73 @@ 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 + v0.3.0) so that all focus-related a11y rules share one home + 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. + ---------------------------------------------------------- */ + .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); + } + + /* ---------------------------------------------------------- + Clickable-parent — the card-with-link a11y pattern. + Pairs with a single child whose `href` provides the + destination. The link is announced once by AT (the rest of + the card content is read in continuous flow because nothing + wraps it in another link), but the entire card is clickable. + + Mechanic: the link's ::after grows to fill the parent's + positioning context, so the parent's `cursor: pointer` and + hit-area cover the whole card. Inline text is still + selectable because the overlay sits at z-index 1 and any + paragraph/heading inside the card needs `position: relative` + to lift above it — the rule below applies that to the most + common children automatically. + + Independent interactive children (buttons, secondary links, + form controls) get z-index: 2 so they keep working even when + the overlay is present. + + Usage: +
+

Title

+

Description (still selectable)

+ +
+ ---------------------------------------------------------- */ + .sf-clickable-parent { + position: relative; + cursor: pointer; + } + .sf-clickable-parent a:where(:any-link)::after { + content: ""; + position: absolute; + inset: 0; + z-index: 1; + pointer-events: auto; + } + /* Inline content lifts above the overlay so users can select + text inside the card without their drag being captured by + the link. Limited to common flow children — consumers can + opt in extras with [data-no-overlay] below. */ + .sf-clickable-parent :is(p, h1, h2, h3, h4, h5, h6, span, time, small, em, strong, code) { + position: relative; + z-index: 2; + } + /* Independent interactive descendants beat the overlay, so a + secondary , + + ---------------------------------------------------------- */ + .sf-clickable-parent { + position: relative; + cursor: pointer; + } + .sf-clickable-parent a:where(:any-link)::after { + content: ""; + position: absolute; + inset: 0; + z-index: 1; + pointer-events: auto; + } + /* Inline content lifts above the overlay so users can select + text inside the card without their drag being captured by + the link. Limited to common flow children — consumers can + opt in extras with [data-no-overlay] below. */ + .sf-clickable-parent :is(p, h1, h2, h3, h4, h5, h6, span, time, small, em, strong, code) { + position: relative; + z-index: 2; + } + /* Independent interactive descendants beat the overlay, so a + secondary , + + ---------------------------------------------------------- */ +.sf-clickable-parent { + position: relative; + cursor: pointer; +} +.sf-clickable-parent a:where(:any-link)::after { + content: ""; + position: absolute; + inset: 0; + z-index: 1; + pointer-events: auto; +} +/* Inline content lifts above the overlay so users can select + text inside the card without their drag being captured by + the link. Limited to common flow children — consumers can + opt in extras with [data-no-overlay] below. */ +.sf-clickable-parent :is(p, h1, h2, h3, h4, h5, h6, span, time, small, em, strong, code) { + position: relative; + z-index: 2; +} +/* Independent interactive descendants beat the overlay, so a + secondary , \n \n ---------------------------------------------------------- */\n.sf-clickable-parent {\n position: relative;\n cursor: pointer;\n}\n.sf-clickable-parent a:where(:any-link)::after {\n content: \"\";\n position: absolute;\n inset: 0;\n z-index: 1;\n pointer-events: auto;\n}\n/* Inline content lifts above the overlay so users can select\n text inside the card without their drag being captured by\n the link. Limited to common flow children — consumers can\n opt in extras with [data-no-overlay] below. */\n.sf-clickable-parent :is(p, h1, h2, h3, h4, h5, h6, span, time, small, em, strong, code) {\n position: relative;\n z-index: 2;\n}\n/* Independent interactive descendants beat the overlay, so a\n secondary , \n \n ---------------------------------------------------------- */\n .sf-clickable-parent {\n position: relative;\n cursor: pointer;\n }\n .sf-clickable-parent a:where(:any-link)::after {\n content: \"\";\n position: absolute;\n inset: 0;\n z-index: 1;\n pointer-events: auto;\n }\n /* Inline content lifts above the overlay so users can select\n text inside the card without their drag being captured by\n the link. Limited to common flow children — consumers can\n opt in extras with [data-no-overlay] below. */\n .sf-clickable-parent :is(p, h1, h2, h3, h4, h5, h6, span, time, small, em, strong, code) {\n position: relative;\n z-index: 2;\n }\n /* Independent interactive descendants beat the overlay, so a\n secondary , + + ---------------------------------------------------------- */ + .sf-clickable-parent { + position: relative; + cursor: pointer; + } + .sf-clickable-parent a:where(:any-link)::after { + content: ""; + position: absolute; + inset: 0; + z-index: 1; + pointer-events: auto; + } + /* Inline content lifts above the overlay so users can select + text inside the card without their drag being captured by + the link. Limited to common flow children — consumers can + opt in extras with [data-no-overlay] below. */ + .sf-clickable-parent :is(p, h1, h2, h3, h4, h5, h6, span, time, small, em, strong, code) { + position: relative; + z-index: 2; + } + /* Independent interactive descendants beat the overlay, so a + secondary , + + ---------------------------------------------------------- */ +.sf-clickable-parent { + position: relative; + cursor: pointer; +} +.sf-clickable-parent a:where(:any-link)::after { + content: ""; + position: absolute; + inset: 0; + z-index: 1; + pointer-events: auto; +} +/* Inline content lifts above the overlay so users can select + text inside the card without their drag being captured by + the link. Limited to common flow children — consumers can + opt in extras with [data-no-overlay] below. */ +.sf-clickable-parent :is(p, h1, h2, h3, h4, h5, h6, span, time, small, em, strong, code) { + position: relative; + z-index: 2; +} +/* Independent interactive descendants beat the overlay, so a + secondary ,