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
5 changes: 4 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,12 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),

## Unreleased

### Features
- **forms:** add `.sf-live-validate` — an opt-in scope (apply to a `<form>`/`<fieldset>`) that restores native `:user-invalid`/`:user-valid` → `--sf-field-border-color` feedback within that subtree, gated behind a real submit attempt rather than firing unconditionally on simple focus+blur. The zero-JS convenience of the removed automatic behaviour, without its premature-feedback failure mode.

### Breaking Changes
- **components:** `.sf-btn` nested in `.sf-card` no longer auto-shrinks its label to `--sf-text-s`. Buttons in cards now render at their own size like anywhere else; add `.sf-btn--s` for a compact card action. The orphaned `--sf-card-btn-font-size` token is removed. See `docs/migration.md`.
- **forms:** form fields no longer auto-colour on native `:user-invalid` / `:user-valid` — a field no longer looks "broken" the instant the browser considers it invalid, before your app or the user submits anything. The explicit `.sf-is-invalid` / `.sf-is-valid` (and `.sf-is-warning` / `.sf-is-info` / `.sf-is-danger`) state classes are unaffected and remain the way to colour a field's validation state. See `docs/migration.md`.
- **forms:** form fields no longer auto-colour on native `:user-invalid` / `:user-valid` — a field no longer looks "broken" the instant the browser considers it invalid, before your app or the user submits anything. The explicit `.sf-is-invalid` / `.sf-is-valid` (and `.sf-is-warning` / `.sf-is-info` / `.sf-is-danger`) state classes are unaffected and remain the way to colour a field's validation state; `.sf-live-validate` opts a `<form>` back into native-triggered feedback. See `docs/migration.md`.

## [0.7.15] - 2026-07-11

Expand Down
10 changes: 10 additions & 0 deletions configurator/src/data/classes.generated.json
Original file line number Diff line number Diff line change
Expand Up @@ -2284,6 +2284,16 @@
"optional": true,
"layer": "slashed.utilities"
},
{
"name": "sf-live-validate",
"selector": ".sf-live-validate",
"kind": "form",
"category": "Forms",
"group": "",
"description": "Scopes native constraint-validation feedback to this subtree: within it, :user-invalid/:user-valid on input/select/textarea drive --sf-field-border-color (the same token .sf-is-invalid/.sf-is-valid set explicitly). Apply to a <form> or <fieldset>. Off by default so a still-empty required field isn't marked invalid before a submit is attempted.",
"optional": true,
"layer": "slashed.forms"
},
{
"name": "sf-marker--action",
"selector": ".sf-marker--action",
Expand Down
2 changes: 1 addition & 1 deletion demos/full-api-demo-with-overrides.html
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,7 @@
</nav>
<main id="main">
<h1>SLASHED Full API Demo</h1>
<p style="color:var(--sf-color-text--muted)">v0.7.15 · full bundle from jsDelivr CDN · 328 classes · 755 tokens (265 configurable)</p>
<p style="color:var(--sf-color-text--muted)">v0.7.15 · full bundle from jsDelivr CDN · 329 classes · 755 tokens (265 configurable)</p>
<div class="notice"><strong>ultimate-override.css is ACTIVE.</strong> Every value below is recomputed from perturbed knob tokens — toggle it off in the toolbar, or compare against <a href="full-api-demo.html">the un-overridden page</a>.</div>

<section id="tokens">
Expand Down
2 changes: 1 addition & 1 deletion demos/full-api-demo.html
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,7 @@
</nav>
<main id="main">
<h1>SLASHED Full API Demo</h1>
<p style="color:var(--sf-color-text--muted)">v0.7.15 · full bundle from jsDelivr CDN · 328 classes · 755 tokens (265 configurable)</p>
<p style="color:var(--sf-color-text--muted)">v0.7.15 · full bundle from jsDelivr CDN · 329 classes · 755 tokens (265 configurable)</p>
<div class="notice">Baseline render with default tokens. Use the toolbar to switch theme, toggle the ultimate override live, or replay motion. The always-on override variant is <a href="full-api-demo-with-overrides.html">full-api-demo-with-overrides.html</a>.</div>

<section id="tokens">
Expand Down
34 changes: 29 additions & 5 deletions docs/api-index.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,30 +26,31 @@
"optimal"
],
"counts": {
"total": 1084,
"total": 1085,
"by_type": {
"class": 329,
"class": 330,
"token": 755
},
"by_tier": {
"INTERNAL": 1,
"PUBLIC": 1024,
"PUBLIC": 1025,
"PUBLIC-ADVANCED": 59
},
"by_role": {
"consumption": 490,
"knob": 265
},
"tokens": 755,
"classes": 329,
"sf_classes": 292,
"classes": 330,
"sf_classes": 293,
"is_classes": 28,
"unprefixed_classes": 9,
"by_category": {
"Accessibility": 8,
"Component tokens": 46,
"Components": 30,
"Core tokens": 622,
"Forms": 1,
"Layout primitives": 145,
"Layout tokens": 53,
"Macro classes": 58,
Expand Down Expand Up @@ -24879,6 +24880,29 @@
"full"
]
},
{
"name": "sf-live-validate",
"type": "class",
"tier": "PUBLIC",
"selector": ".sf-live-validate",
"prefix": "sf",
"kind": "form",
"category": "Forms",
"area": "forms",
"group": "",
"description": "Scopes native constraint-validation feedback to this subtree: within it, :user-invalid/:user-valid on input/select/textarea drive --sf-field-border-color (the same token .sf-is-invalid/.sf-is-valid set explicitly). Apply to a <form> or <fieldset>. Off by default so a still-empty required field isn't marked invalid before a submit is attempted.",
"isVariant": false,
"baseClass": null,
"optional": true,
"layer": "slashed.forms",
"sourceFiles": [
"optional/forms.css"
],
"bundles": [
"full",
"optimal"
]
},
{
"name": "sf-marker--action",
"type": "class",
Expand Down
12 changes: 9 additions & 3 deletions docs/api-index.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,11 @@ and a short description. The machine-readable companion (with all columns) is
[registry.json](registry.json); for the tier contract see
[architecture.md](architecture.md).

**1084 elements** — 755 tokens, 329 classes.
**1085 elements** — 755 tokens, 330 classes.

| Tier | Count | Meaning |
|---|---|---|
| PUBLIC | 1024 | Everyday surface. SemVer-stable. |
| PUBLIC | 1025 | Everyday surface. SemVer-stable. |
| PUBLIC-ADVANCED | 59 | Same SemVer guarantee; niche/powerful. |
| INTERNAL | 1 | Implementation detail; may change without a major bump. |

Expand Down Expand Up @@ -795,7 +795,7 @@ and a short description. The machine-readable companion (with all columns) is
| `--sf-surface-bg-size` | PUBLIC | knob | surface | `cover` | background-size for the .sf-surface-bg image. Default: cover. |
| `--sf-surface-color` | PUBLIC | consumption | surface | `var(--sf-color-base)` | Input for the generic .sf-surface macro. Set any color (including palette shades); the macro derives background, auto-contrast foreground, and the contextual token set from it. |

## Classes (329)
## Classes (330)

### Accessibility (8)

Expand Down Expand Up @@ -845,6 +845,12 @@ and a short description. The machine-readable companion (with all columns) is
| `.sf-card--elevated` | PUBLIC | component | .sf-card | Floating card: hides the border, applies a larger shadow. |
| `.sf-card--interactive` | PUBLIC | component | .sf-card | Adds a pointer cursor and a shadow/translate lift on hover or keyboard focus. Pair with .sf-clickable-parent for a fully clickable card. Lift is suppressed under prefers-reduced-motion. |

### Forms (1)

| Class | Tier | Kind | Group | Description |
|---|---|---|---|---|
| `.sf-live-validate` | PUBLIC | form | — | Scopes native constraint-validation feedback to this subtree: within it, :user-invalid/:user-valid on input/select/textarea drive --sf-field-border-color (the same token .sf-is-invalid/.sf-is-valid set explicitly). Apply to a <form> or <fieldset>. Off by default so a still-empty required field isn't marked invalid before a submit is attempted. |

### Layout primitives (145)

| Class | Tier | Kind | Group | Description |
Expand Down
10 changes: 9 additions & 1 deletion docs/classes.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

**292 .sf-classes, 28 .sf-is-classes.** Every `.sf-*` layout/macro class and `.sf-is-*` state class.
**293 .sf-classes, 28 .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`)
Expand Down Expand Up @@ -317,6 +317,14 @@ See [architecture.md](architecture.md) for layer order and naming conventions.
| `.sf-theme-light` |
| `.sf-theme-transition` |

## Forms (`optional/forms.css`)

1 classes.

| Class |
|---|
| `.sf-live-validate` |

## Components (`optional/components.css`)

32 classes.
Expand Down
10 changes: 9 additions & 1 deletion docs/llm-guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -1091,10 +1091,18 @@ Used by `.sf-is-active`, `.sf-is-current`, etc. in `core/states.css`. Allow comp
--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) */
--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-text-color /* set by validation states for text color feedback */
```

Native browser validation (`:user-invalid`/`:user-valid`) does NOT colour a field's
border by default — add `.sf-live-validate` to the `<form>`/`<fieldset>` to opt a
subtree into that feedback, gated behind a real submit attempt (not simple
focus+blur, which would flag a still-empty required field prematurely). The
explicit `.sf-is-invalid`/`.sf-is-valid` state classes (`core/states.css`) always
win over `.sf-live-validate`'s native-triggered colour, regardless of specificity —
`slashed.states` is declared after `slashed.forms` in the layer order (`core/layers.css`).

Component tokens from `optional/tokens.components.css`:

```css
Expand Down
14 changes: 14 additions & 0 deletions docs/migration.md
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,20 @@ The underlying `--sf-field-border-color` token and its consumers (resting
border, focus, autofill) are unchanged — only the automatic pseudo-class
trigger is gone.

**Want the native-triggered feedback back, without writing JS?** Add
`.sf-live-validate` to the `<form>` (or a `<fieldset>`) — it re-enables the
`:user-invalid`/`:user-valid` → `--sf-field-border-color` pivot, scoped to that
subtree, but only once a submit has actually been attempted (not on simple
focus+blur, which is what made the original unconditional behaviour fire
prematurely mid-form-fill):

```html
<form class="sf-live-validate">
<input type="email" required>
<button type="submit">Submit</button>
</form>
```

Comment on lines +96 to +109

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Confirm llm-guide.md references the new class / was touched in this PR.
rg -n "sf-live-validate" docs/llm-guide.md
git log --oneline -- docs/llm-guide.md | head -5

Repository: codeslash-dev/SLASHED

Length of output: 655


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n== files ==\n'
git ls-files docs/migration.md docs/llm-guide.md optional/forms.css || true

printf '\n== migration excerpt ==\n'
sed -n '80,130p' docs/migration.md

printf '\n== llm-guide excerpt (search terms) ==\n'
rg -n -C 3 "form|validate|token role|browser support|user-invalid|user-valid|sf-live-validate|border-color" docs/llm-guide.md || true

printf '\n== forms.css excerpt (search terms) ==\n'
rg -n -C 3 "sf-live-validate|user-invalid|user-valid|sf-field-border-color|form|fieldset" optional/forms.css || true

Repository: codeslash-dev/SLASHED

Length of output: 10362


Document .sf-live-validate in docs/llm-guide.md
optional/forms.css now makes native validation feedback opt-in. Add the new .sf-live-validate gate and the removed unconditional :user-invalid/:user-valid behavior to the guide so the forms docs match the shipped behavior.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/migration.md` around lines 96 - 109, Update the forms guidance in
llm-guide.md to document that native :user-invalid/:user-valid feedback is no
longer unconditional and is now enabled opt-in by adding .sf-live-validate to a
form or fieldset. Include the submit-attempt gating and subtree scoping
behavior, with an appropriate usage example consistent with the migration
documentation.

Source: Coding guidelines

### `--sf-touch-target` decoupled from `--sf-size-l`; size scale regularised (breaking)

`--sf-touch-target` used to be `var(--sf-size-l)`, which coupled the WCAG 2.5.5
Expand Down
3 changes: 2 additions & 1 deletion docs/registry.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
],
"counts": {
"tokens": 755,
"sf_classes": 292,
"sf_classes": 293,
"is_classes": 28
}
},
Expand Down Expand Up @@ -978,6 +978,7 @@
"sf-link--subtle",
"sf-link-external",
"sf-list-none",
"sf-live-validate",
"sf-marker--action",
"sf-marker--primary",
"sf-marker--secondary",
Expand Down
1 change: 1 addition & 0 deletions docs/token-annotations.json
Original file line number Diff line number Diff line change
Expand Up @@ -1063,6 +1063,7 @@
"sf-link--reverse": "Link color modifier: reverses the default link color to work on dark/inverted backgrounds. Sets color to --sf-color-text--inverse.",
"sf-link--subtle": "Link color modifier: renders the link in the body text color instead of the default action color. Underline still indicates the link on hover.",
"sf-link-external": "Automatically appends an external-link icon (via ::after content) to indicate the link opens in a new tab or goes to an external domain.",
"sf-live-validate": "Scopes native constraint-validation feedback to this subtree: within it, :user-invalid/:user-valid on input/select/textarea drive --sf-field-border-color (the same token .sf-is-invalid/.sf-is-valid set explicitly). Apply to a <form> or <fieldset>. Off by default so a still-empty required field isn't marked invalid before a submit is attempted.",
"sf-no-tap-highlight": "Removes the mobile tap highlight color (-webkit-tap-highlight-color: transparent). Use on interactive elements with a custom active state.",
"sf-not-prose": "Opt-out of sf-prose typography styles within a prose block. Apply to an element that should keep default browser or component styles.",
"sf-overflow-fade": "Adds a gradient fade at the inline-end of an overflowing element to hint at hidden content. Use inside sf-reel or any scroll container.",
Expand Down
28 changes: 28 additions & 0 deletions optional/forms.css
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,34 @@
}
/* stylelint-enable selector-pseudo-class-no-unknown */

/* Opt-in native validation feedback. Unconditional :user-invalid/:user-valid
styling was removed in 0.8.0 (see docs/migration.md): it fires on simple
focus+blur, so a still-empty required field could be marked invalid mid
multi-field form-fill, before any submit was attempted — the pattern
Bootstrap's own .was-validated gate exists to avoid. .sf-live-validate is
that same gate: put it on a <form> or <fieldset> to scope the pivot to
that subtree, so it only lights up on a real submit attempt (or explicit
interaction) instead of applying everywhere by default.

Safe to combine with manual validation: .sf-is-invalid/.sf-is-valid
(core/states.css, @layer slashed.states) always win over the rules below
regardless of selector specificity, because slashed.states is declared
AFTER slashed.forms in the layer order (core/layers.css) — cascade layer
order beats specificity. A field you've manually marked .sf-is-invalid
(e.g. a server-side error) can't be silently un-coloured by these rules
even if the browser considers it natively valid. */
.sf-live-validate input:user-invalid,
.sf-live-validate select:user-invalid,
.sf-live-validate textarea:user-invalid {
--sf-field-border-color: var(--sf-color-danger);
}

.sf-live-validate input:user-valid,
.sf-live-validate select:user-valid,
.sf-live-validate textarea:user-valid {
--sf-field-border-color: var(--sf-color-success);
}
Comment on lines +89 to +115

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 docs/llm-guide.md not updated — CLAUDE.md requirement

CLAUDE.md mandates: "Any PR that touches core/*.css, optional/*.css, or token-registry.json must also review docs/llm-guide.md and update it if needed." This PR touches optional/forms.css but docs/llm-guide.md is not among the changed files. Section 9.10 of the guide documents --sf-field-border-color as the hook set by validation states, but doesn't mention .sf-live-validate as the mechanism to re-enable native-triggered feedback. An LLM consuming the guide to help a user implement form validation would have no way to learn this class exists.

Context Used: CLAUDE.md (source)

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Comment on lines +105 to +115

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Interaction with explicit .sf-is-invalid / .sf-is-valid state classes inside .sf-live-validate

The new rules have specificity 0-2-1 (.sf-live-validate + element + pseudo-class), while the explicit .sf-is-invalid / .sf-is-valid state classes live in core/states.css. If a field inside .sf-live-validate has .sf-is-invalid applied for custom/server-side validation but the browser considers it natively valid (:user-valid), the .sf-live-validate input:user-valid rule will set --sf-field-border-color to --sf-color-success, silently overriding the manually-applied invalid styling. This is an edge case, but a silent, non-obvious CSS cascade conflict worth documenting in migration.md or a code comment.


/* Numeric inputs */
input[type="number"] {
font-variant-numeric: var(--sf-font-numeric, tabular-nums);
Expand Down
2 changes: 1 addition & 1 deletion scripts/lib/api-index/extract.js
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ export const FILE_META = {
'core/motion.css': { category: 'Motion / animation', area: 'motion', kind: 'motion' },
'core/print.css': { category: 'Print', area: 'print', kind: 'print' },
'core/themes.css': { category: 'Theme utilities', area: 'theme', kind: 'theme' },
'optional/forms.css': { category: 'Forms (classless)', area: 'forms', kind: 'form' },
'optional/forms.css': { category: 'Forms', area: 'forms', kind: 'form' },
'optional/components.css': { category: 'Components', area: 'components', kind: 'component' },
'optional/theme-example.css': { category: 'Theme example', area: 'theme', kind: 'theme' },
'optional/utilities.css': { category: 'Utilities', area: 'utilities', kind: 'utility' },
Expand Down
28 changes: 28 additions & 0 deletions tests/forms.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,34 @@ for (const theme of ['light', 'dark']) {
expect(cs['border-top-color']).toBe('rgb(1, 2, 3)');
});

test('.sf-live-validate scopes native validation feedback to a submitted form', async ({ page }) => {
// Two forms, both with an invalid required field, both submitted (a real,
// trusted click — :user-invalid needs genuine interaction, not a bare JS
// focus()/blur() call, to match reliably). Only "validated" carries the
// opt-in class, so only its field should pick up --sf-field-border-color.
// The invalid-event listener suppresses the native validation bubble —
// the standard pairing for custom validation styling — so the assertion
// isn't confused by the browser's own transient bubble-UI rendering.
await mount(page, `
<form id="plain"><input type="email" id="plain-input" value="not-an-email" required><button type="submit">Go</button></form>
<form id="validated" class="sf-live-validate"><input type="email" id="validated-input" value="not-an-email" required><button type="submit">Go</button></form>
<script>
for (const id of ['plain-input', 'validated-input']) {
document.getElementById(id).addEventListener('invalid', (e) => e.preventDefault());
}
</script>
`);
await page.locator('#plain button[type=submit]').click();
await page.locator('#validated button[type=submit]').click();
const [plainVar, validatedVar, danger] = await page.evaluate(() => [
getComputedStyle(document.getElementById('plain-input')).getPropertyValue('--sf-field-border-color').trim(),
getComputedStyle(document.getElementById('validated-input')).getPropertyValue('--sf-field-border-color').trim(),
getComputedStyle(document.body).getPropertyValue('--sf-color-danger').trim(),
]);
expect(plainVar).toBe(''); // unscoped: no auto-colouring, even on submit
expect(validatedVar).toBe(danger); // scoped: native invalidity now drives the token
});

test('textarea is vertically resizable with a minimum height', async ({ page }) => {
await mount(page, `<textarea id="t"></textarea>`);
const cs = await computed(page, ['resize', 'min-block-size']);
Expand Down