Repository navigation
Conversation
commit: |
The nine-step scale is replaced by four inputs, --primary, --secondary, --neutral and --error, and fourteen colours derived from them on every element. Declaring the derived colours on * is what lets an input set on any element re-theme its subtree: a custom property declared once at the root is substituted there, so a descendant changing an input never reached it. Colours keep the input's own hue and chroma and pin only the lightness, so a brand colour survives; chroma is capped per role and scheme to stay in gamut, using 2.x's stops so a vivid brand looks the same. Text is solid, surfaces and rules are a tinted mid-tone at partial opacity. Disabling is one declaration, --chroma: 0, instead of re-binding 29 variables per selector. --line moves from 0.695 to 0.62 (light) and 0.54 (dark): the 2.x value failed WCAG 1.4.11 at about 2.7:1 for every brand tested. Measured across ten brands in both schemes, every text pair clears 4.5:1 and --line clears 3:1. index.css: 60.2 kB to 38.4 kB raw, 7.3 kB to 6.3 kB gzip, 477 custom-property declarations to 95.
|
📚 Docs preview: https://sparkstonepdx-css-pr-6.surge.sh Built from 33099dc. |
The pickers drive the swatches, so they now sit right under the inputs they set. Seeding them from the page also broke visibly there: --link's computed value is still a relative-colour expression, which the canvas conversion can't parse, so the Secondary picker showed black. They now seed from an element's resolved colour, read back as a pixel.
The core .skeleton turned its shimmer off and went transparent unless the element carried Kobalte's data-animate and data-visible, so class and classless skeletons never animated and showed nothing. That state moves into the Kobalte layer, keyed on Kobalte's root, role="group"; the core class always animates, still off under prefers-reduced-motion.
Declaring the derived colours on * made every element a re-derivation point. That blocked overriding a derived colour for a subtree, since each descendant re-derived its own, and made restyles 34% slower than 2.x. Colours are now derived at the root, on [data-color-scheme], and on .retheme, as sparkcss did; components that set an input themselves (error fields, error alerts, every disabled state) @extend .retheme, so they share one rule. The derived colours are registered with @Property as <color>. Unregistered, a custom property inherits as text, so every use re-evaluated the whole nested relative-colour expression; registered, it resolves once and inherits as a colour. Restyles are now 8% faster than 2.x. --primary is never declared by the library; the derivation reads var(--primary, var(--color, rebeccapurple)), so either name works on any boundary, not only at the root. A boundary also re-applies text colour, so a disabled element's own text greys.
The text colour a boundary re-applies lived in its own .retheme rule, which @extend also appended every boundary selector to, so the 31-selector list was emitted twice. It now sits in the one rule with the derived colours.
Every component set its own --chroma: 0 or --primary: var(--error) and made itself a boundary, so the boundary rule carried 33 selectors. Now two rules cover everything, however it says so: :disabled, [data-disabled], [aria-disabled="true"] and .disabled for disabled; [aria-invalid="true"] and .error for invalid. Component state classes (.tab-disabled, .menu-disabled, .btn-disabled, .input-error, .alert-error, .text-error) @extend those. 19 boundary selectors, one --chroma: 0 in the build. All 15 disabled and error states compute as before, except that any element Kobalte marks data-disabled now greys in the class build too.
The field, field-block, field-text, field-height and field-icon blocks were included into every control, so the same declarations were emitted up to seven times. They are now placeholder selectors that each control @extends, which groups the controls onto one rule per block. Declared before the controls, so a control's own declarations still override them. Every form control and its pseudo-elements compute identically before and after, in both builds (250 styles compared). index.css: 39.8 kB to 37.5 kB raw.
Translucent surfaces made every contrast ratio depend on what was underneath. The docs' alpha badge passed on its own and failed at 4.34:1 on the current page's highlighted sidebar row, where two translucent layers stacked. Surfaces, rules, the placeholder and the docs badge are now color-mix()ed into --bg: solid, so each pairing has one ratio that can be checked once. Mixing with the page colour rather than black or white keeps the scheme flip for free. The badge now measures 5.58:1 (light) and 5.46 (dark) wherever it sits. A card over an image is opaque again, and nested surfaces no longer stack. Also: the tooltip was a mid-tone with light text and measured 3.47:1 at worst; it is now a true inverse, --fg on --bg, at 18.37.
The placeholder was --fg-muted mixed 50% into the page, 2.86:1 at worst. 70% is the smallest mix that clears 4.5:1 for every brand tested in both schemes (lowest 4.78), and still reads lighter than an entered value.
The 1.x classless build hung an absolutely positioned, full-height SVG off every element inside a <details>: the space in 'details ::after' made it a descendant selector. It drew nothing visible, but the overflow stretched the page, so a classless collapse was taller than the class and Kobalte ones.
The box fades in from scale(1.25) over 550ms, off under prefers-reduced-motion; the dialog clips horizontal overflow so the scaled box never scrolls sideways. The animation sits on .dialog-box itself: a closed dialog's contents are not rendered, and an animation restarts whenever its element starts rendering, so it replays on every open without a dialog[open] ancestor, and the classless dialog > article binding picks it up via @extend.
Toggles, checkbox ticks, radio dots and the tab and segmented indicators each animated at their own speed (150, 200 or 250ms). They now all read --duration-change, 200ms, the value 1.x used for the tick and radio dot. The dialog's open animation reads a new --duration-enter, 400ms, down from 550ms: Material 3's duration for an element entering the screen. Loops (the spinner and skeleton) keep their own durations.
All behind prefers-reduced-motion: no-preference, all on the two duration tokens, none depending on a component library: - dialogs fade their blur and backdrop in, and fade out on close, kept in the top layer with transition-behavior: allow-discrete - dropdowns, popovers and tooltips scale in from 0.96 as they start rendering - buttons ease colour changes and press to scale(0.98); menu rows, tabs and collapse titles ease their hover - toast messages rise into the stack - a native <details> collapse animates its height via ::details-content and interpolate-size, where supported Verified in Chromium 141: the dialog closes through opacity 0.42, 0.13, 0.02 before hiding; panels and toasts animate on mount; a collapse passes 49px on its way to 120px. With reduced motion, none of it runs.
The box eases to its checked fill, and the tick pops in from scale(0.4) with a slight overshoot (peaking at about 1.05) before settling at its normal size. Behind prefers-reduced-motion: no-preference, on --duration-change; with reduced motion the tick keeps its 1.x fade only. A checked box at rest is identical to before.
A pressed .btn now scales to 1.02, rising to meet the click, instead of 0.98.
The press used the default ease, which starts slowly: 25ms after the click the button had covered 7% of its grow. On cubic-bezier(0.2, 0, 0, 1) it has covered 41%, so the press answers the click at once and lands softly.
Page buttons were full-size .btn elements, 55px tall, so single-digit pages were tall, narrow rectangles (46 by 55). Inside .pagination they are now 2.25rem tall with a minimum width to match, so a number is square (38 by 36) and Previous and Next keep comfortable padding. The size is --pagination-size.
Each item is a <label> wrapping a radio, so in the classless build (and the Kobalte layer on top of it) the label rules laid it out as a form row: a 6px bottom margin, and the hidden radio put back in the flow as a 20px box that pushed the text right. Items were 28px wider than in the class build. The segmented control now outranks those rules for its own items, so all three builds measure the same (44, 64 and 89px by 35). The docs' frame runtime also places the Kobalte demo's indicator on the checked item, as Kobalte does, instead of a hard-coded width.
…d panel Menu rows now respond to the standard state signals as well as the classes: a row is highlighted on :focus or data-highlighted (Kobalte, Radix and Ark all set it), and selected on aria-selected or aria-checked. Before, a Kobalte select on the class build showed no highlight and no selection, and no class could fix it, since no headless library exposes which row is highlighted. In the Kobalte layer the listbox no longer doubles as a panel. A real Kobalte Select renders the panel as Select.Content around the listbox; binding .dropdown-content to the listbox drew a second bordered panel inside it. The select page's demo now mirrors the DOM Kobalte renders, and gains a class-build example.
The field reserved 2.5rem on the right and the chevron sits 1.25rem to 2.25rem in, so truncated text ran to within 4px of it. Selects now reserve 3rem, a 12px gap. And .select truncates with an ellipsis on one line, as a native select does; a <button> trigger, such as Kobalte's, wrapped onto a second line instead. Date and time inputs share the icon rule but keep 2.5rem, since their invisible picker button is positioned to it.
The boundary rule re-applies text colour, and components that set an input @extend .retheme. In the classless build, the extended selector for a .contrast button outranked the button's own colour rule, so it got dark text on its grey fill in light mode, about 2.5:1. Wrapped in :where(), the rule has zero specificity: any colour a component sets wins, and it only fills in where none is. Found by the new cross-build tests.
tests/cross-build.spec.ts drives the real docs: every demo is screenshotted in each build it shows (Class, Classless, Kobalte), in light and dark, and must match the Class render pixel for pixel, apart from anti-aliasing. All renders happen in one run on one machine, so there are no stored images and it gives the same answer on any platform. A failure attaches both screenshots, a diff, and the computed styles that differ. By-design differences live in cross-build-exceptions.json with a reason; an exception that stops happening fails too. The divider demo's text moved from bare <p> to <div>, which the classless build styles as prose, so it compares the divider alone. tests/regression.visual.ts compares every render against stored screenshots, meant to run in Playwright's Docker image (tests/README.md). pnpm test runs the cross-build suite, and CI runs it on every pull request.
Selects reserve 2.25rem on the right with the chevron 0.75rem in, leaving 8px between text and chevron. Date and time inputs keep their own spacing, since their invisible picker button is positioned to it.
…by member _input.scss built every control from five mixins wrapped in five placeholder selectors, extended in a different combination per control, and some controls then undid what they inherited. The radio's look lived in _label.scss and only applied inside a label. Now: - _field.scss holds text, date and time inputs, selects and textareas. What controls share is written as grouped rules whose selector lists name their members, so finding a control in those lists tells you what it inherits; each control's own rule only adds or overrides. Measurements that have to stay in step, such as the icon inset and the gutter reserved for it, are named. - _checkbox.scss holds the checkbox and the radio, which is drawn on .radio itself, so it looks the same outside a label. - _file-input.scss, _color-input.scss and _range.scss hold one control each. Also fixes a bug the old structure hid: the date icon padding came in through a placeholder extend, which emitted it outside its @supports block, so Firefox drew our calendar icon beside its own. Every form control and pseudo-element computes identically to before in all three builds and both schemes (2,466 styles compared), except a radio outside a label, which is now drawn like one inside. The builds come out slightly smaller: classless and Kobalte by 762 and 809 bytes raw. All 60 cross-build tests pass.
Rows were --padding / 3 by --padding / 2; they are now 8px by 16px, 42px tall. Menubar triggers use .menu-item too, so they widen with them.
A "Composition" group in the docs: one page on how the pieces fit together (who owns layout, surface, text, controls and colour, and where spacing comes from), and two pages of composed sections modelled on sparkstonepdx.com and lzr.life, each with notes on which piece does which job. Class build only; no library CSS changed.
Rebuild the studio and community section pages from the actual structure and copy of sparkstonepdx.com and lzr.life, and swap the composition page's invented examples for real sections. Images are placeholders; the studio sections follow the site's own CSS, the community sections only its structure.
A zine has one fold, so trifold and quarters become a single choice; color and the sided topics stay checkboxes.
Every component page now ends with a Composition section: one or two generic patterns using the component with others, and notes on which piece does which job. The general Composition page moves into Getting started with generic copy, and the two site-specific section pages are removed.
Simple components (kbd, progress, skeleton, loading, colour and file inputs, textarea, checkbox, range, slider, toggle, tooltip, toast, divider, breadcrumbs, pagination) lose their Composition section; their useful patterns move to the component they combine with. The rest get more varied examples: an account menu and shortcut menu on Dropdown, a busy state and a toolbar on Button, tabs inside a card, counts on tabs, a details table, advanced options in a collapse, a destructive confirm dialog, and more.
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The nine-step scale is replaced by four inputs, --primary, --secondary, --neutral and --error, and fourteen colours derived from them on every element. Declaring the derived colours on * is what lets an input set on any element re-theme its subtree: a custom property declared once at the root is substituted there, so a descendant changing an input never reached it.
Colours keep the input's own hue and chroma and pin only the lightness, so a brand colour survives; chroma is capped per role and scheme to stay in gamut, using 2.x's stops so a vivid brand looks the same. Text is solid, surfaces and rules are a tinted mid-tone at partial opacity. Disabling is one declaration, --chroma: 0, instead of re-binding 29 variables per selector.
--line moves from 0.695 to 0.62 (light) and 0.54 (dark): the 2.x value failed WCAG 1.4.11 at about 2.7:1 for every brand tested. Measured across ten brands in both schemes, every text pair clears 4.5:1 and --line clears 3:1.
index.css: 60.2 kB to 38.4 kB raw, 7.3 kB to 6.3 kB gzip, 477 custom-property declarations to 95.