Releases: willink-oss/willink-design-system
v1.8.0 — catalog expansion (14 components) + @layer extension surface
npm lockstep 1.7.0 → 1.8.0 · @willink-labs/{tokens,tailwind-preset,css-tokens,react} · published to npmjs.org via OIDC Trusted Publisher (with SLSA provenance). Flutter willink_theme stays 1.5.0 (independent cadence). Roadmap milestone v1.9 (extensibility).
Highlights
- 14 new React components (25 → 39) — Popover, ScrollArea, Spinner, Empty, Kbd, ButtonGroup, Alert, Table, Pagination, Breadcrumb, ContextMenu, HoverCard, Collapsible, ToggleGroup. Each ships a
jest-axetest, a Storybook story, and an a11y-matrix row. @layer componentsextension surface (.wl-btn/.wl-card/.wl-input, ADR-0021 §3) — framework-agnostic component classes for non-React consumers, overridable with atomics.- Reduced-motion contract completed (ADR-0008 Layer 1) on DropdownMenu/Select + a
check-motion-contractregression gate. - Packaging:
homepage+bugsmetadata on all 4 packages.
Install
npm i @willink-labs/react@1.8.0 @willink-labs/tailwind-preset@1.8.0 @willink-labs/tokens@1.8.0 @willink-labs/css-tokens@1.8.0Out of scope
willink_theme(Flutter / pub.dev) — unchanged at 1.5.0.apps/registry— scaffolded (#93), not yet hosted; no consumer-facing change.
Verification
Layer 2 (ADR-0012): version × CHANGELOG × README agree; full local gate green (build / test / guardrails / contrast); tree-parity verified against the gated commit; published packages carry provenance attestation.
Related: ADR-0008 · ADR-0010 · ADR-0021 · roadmap/v1.9
v1.7.0 — dark-aware gradient text + bg-clip-text contrast gate
Lockstep npm MINOR 1.6.0 → 1.7.0 (@willink-labs/tokens · tailwind-preset · react · css-tokens). No Flutter release (willink_theme stays 1.5.0, ADR-0011).
Decisions: ADR-0018 · cycle v1.8 roadmap. PR #61.
What shipped
Dark-aware text-gradient-primary (tailwind-preset, real content). The bg-clip-text gradient heading painted its glyphs with the fixed brand → brand-glow pair (brand-600 → brand-500), which does not flip (ADR-0013) — so on the dark bg the worst endpoint rendered at 3.54:1, below AA, washing out. New preset-internal --color-gradient-primary-from/-to make the endpoints dark-aware: light byte-identical (brand → brand-glow, worst 4.23:1), dark brand-300 → brand-400 (worst 7.41:1), staying recognizably brand-purple. bg-gradient-primary / bg-gradient-ai unchanged (vivid bg behind white text).
Closed the audit blind spot the CEO caught twice. scripts/check-contrast.mjs gained a TEXT_GRADIENTS registry that checks every text-clipped gradient's worst endpoint against bg — required ≥ 4.5 in dark, report-only baseline in light — wired into CI via the existing tokens contrast gate. A deliberately-bad endpoint (the pre-1.7 fixed pair) turns the gate red (3.54:1 ✗, exit 1). bg-clip-text gradient headings are now first-class contrast-gate citizens.
Consumer contract for accent-on-gradient text. New docs/a11y/gradient-and-accent.md documents the rule the DS can't enforce (hue/lightness separation for custom accent text on gradient backgrounds — the i-willink.com hero blind spot).
tokens / css-tokens / react carry lockstep markers (the gradient vars are preset-internal — not semantic.json / css-tokens output / component refs, mirroring --color-gradient-subtle-end). reduced-motion behavior unaffected.
🤖 Generated with Claude Code
v1.6.0 — dark-mode link contrast fix (#58) + info-fg defer (#59)
Lockstep MINOR — 4 npm packages → 1.6.0 (@willink-labs/tokens · tailwind-preset · react · css-tokens). No Flutter release this cycle (willink_theme stays 1.5.0 — independent cadence, ADR-0011).
Button link dark contrast fix (#58)
Button variant="link" resting color used text-brand — the fixed brand-600 primitive, which is mode-invariant (ADR-0013) — so on the dark page background it rendered 3.54:1, below WCAG AA. Reproduced by the i-willink.com production sweep on /design-system. Re-pointed to the flipping text-brand-soft-fg (light brand-700 7.10:1 / dark brand-300 10.93:1 — both AAA). Hover (text-brand-hover) already flips and stays legible (dark 4.76:1).
The same bug class (fixed brand foreground on a flipping surface) was audited DS-wide: AccordionTrigger hover:text-brand → hover:text-brand-hover (also fixed); RadioGroupItem text-brand audited and kept (8px indicator dot is a graphical object under WCAG 1.4.11's 3:1 rule, which it clears). The contrast gate gained two required pairs (brand-soft-fg/bg, brand-hover/bg) in both modes.
Light-mode resting links are now one ramp step darker (brand-600 → brand-700) — a deliberate, contrast-improving MINOR default-visual change (ADR-0010). No migration required.
info-fg upstreaming verdict (#59) — deferred
Evaluated i-willink.com's local --color-info-fg (blue) for upstreaming. Verdict NO this cycle: single usage site, no info hue/surface family in the DS for it to belong to, below the ADR-0016 promote-on-reproduction bar. #59 left open with the reasoning as a comment — a defer, not a reject.
Decisions
ADR-0017 — Button link fix rationale (both fix options weighed) + info-fg defer verdict. Roadmap: docs/roadmap/v1.7.md.
Packages
Real content in @willink-labs/react; tokens / tailwind-preset / css-tokens carry lockstep markers (the fix reuses existing roles — no new token, variable, safelist entry, or generated var).
v1.5.0 — fg-* text-emphasis roles
v1.5.0 — fg-* text-emphasis roles
Upstreams the foreground emphasis ladder i-willink.com grew during its dark rollout (v1.5 Outcome 3) into the Design System as official semantic roles. The DS previously shipped only fg (strongest body) and muted (weakest supporting) — with no in-between steps, consumers reached into mode-invariant text-neutral-* for emphasis, which froze light grays under a dark root. These five roles close that gap and flip in dark mode like every other semantic role.
New roles (between fg and muted)
| role | utility | light | dark | contrast contract (on bg) |
|---|---|---|---|---|
fg-strong |
text-fg-strong |
neutral-800 | neutral-100 | ≥ 7 (AAA) — headings / strong runs |
fg-emphasis |
text-fg-emphasis |
neutral-700 | neutral-200 | ≥ 7 (AAA) — labels / links |
fg-secondary |
text-fg-secondary |
neutral-600 | neutral-300 | ≥ 4.5 (AA) — secondary body |
fg-subtle |
text-fg-subtle |
neutral-400 | neutral-500 | documented baseline — captions / meta / placeholders (non-body) |
fg-faint |
text-fg-faint |
neutral-300 | neutral-600 | documented baseline — disabled text / separators (non-text) |
fg and muted are unchanged (the ladder's anchors). Each role is a neutral-step alias + a willink.dark extension, so it rides the existing ADR-0013 dark-flip + consumer-override machinery with no new mechanism.
Packages (npm lockstep 1.4.1 → 1.5.0)
@willink-labs/tokens@1.5.0— 5 new semantic roles@willink-labs/tailwind-preset@1.5.0— light decls + both dark blocks +text-fg-*safelist@willink-labs/css-tokens@1.5.0— regen (5 vars intokens.css/tokens.semantic.css, flipped intokens.dark.css)@willink-labs/react@1.5.0— lockstep marker (no component change)
No flutter-v* tag — zero Flutter changes (willink_theme stays 1.5.0 per ADR-0011).
pnpm add @willink-labs/tokens@1.5.0 @willink-labs/tailwind-preset@1.5.0 @willink-labs/react@1.5.0 @willink-labs/css-tokens@1.5.0
For consumers
Replace local fg-* definitions (or text-neutral-* used for body emphasis) with the new text-fg-* utilities — they render byte-identical in light (the DS neutral scale is the slate scale) and gain a correct dark flip. A separate task migrates i-willink.com's local fg-* block to these roles.
Contrast policy
scripts/check-contrast.mjs enforces the per-role targets in both modes: fg-strong/fg-emphasis ≥ 7 (AAA) and fg-secondary ≥ 4.5 (AA) are required; fg-subtle/fg-faint are documented report-only baselines below the body-text floor (non-body tiers — not for content text).
Out of scope (v1.7)
Pastel-chip semantic pass, naskit diagram theming, theme-toggle UX (all i-willink.com consumer-side follow-ups).
Links
v1.4.1 — react dist ships 'use client' (RSC fix)
v1.4.1 — react dist ships 'use client' (RSC fix)
Lockstep PATCH cut for the npm group. Production-discovered in the clublink-platform rollout: @willink-labs/react@1.4.0 executes client-only code at module top level (createContext in FormField, hooks throughout) but the published dist carried no 'use client' directive — esbuild strips source-level directives during bundling — so next build crashed for any RSC consumer importing from a Server Component.
Consumers on Next.js App Router can now delete their client re-export shims and import @willink-labs/react directly from Server Components again (components remain Client Components, as before). No API or type change — the 1.4.1 index.d.ts is byte-identical to 1.4.0.
Packages
| Package | Version | Content |
|---|---|---|
@willink-labs/react |
1.4.1 | dist leads with 'use client' (tsup banner; rollup treeshake pass off — it deleted directives; ~4.7 KB dist cost) + build-output regression check |
@willink-labs/tokens |
1.4.1 | lockstep marker (no source change) |
@willink-labs/tailwind-preset |
1.4.1 | lockstep marker (no source change) |
@willink-labs/css-tokens |
1.4.1 | lockstep marker (no source change) |
No flutter-v* tag this cycle — willink_theme stays at 1.5.0 (ADR-0011).
Install
pnpm add @willink-labs/react@^1.4.1 @willink-labs/tailwind-preset@^1.4.1 @willink-labs/tokens@^1.4.1Regression guard
packages/react/scripts/check-dist-use-client.mjs runs inside pnpm build (CI gate per ADR-0012 Layer 0) and asserts every dist JS file starts with the directive — position matters for RSC, so it checks the first bytes rather than grepping for containment.
Verification
ADR-0012 Layer 0–2: full local gate at the cut (guardrails / 194 unit tests / pnpm -r build incl. playground + storybook / audit / contrast / flutter analyze + 66 tests + dry-run) / d.ts diff vs published 1.4.0 = identical (PATCH proof) / published-tarball smoke test: 'use client'; at byte 0 of dist/index.js, exactly 1 occurrence, SLSA provenance attested, workspace:* peerDeps resolved to 1.4.1 / npm view ×4 == 1.4.1. PR: #53.
v1.4.0 — FormField compound (npm lockstep)
v1.4.0 — FormField compound
Lockstep MINOR cut for the npm group. Closes the longest-deferred React roadmap item (deferred at the v1.0 Phase 9.1 audit, carried through v1.1–v1.3): a FormField compound that automates the id / htmlFor / aria-describedby / aria-invalid wiring consumers previously hand-wrote per field.
Packages
| Package | Version | Content |
|---|---|---|
@willink-labs/react |
1.4.0 | FormField compound — 25th component |
@willink-labs/tailwind-preset |
1.4.0 | text-danger safelist entry (FormFieldError class string) |
@willink-labs/tokens |
1.4.0 | lockstep marker (no source change) |
@willink-labs/css-tokens |
1.4.0 | lockstep marker (no source change) |
No flutter-v* tag this cycle — willink_theme stays at 1.5.0 (ADR-0011).
API
import { FormField, FormFieldControl, FormFieldDescription, FormFieldError, FormFieldLabel, Input } from "@willink-labs/react";
<FormField>
<FormFieldLabel required>Email</FormFieldLabel>
<FormFieldControl>
<Input type="email" />
</FormFieldControl>
<FormFieldDescription>会社のメールアドレスを入力してください。</FormFieldDescription>
<FormFieldError>{errors.email}</FormFieldError>
</FormField>- ids generated via
useId()(collision-free in lists); labelhtmlForpre-wired FormFieldControlis a RadixSlot(zero new runtime deps) — injectsid+ mergedaria-describedby+aria-invalidonto any single control (Input / Textarea / native elements / Radix triggers)aria-describedbyonly ever references rendered nodes (description/error detected among direct children, SSR-safe)FormFieldErrorrenders only with content, carriesrole="alert", and drives the control'saria-invalidautomatically (invalidprop onFormFieldas override)- Exported types:
FormFieldProps/FormFieldLabelProps/FormFieldControlProps/FormFieldDescriptionProps/FormFieldErrorProps
Design rationale (incl. what was deliberately left out — no CVA, no validation/RHF coupling, no layout props): ADR-0015. Cycle doc: docs/roadmap/v1.4.md.
Install
pnpm add @willink-labs/react@^1.4.0 @willink-labs/tailwind-preset@^1.4.0 @willink-labs/tokens@^1.4.0Out of scope (v1.5 candidates)
Dark-mode consumer rollout / Flutter Checkbox・Switch・RadioGroup wrappers / Popover・Combobox・Breadcrumb・Stepper (Discussion-gated) — see the v1.4 roadmap.
Verification
ADR-0012 Layer 0–2: 171 unit tests (12 new, incl. jest-axe) / Storybook addon-a11y story surface / full local gate before tagging (guardrails・build・audit・contrast・flutter analyze+test+dry-run) / post-publish npm view ×4 == 1.4.0 with OIDC provenance.
v1.3.0 — WordPress consumption (4 npm packages lockstep)
@willink-labs/{tokens, tailwind-preset, css-tokens, react} 1.3.0
WordPress consumption ships — and the "WordPress package" roadmap item closes without a package. Two pilots proved @willink-labs/css-tokens is the WordPress integration: wp-modern-starter-kit (canonical wiring pattern) and esperanza-wp-theme (production — 10/10 variable parity, byte-identical dist except inert variable additions, 4/4 visual regression; consumer-side checks ran in that repo). Decision record: ADR-0014.
- css-tokens — both pilot findings fixed:
- Root-level proxy CSS files (
tokens.css/tokens.scale.css/tokens.semantic.css/tokens.dark.css/tokens.primitives.css): plain-path resolvers — postcss-import, and therefore the Tailwind v3 CLI and classic WP PostCSS toolchains — never read the packageexportsmap, so the documented specifiers were unresolvable (the pilots' BLOCKER). Every documented path is now a physical file; theexportsmap stays, plus a./src/*.csspassthrough keeps the pilots' workaround imports valid. - New export
tokens.primitives.css— color-free primitives: radius + duration + easing only (10 vars, zero--color-*/--shadow-*). For consumers that keep their own palette and adopt only the DS shape/motion contract — esperanza's radius/motion-only contract no longer ingests 50 inert color/shadow vars.
- Root-level proxy CSS files (
- tokens / tailwind-preset / react — lockstep markers, no source change.
Install
pnpm add @willink-labs/css-tokens@^1.3.0
# WordPress / PostCSS toolchains can now use the documented plain paths:
# @import "@willink-labs/css-tokens/tokens.css";
# @import "@willink-labs/css-tokens/tokens.primitives.css";All four packages published via npm OIDC Trusted Publisher with --provenance (SLSA v1). No Flutter release this cycle — willink_theme stays at 1.5.0 (ADR-0011).
Out of v1.3 scope (v1.4 candidates)
FormField compound export (strongest candidate) · Flutter Checkbox/Switch/RadioGroup wrappers · Popover / Combobox / Breadcrumb / Stepper (Discussion-gated).
Roadmap: docs/roadmap/v1.3.md (PR #50).
v1.2.0 — Dark mode (4 npm packages lockstep)
@willink-labs/{tokens, tailwind-preset, css-tokens, react} 1.2.0
Dark mode ships. Auto via prefers-color-scheme; explicit control with <html data-theme="dark"> / "light". Semantic tokens flip, primitives and the numeric brand scale stay invariant — components need zero dark: variants. Design record: ADR-0013.
- tokens —
willink.darkDTCG $extensions on all 16 flipping roles; 5 new surface roles (surface-subtle/muted,track,surface-inverted(-fg)); WCAG contrast audit (scripts/check-contrast.mjs) now a permanent CI gate — dark pairs pass at 5.4–19.3:1. - tailwind-preset — full dark flip in both selector paths (media +
data-theme), dark shadows, dark-awarebg-gradient-subtle;animate-pulsejoins theprefers-reduced-motionsafety net (closes the Skeleton flag carried since 0.13.0). - css-tokens — new
tokens.dark.cssexport for non-Tailwind consumers. - react — 15 primitive-utility leaks across 11 components migrated to the new semantic roles; light-mode rendering is pixel-identical. Skeleton gains
motion-reduce:animate-none. Regression guard added to the token-name test gate.
Storybook now has a light / dark / auto toolbar toggle. Roadmap: docs/roadmap/v1.2.md (PRs #43–#48).
willink_theme 1.5.0 — WillinkTheme.willinkDark()
willink_theme 1.5.0
Dark mode for Flutter — WillinkTheme.willinkDark() mirrors the web semantic flips (ADR-0013) onto a manual Brightness.dark ColorScheme (surface ladder from neutral-950/900/800/700, brand-600 invariant, containers brand-950/300, error red-500). A shared _base() builder structurally guarantees light/dark slot parity. All 9 components adapt with no changes — verified by tests rendering Button / SnackBar / TabBar under the dark theme (66/66).
MaterialApp(
theme: WillinkTheme.willink(),
darkTheme: WillinkTheme.willinkDark(),
themeMode: ThemeMode.system,
)v1.1.0 — Storybook catalog + sonner 2 migration (4 npm packages lockstep)
@willink-labs/{tokens, tailwind-preset, css-tokens, react} 1.1.0
First post-freeze MINOR. Strict SemVer per ADR-0010 — ^1.0.0 consumers update safely.
@willink-labs/react (the substantive package this cut)
- Toast engine: sonner 1.7.4 → 2.0.7 (#30) — the
toasttype is unchanged;Toasterkeeps the two props sonner 2.x removed as deprecated compat props (loadingIconmapped ontoicons.loading;pauseWhenPageIsHiddennow a documented no-op). Classified MINOR; details in the CHANGELOG. - Slider a11y fix (#38) — single-thumb
Slidernow forwardsaria-labelto the thumb (axearia-input-field-name); multi-thumb ranges keep Radix "Minimum"/"Maximum". Found by the new Storybook a11y pass.
tokens / tailwind-preset / css-tokens
Lockstep markers — no source change.
Also in this cycle (repo, not packages)
- Storybook 10 catalog at
apps/storybook— all 24 components,@storybook/addon-a11y(axe per story) cross-linked with the WCAG 2.1 AA matrix (#31, #34–#37) - v1.1 roadmap shipped: docs/roadmap/v1.1.md