@karmo-co/drift-kit-reference 0.0.0-stage → 23.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (93) hide show
  1. package/LICENSE +7 -0
  2. package/README.md +24 -2
  3. package/bin/drift-kit-ref.js +197 -0
  4. package/manifest/components/accordion.md +47 -0
  5. package/manifest/components/action-bar.md +51 -0
  6. package/manifest/components/alert.md +53 -0
  7. package/manifest/components/app-shell.md +43 -0
  8. package/manifest/components/avatar.md +42 -0
  9. package/manifest/components/body.md +48 -0
  10. package/manifest/components/box.md +58 -0
  11. package/manifest/components/breadcrumb.md +59 -0
  12. package/manifest/components/button.md +46 -0
  13. package/manifest/components/calendar.md +69 -0
  14. package/manifest/components/card.md +71 -0
  15. package/manifest/components/checkbox.md +43 -0
  16. package/manifest/components/choice-card.md +61 -0
  17. package/manifest/components/code-input.md +56 -0
  18. package/manifest/components/date-picker.md +66 -0
  19. package/manifest/components/day-of-month.md +54 -0
  20. package/manifest/components/detail-list.md +37 -0
  21. package/manifest/components/divider.md +33 -0
  22. package/manifest/components/dropdown.md +52 -0
  23. package/manifest/components/dropzone.md +56 -0
  24. package/manifest/components/eyebrow.md +45 -0
  25. package/manifest/components/feedback-badge.md +35 -0
  26. package/manifest/components/glider.md +34 -0
  27. package/manifest/components/heading.md +47 -0
  28. package/manifest/components/hero.md +46 -0
  29. package/manifest/components/icon-button.md +41 -0
  30. package/manifest/components/icon-tile.md +44 -0
  31. package/manifest/components/icon.md +30 -0
  32. package/manifest/components/input.md +63 -0
  33. package/manifest/components/link.md +42 -0
  34. package/manifest/components/loading-spinner.md +42 -0
  35. package/manifest/components/logo.md +38 -0
  36. package/manifest/components/modal.md +53 -0
  37. package/manifest/components/pagination.md +54 -0
  38. package/manifest/components/payment-form.md +29 -0
  39. package/manifest/components/price-summary.md +30 -0
  40. package/manifest/components/radio.md +42 -0
  41. package/manifest/components/section-heading.md +41 -0
  42. package/manifest/components/select.md +99 -0
  43. package/manifest/components/shield.md +34 -0
  44. package/manifest/components/sidebar.md +73 -0
  45. package/manifest/components/stepper.md +53 -0
  46. package/manifest/components/switch.md +45 -0
  47. package/manifest/components/table.md +77 -0
  48. package/manifest/components/tabs.md +43 -0
  49. package/manifest/components/tag.md +43 -0
  50. package/manifest/components/textarea.md +52 -0
  51. package/manifest/components/toast.md +52 -0
  52. package/manifest/components/tooltip.md +42 -0
  53. package/manifest/components/top-bar.md +60 -0
  54. package/manifest/components/vehicle.md +43 -0
  55. package/manifest/index.json +1171 -0
  56. package/manifest/migrations/1.0.0.md +50 -0
  57. package/manifest/migrations/10.0.0.md +154 -0
  58. package/manifest/migrations/11.0.0.md +24 -0
  59. package/manifest/migrations/12.0.0.md +38 -0
  60. package/manifest/migrations/13.0.0.md +50 -0
  61. package/manifest/migrations/14.0.0.md +148 -0
  62. package/manifest/migrations/15.0.0.md +140 -0
  63. package/manifest/migrations/16.0.0.md +196 -0
  64. package/manifest/migrations/17.0.0.md +19 -0
  65. package/manifest/migrations/18.0.0.md +23 -0
  66. package/manifest/migrations/19.0.0.md +15 -0
  67. package/manifest/migrations/2.0.0.md +18 -0
  68. package/manifest/migrations/20.0.0.md +43 -0
  69. package/manifest/migrations/21.0.0.md +97 -0
  70. package/manifest/migrations/22.0.0.md +17 -0
  71. package/manifest/migrations/23.0.0.md +86 -0
  72. package/manifest/migrations/3.0.0.md +34 -0
  73. package/manifest/migrations/4.0.0.md +147 -0
  74. package/manifest/migrations/5.0.0.md +73 -0
  75. package/manifest/migrations/6.0.0.md +67 -0
  76. package/manifest/migrations/7.0.0.md +71 -0
  77. package/manifest/migrations/8.0.0.md +16 -0
  78. package/manifest/migrations/9.0.0.md +107 -0
  79. package/manifest/rules.md +37 -0
  80. package/manifest/setup.md +70 -0
  81. package/manifest/tokens/border-width.md +12 -0
  82. package/manifest/tokens/border.md +147 -0
  83. package/manifest/tokens/font.md +90 -0
  84. package/manifest/tokens/icon.md +84 -0
  85. package/manifest/tokens/interactive.md +53 -0
  86. package/manifest/tokens/layout.md +8 -0
  87. package/manifest/tokens/motion.md +97 -0
  88. package/manifest/tokens/radius.md +20 -0
  89. package/manifest/tokens/shadow.md +28 -0
  90. package/manifest/tokens/spacing.md +35 -0
  91. package/manifest/tokens/surface.md +138 -0
  92. package/manifest/tokens/text.md +92 -0
  93. package/package.json +20 -4
@@ -0,0 +1,50 @@
1
+ ## 1.0.0 — 2026-08-18
2
+
3
+ The source is reorganised by token collection and by component, and the package
4
+ now has exactly one CSS entry point.
5
+
6
+ ### Breaking
7
+
8
+ - Per-layer exports removed. `./core.css`, `./tokens.css`,
9
+ `./components-tokens.css` and `./components.css` are gone; everything is
10
+ `./drift-kit.css`. A layered interface let a consumer assemble an incoherent
11
+ subset — component CSS without its tokens — which is a silent-failure class,
12
+ not a feature.
13
+ - Core Drift is no longer a vendored CDN snapshot. The upstream token layers are
14
+ generated from component-library's own SCSS partials (`refresh:tokens`
15
+ replaces `refresh:core`), which drops 193 declarations that the CDN snapshot
16
+ carried and the SCSS source does not: 95 legacy `--ui-*` palette
17
+ declarations, 68 declared solely inside component-library's hashed React
18
+ CSS-module selectors, and 30 more in hashed `@keyframes`/`@property`/
19
+ `@supports` blocks and two `.pretty-scrollbar` rules (1,264 CSS-module rules
20
+ removed in total). All 193 are accounted for — 0 unexplained. Migrating the
21
+ `--ui-*` palette and the extracted CSS-module rules into the three Forge
22
+ prototypes that use them is a later project stage, not yet done. The bundle
23
+ falls from 519,815 to 334,060 bytes (507.6 KB → 326.2 KB, a 181 KB / 35.7%
24
+ reduction).
25
+
26
+ #### Structure
27
+
28
+ - `src/tokens/**` mirrors component-library's collection layout — brand,
29
+ responsive, dynamic, alias, mapped — so an upstream diff maps 1:1 onto our
30
+ files. Our extensions live below an `UPSTREAM … END UPSTREAM` fence in the
31
+ same group file; `refresh:tokens` rewrites only the fenced bytes.
32
+ - `src/components/<name>/` holds each component's `tokens.css`, `styles.css` and
33
+ `behaviour.js` together, replacing two files that held eleven components
34
+ between them.
35
+ - `src/manifest.js` is the only place cascade order is encoded: 87 CSS entries
36
+ and 7 JS entries, matching the 87 `.css` files on disk one-for-one.
37
+ - The bundle ships 6 self-hosted `@font-face` rules (Inter and Syne, 6
38
+ `.woff2` files) — unchanged from the retired snapshot.
39
+
40
+ ##### Gates
41
+
42
+ - The build fails on: a CSS file absent from the manifest, a manifest entry that
43
+ does not exist, a surviving `@import`/`@use`, component styles ordered before
44
+ component tokens, a malformed fence, or a Karmo token shadowed by a later
45
+ upstream fence (`check:placement`).
46
+ - `npm test` (11 tests) proves the restructure lost nothing: canonical-form
47
+ equality against a frozen pre-restructure bundle, and (selector, property,
48
+ value) parity between the converted upstream tokens and the retired
49
+ snapshot.
50
+
@@ -0,0 +1,154 @@
1
+ ## 10.0.0 - 2026-09-17
2
+
3
+ Breaking change to Input's markup. Anything that hand-wrote
4
+ `dk-component-input` markup needs the new shape below.
5
+
6
+ - **The wrapping `<label>` doubled as the accessible name and silently
7
+ absorbed the caption.** Measured with the new contract gate (see below)
8
+ before any fix: a field labelled "Full name" with a "Helper text goes
9
+ here." caption announced as one string, `"FULL NAME Helper text goes
10
+ here."`, and had no separate accessible description at all (empty string).
11
+ The all-caps portion is the label's own `text-transform: uppercase`
12
+ bleeding into the accessible-name computation, not a second defect.
13
+ - **Fixed by giving the label and the field their own elements.** The root
14
+ is now `<div class="dk-component-input">`, optionally carrying
15
+ `data-size="small"`, `"medium"` or `"large"`. The label is a real
16
+ `<label class="dk-component-input__label" for="ID">`. The field wrapper is
17
+ `<div class="dk-component-input__field">` (a `<span>` still renders
18
+ correctly, `<div>` is what the docs now teach). Measured after the fix:
19
+ the accessible name is the label alone, `"Full name"`, and the caption is
20
+ wired in as the accessible description through
21
+ `<p class="dk-component-input__caption" id="ID-caption">` plus
22
+ `aria-describedby="ID-caption"` on the control. `check:contract` now
23
+ passes for Input.
24
+ - **Migrated Forge's `model-approval` prototype to the new shape: 24
25
+ instances, 12 in `index.html` and 12 in `v2.html`.** Every one already had
26
+ a control `id`, so the migration reused it rather than inventing one.
27
+ `grep -c` for the old `<label class="dk-component-input">` shape returns 0
28
+ in both files. Input's own docs entry in `design-system/registry.js`
29
+ carried one further instance, migrated separately while that entry was
30
+ rebuilt, and Modal's own Preview and Trigger
31
+ examples in the same file carried two more, migrated in the final review
32
+ fix wave so the docs no longer teach the retired shape anywhere on the
33
+ Modal page. A handful of other embedded Input snippets outside Input's
34
+ own docs, `model-approval`, and Modal, Forge's root `index.html` and
35
+ `assets/forge-cards.js`, still use the old wrapping-label shape. They
36
+ render fine (the class names carry the CSS regardless of tag), but they
37
+ are out of scope for this change and were left alone.
38
+ - **Fix: `outline: none` on the control silently removed the focus ring
39
+ from a `<textarea>` or `<select>` field, and the disabled border with
40
+ it.** Both the resting focus ring and the disabled-border hook were
41
+ narrowed to `:has(input:focus-visible)` and `:has(input:disabled)` while
42
+ `outline: none` stayed on all three control types, `input`, `textarea`
43
+ and `select`. A textarea or select inside a field therefore lost its
44
+ native outline and matched no rule that restored one: no focus indicator
45
+ at all, and no border change when disabled. Both selectors now read
46
+ `:has(:is(input, textarea, select):focus-visible)` and
47
+ `:has(:is(input, textarea, select):disabled)`. Verified with a keyboard
48
+ probe on a textarea field: before the fix the field's border and
49
+ box-shadow stayed at their resting values on real focus; after, the
50
+ border switches to the primary ring colour and the box-shadow shows the
51
+ usual 4px focus ring, matching an `<input>` field exactly.
52
+ - **The error state is `aria-invalid="true"` on the control, not a modifier
53
+ class.** `.dk-component-input--error` and `.dk-component-input--disabled`
54
+ are both gone; the CSS now keys off `:has(input[aria-invalid="true"])` and
55
+ `:has(input:disabled)`. The visual state and what a screen reader
56
+ announces read off the same attribute, so they cannot drift apart the way
57
+ a class and an ARIA attribute set separately could.
58
+ - **Three field heights, 38 / 44 / 52px for small / medium / large, all
59
+ `border-box`, each moving the box and the type together at 14 / 16 /
60
+ 18px.** None is written as a literal: each is `calc()`'d from the
61
+ primitive scale, padding doubled plus the line box. Small and medium land
62
+ on Button's first two rungs so a field and a button share a row cleanly,
63
+ and Forge's `check:contract` measures a rendered field against a rendered
64
+ button to keep them there. Large deliberately does not match Button's
65
+ 56px: a 52px field is the proportion a form wants, and a large field
66
+ usually stands alone.
67
+ - **`medium` is a real value.** It was `small | large` with the middle rung
68
+ unnamed. Omitting the attribute still lands on the same rule, but Button
69
+ names its middle rung and one component spelling the middle size while
70
+ another leaves a hole is not something anyone should have to discover.
71
+ - **That same derivation fixes the 46px these three heights replace.** The
72
+ old field was `min-height: 44px` under `box-sizing: content-box` with a
73
+ 1px border, which rendered at 46px, two pixels proud of Button's 44px
74
+ medium in the same row. The field is `border-box` now, so it lines up
75
+ exactly.
76
+ - **Two new slots, leading and trailing**, `.dk-component-input__leading`
77
+ and `__trailing`, sizing a child `svg` or `i` to 20px, stepping to 16px
78
+ at `data-size="small"` so they match the action buttons beside them. For
79
+ a currency prefix, a unit, or a search glyph.
80
+ - **The field is flush with its surface and defined by its border.**
81
+ Background is `--dk-surface-current`, so the field tracks
82
+ `data-surface` at every level instead of holding one colour. The border
83
+ and the placeholder read three new mapped tokens,
84
+ `--dk-border-field-default`, `--dk-border-field-hover` and
85
+ `--dk-text-field-placeholder`, declared per theme because the neutral
86
+ ramp is not mirrored between light and dark and every borrowed token
87
+ therefore reads wrong in one of them. Hover moves the border and the
88
+ fill together.
89
+ - **Two in-field actions**, `.dk-component-input__action` with
90
+ `data-input-reveal` or `data-input-clear`, wired by a new
91
+ `components/input/behaviour.js` (`dk.component.input.init()`, auto-run
92
+ on `DOMContentLoaded`). The reveal toggle flips `type` between
93
+ `password` and `text`, moves `aria-pressed`, and swaps its accessible
94
+ name; both glyphs live in the markup and CSS shows one, so the picture
95
+ cannot drift from the announced state. The clear button empties the
96
+ control, fires a bubbling native `input` event, and hides itself while
97
+ the field is empty. These are real buttons in the tab order with their
98
+ own names and focus rings, which is why the field's own ring keys off
99
+ `:has(input:focus-visible)` rather than `:focus-within`.
100
+ - **Autofill no longer looks autofilled.** Chrome and Safari paint their
101
+ own fill at a level `background-color` cannot beat; the control now
102
+ stays genuinely transparent so it keeps following the field behind it,
103
+ including on hover.
104
+ - **The required asterisk is warning, not error.** An unfilled required
105
+ field is a prompt, not a failure, and a form of ten of them should not
106
+ open entirely in red.
107
+ - **The error message has its own colour token**,
108
+ `--dk-component-input-error-color`. It read `--dk-text-error-default`,
109
+ which is the status fill and glyph rung and measures 3.48:1 on a white
110
+ page, below AA for body text.
111
+ - **A React component**: `Input`, `InputProps`, `InputSize`, exported from
112
+ `src/react/index.ts`. Falls back to `useId()` when the caller passes no
113
+ `id`, wires `aria-describedby` to caption then error (caption first) when
114
+ both are present, and sets `aria-invalid` only when an `error` prop is
115
+ given, never a class. `revealable` and `clearable` give the two in-field
116
+ actions; clearing goes through the prototype's value setter so it works
117
+ for a controlled and an uncontrolled field alike.
118
+ - **A new gate: `check:contract`, added to Forge.** A stylesheet cannot
119
+ assert what a screen reader announces or what height a control renders
120
+ at, so this gate loads each documented component's live page, takes an
121
+ accessibility snapshot, and measures `getBoundingClientRect()`, against a
122
+ `contract` block declared in that component's registry entry. Input is
123
+ the first component to carry one. It also checks cross-component height
124
+ cohorts, which is what replaced Input reading Button's padding tokens
125
+ directly: an agreement between two components is a thing to assert, not
126
+ to implement by coupling.
127
+
128
+ **Kit-wide, not Input.** Two files that affect every component:
129
+
130
+ - **`kit/reset.css`** zeroes the browser's own margin on anything carrying
131
+ a `dk-component-` class, and gives form controls `font: inherit`. Every
132
+ text-bearing component was doing this by hand and Input forgot, which
133
+ put its caption 14px out of place. Scoped to kit classes and written at
134
+ zero specificity with `:where()`, so a consumer's markup is untouched
135
+ and any component rule wanting a margin still wins. A new Forge gate,
136
+ `check:ua-defaults`, proves nothing else is falling through.
137
+ - **`kit/selection.css`** paints selected text in the brand primary with
138
+ white on top, the same in both themes. Scoped to `[data-brand]`, the
139
+ attribute a page sets to opt into the kit, because selection is
140
+ document-wide and this stylesheet loads into applications whose markup
141
+ the kit does not own.
142
+
143
+ **Migration.** Replace `<label class="dk-component-input">...</label>` with
144
+ `<div class="dk-component-input">...</div>`. Turn
145
+ `<span class="dk-component-input__label">Text</span>` into
146
+ `<label class="dk-component-input__label" for="ID">Text</label>`, where `ID`
147
+ is the control's own `id`. Turn
148
+ `<span class="dk-component-input__field">` into
149
+ `<div class="dk-component-input__field">` (optional, a span still works).
150
+ If there is a caption, give it `id="ID-caption"` and add
151
+ `aria-describedby="ID-caption"` to the control. Replace any reliance on
152
+ `.dk-component-input--error` or `.dk-component-input--disabled` with
153
+ `aria-invalid="true"` or the native `disabled` attribute.
154
+
@@ -0,0 +1,24 @@
1
+ ## 11.0.0 - 2026-09-18
2
+
3
+ - **Search input is now called Search bar**, and its source directory moves
4
+ with it. Input takes `type="search"` and gets a suggested magnifier and a
5
+ clear button with it, so there are genuinely two things, and one name for both
6
+ gave nobody a way to say which they meant. This is the prominent one: the
7
+ large rung, an elevation no other field has, one per page, no visible label.
8
+ A form field sits in the page; a search bar sits on top of it.
9
+ - **BREAKING: the class and token names follow.** `.dk-component-search`
10
+ becomes `.dk-component-search-bar`, and the parts go from a hyphen to the
11
+ `__` every other component in the kit uses:
12
+ `.dk-component-search-icon` and `.dk-component-search-input` become
13
+ `.dk-component-search-bar__icon` and `.dk-component-search-bar__input`. Search
14
+ bar was the only component naming its parts with a hyphen, so fixing the name
15
+ and leaving the convention broken would have been half a job. The token prefix
16
+ moves with them, `--dk-component-search-*` to `--dk-component-search-bar-*`,
17
+ a clean 1:1 rename of six names.
18
+
19
+ **Migration.** Search and replace, longest first so the root does not eat the
20
+ parts: `dk-component-search-input` to `dk-component-search-bar__input`, then
21
+ `dk-component-search-icon` to `dk-component-search-bar__icon`, then the bare
22
+ `dk-component-search` to `dk-component-search-bar`, then
23
+ `--dk-component-search-` to `--dk-component-search-bar-`.
24
+
@@ -0,0 +1,38 @@
1
+ ## 12.0.0 - 2026-09-18
2
+
3
+ **Icon button, rebuilt.** It was a 34-line port with no token collection, the
4
+ only component in the kit without one.
5
+
6
+ - **BREAKING: `.dk-component-icon-btn` becomes `.dk-component-icon-button`**,
7
+ matching its own directory and the kit's no-abbreviation convention. Every
8
+ consumer is migrated in this release: 23 tags in Forge and five inside the kit
9
+ (Toast's close button, Dropzone's clear, three comments).
10
+ - **BREAKING: it now lines up with Button.** Measured before: 28 / 32 / 40 / 48
11
+ against Button's 40 / 46 / 58 / 66 at the same size names, so an icon button
12
+ beside a text one was 12 to 18px short at every rung but the smallest. Height
13
+ is now derived the way Button's is, from the same padding and line-height
14
+ primitives, NOT from Button's own tokens, which would be a
15
+ component-to-component dependency. Three `check:contract` cohorts assert the
16
+ pairs so it cannot drift back.
17
+ - **BREAKING: four rungs, `extra-small` to `large`.** small, medium and large
18
+ are Button's exactly; `extra-small` sits below Button's ladder because a text
19
+ button that small stops being pressable and an icon one does not. Measured
20
+ across Forge, every icon button in the product sat at 28, 32 or 40px, all at
21
+ or below Button's smallest, and nothing used the old top two rungs. Forge's
22
+ usages are remapped to the nearest new rung, so rendered sizes move by at most
23
+ 2px.
24
+ - **BREAKING: `.dk-component-icon-btn--subtle` retires into `data-variant`.**
25
+ It was documented as "the transparent-at-rest variant" and set
26
+ `background-color: var(--dk-surface-default-body)`: an opaque white square on
27
+ any surface that was not white. `data-variant="plain"` is the real thing.
28
+ Three variants now, `subtle` (the default, and the look this already had),
29
+ `plain`, and `raised`, which pairs with Button's raised.
30
+ - **The accessible name is asserted, not assumed.** With no text, `aria-label`
31
+ is the only thing between this control and "button" being all a screen reader
32
+ can say about it, so `check:contract` now checks it.
33
+ - Gone with the rewrite: five hardcoded icon sizes, a hardcoded 4px focus ring, a
34
+ `scale(0.9)` press that pressed harder than every other control in the kit, and
35
+ an alias colour read straight into a component rule.
36
+ - Full docs: api table, three examples including one proving the row alignment,
37
+ and a playground.
38
+
@@ -0,0 +1,50 @@
1
+ ## 13.0.0 - 2026-09-19
2
+
3
+ **Breaking: the icon button's `plain` variant now has a border.** The borderless
4
+ treatment it used to have is called `ghost`. Rename `data-variant="plain"` to
5
+ `data-variant="ghost"` to keep the old look, or leave it to gain the edge.
6
+
7
+ - **Button and the icon button now carry the same neutral ladder**, four rungs
8
+ meaning the same thing in both: `ghost` has no chrome until you touch it,
9
+ `subtle` is a filled tint, `plain` is flat on its surface with an edge,
10
+ `raised` is lifted above it. `plain` used to mean borderless in one component
11
+ and bordered in the other, which is the whole reason for this release.
12
+ - **New: `ghost` on Button.** It had no borderless rung at all. `accent` looks
13
+ like one and is not: it paints on-solid text for a coloured backdrop, so on an
14
+ ordinary page it is invisible rather than quiet.
15
+ - **New: `ghost` on the icon button**, which is its old `plain`, unchanged.
16
+ - **New: a bordered `plain` on the icon button**, mirroring Button's exactly:
17
+ the panel's own colour at rest, read by its edge, where `raised` is lifted.
18
+ - Button's `plain` and `raised` are untouched. `check:button` reports 24 of 24
19
+ existing combos byte-identical, and the gate now covers `ghost` at all four
20
+ sizes from its first day.
21
+ - A bordered rung is 2px taller than a borderless one at the same size, in both
22
+ components. That is the existing deliberate behaviour, not a new one, and it
23
+ is why `check:contract`'s height cohorts measure the default variant.
24
+ - **Fix: `ghost` and `plain` read the control family unshifted.** They start a
25
+ state late at REST, which is what makes them what they are, and they used to
26
+ run a state behind for the whole interaction as well, so every label sat on a
27
+ fill it was not chosen for. On dark warning level 2, where the ladder
28
+ legitimately flips from a light rung to a dark one between rest and hover, a
29
+ pressed label picked for `surface-control-active` landed on
30
+ `surface-control-hover` at 2.76:1. They now differ from `subtle` and `raised`
31
+ at rest and nowhere else.
32
+ - **Fix: a border must clear the PANEL, not only its own fill.** `plain` rests
33
+ on the panel, so its edge sits on the panel: on dark warning level 1 the rung
34
+ one step from `surface-control-default` is 800, which IS the panel's rung, and
35
+ the edge measured 1.00 against it. Invisible, in the variant whose whole job is
36
+ to be read by its edge.
37
+ - **Fix: `data-status="error"` did nothing on `ghost`.** The playground offered
38
+ the status and no CSS rule existed for it, so selecting it changed nothing.
39
+ `ghost · error` is now the same ladder `subtle` and `plain` carry, minus the
40
+ resting fill and minus the edge: transparent with an error label and glyph at
41
+ rest, one step of the error ramp on hover, two on press. Its readings are
42
+ identical to `plain · error`'s, combo for combo.
43
+ - `check:contrast` now measures `ghost`, `ghost · error` and `plain · error`.
44
+ The last of those has existed since 7.1.0 and had never been gated.
45
+ - Those two fixes move 20 of Button's contrast readings. Twelve improve and
46
+ three of them cross UP over AA (`Light s2 raised hover` 3.82 to 7.14,
47
+ `Light s2 raised active` 4.19 to 6.45, `Dark s2 raised active` 3.44 to 4.58);
48
+ eight decrease and all stay above it, the lowest at 4.58. Readings below AA go
49
+ from 172 to 169. Both baselines re-saved deliberately.
50
+
@@ -0,0 +1,148 @@
1
+ ## 14.0.0 - 2026-09-22
2
+
3
+ - **BREAKING: Search bar is gone. It is `data-raised` on Input.** The component
4
+ called itself "a preset of Input, not a second field implementation" in its
5
+ own header, and by the end that was literally true: its heights, its type, its
6
+ border, its radius, its placeholder colour, its slot gap, its glyph size, its
7
+ font weight and both halves of its transition all resolved through
8
+ `--dk-component-input-*`. What it still owned was a lift, a roomier padding
9
+ and a magnifier that tinted on focus. A preset costing 145 lines of CSS, nine
10
+ tokens, a documentation page and an entry in four gates is an expensive way to
11
+ say "Input with a shadow".
12
+
13
+ ```text
14
+ <label class="dk-component-search-bar"> → <div class="dk-component-input" data-raised data-size="large">
15
+ <svg class="dk-component-search-bar__icon">… → <div class="dk-component-input__field">
16
+ <input class="dk-component-search-bar__input"> → <span class="dk-component-input__leading">…</span>
17
+ </label> → <input type="search" aria-label="Search vehicles">
18
+ → </div>
19
+ → </div>
20
+ ```
21
+
22
+ The bar was the LARGE rung, so keep `data-size="large"`: Input's unmarked
23
+ default is medium. The `aria-label` is still required and for the same reason,
24
+ now that there is no visible label rather than no label text.
25
+ - **`data-raised` is the shadow and nothing else.** The bar's roomier inline
26
+ padding came across with it and was cut on review: it made one attribute two
27
+ unrelated changes, and a field whose text starts further in than the field
28
+ below it reads as a different component rather than the same one lifted.
29
+ Padding comes from the size rung, where every other field reads it from, so a
30
+ raised field and a flush field of the same size are the same box with a
31
+ shadow under one of them.
32
+ - **Nothing else about the look changed.** A raised, large, unlabelled Input was
33
+ measured against the old bar on eight properties before the component was
34
+ deleted, and matched on all eight, at 52.0px both. The inline padding is the
35
+ one deliberate difference: 10px at every rung rather than the bar's 16px.
36
+ - **The focus ring survives the hover shadow**, which it did not before. Both
37
+ are `box-shadow`, so the bar's hover shadow overwrote the ring and a focused
38
+ search bar under the pointer had no focus state at all. The raised rules write
39
+ both in one declaration.
40
+ - **The magnifier still goes primary on focus, and one rung on hover**, and that
41
+ now happens on every `__leading` slot rather than only a raised one: tying a
42
+ glyph's colour to whether its field has a shadow is a coupling nobody would
43
+ guess. It reads `--dk-border-field-focus` rather than the old
44
+ `--dk-icon-primary-default`, which a status tone remaps; the field palette
45
+ escapes the tone, and the glyph now escapes it with the ring it matches.
46
+ - **Clicking the box focuses the control.** The field paints `cursor: text`
47
+ across its whole width, which is a promise it did not keep: a click on the
48
+ padding or on the leading glyph landed nowhere, and from a focused field it
49
+ blurred. Search bar had this for free, its wrapper being a `<label>`. One
50
+ delegated listener, no `init()` call needed, every field.
51
+ - **The clear button is not search-only, and never was.** Nothing in the CSS,
52
+ the behaviour or the React component ever checked the type; the docs implied
53
+ otherwise by deriving the button from `type="search"`, so a reader could not
54
+ see one on a text field and the TSX would not emit `clearable` for one. It is
55
+ its own control in the playground now, with search merely suggesting it. A
56
+ long name, a filter or a reference number are all fields somebody may want to
57
+ empty in one go.
58
+ - **React renders the in-field actions in the documented order**: clear, reveal,
59
+ then help. Help used to render between the other two, which said the opposite
60
+ of its own comment and put React one element out of step with the same field
61
+ written by hand.
62
+ - **React: `raised`, and `label` is optional when a name arrives another way.**
63
+ `InputProps` is a union now, so a field with no label and no `aria-label` or
64
+ `aria-labelledby` does not compile. An unnamed field announces as "edit text"
65
+ and is the most common field defect there is.
66
+ - Tokens: `--dk-component-search-bar-*` (9) are gone, and
67
+ `--dk-component-input-raised-{shadow,shadow-hover}` (2) replace the two that
68
+ were genuinely the preset's own.
69
+ `migrations/14.0.0-search-bar-into-input.json` maps every class and every
70
+ token, including the six that were already aliases of an Input token.
71
+ - **New: `drift-kit.min.css`,** the same stylesheet without the prose. The
72
+ readable build is 1,277 KB and 375 KB of that is comments, because this kit
73
+ writes down why every number is the number it is. That is the right trade in
74
+ source and the wrong one over the wire:
75
+
76
+ ```text
77
+ readable 181 KB gzip 126 KB brotli
78
+ minified 61 KB gzip 34 KB brotli
79
+ ```
80
+
81
+ Two thirds off what a consumer downloads. Exported as
82
+ `@karmo-co/drift-kit/drift-kit.min.css`; the readable build stays the one to
83
+ debug against, and stays what Forge's docs read, because the tokens page finds
84
+ its component segments by the `drift-kit:layer` banners and its upstream
85
+ halves by the `END UPSTREAM` fences, and both are comments.
86
+ - **The minifier removes comments and whitespace and nothing else.** Nothing is
87
+ renamed, reordered, merged, shortened or rewritten: colours stay as written,
88
+ shorthands stay as written, duplicate rules stay duplicated. The value of this
89
+ file is that its declarations are the ones in the source in the order the
90
+ cascade sees them.
91
+ - Whitespace only disappears beside `{ } ;` and `,`, which is narrower than the
92
+ usual rule and deliberately so. Deleting a space next to `:` turns
93
+ `.a :hover` into `.a:hover`, a different selector; deleting one next to `+`
94
+ turns `calc(1px + 2px)` into something invalid. The safe rule costs 12 KB raw
95
+ and, after gzip, nothing.
96
+ - `test/minified.test.js` compares the two files rule by rule and declaration by
97
+ declaration, in order, because order is the cascade. A minifier is a program
98
+ that rewrites every line of a design system's output and the only honest way
99
+ to ship one is to prove the rewrite changed nothing a browser can see.
100
+
101
+ ### Rename map: `migrations/14.0.0-search-bar-into-input.json`
102
+
103
+ ```json
104
+ {
105
+ "version": "14.0.0",
106
+ "summary": "Search bar is no longer a component. It is data-raised on Input, which is what it had become in everything but name: its heights, border, radius, placeholder colour, slot gap, glyph size, font weight and both halves of its transition already resolved through --dk-component-input-*.",
107
+ "markup": {
108
+ "note": "One element becomes two, because Input separates the grid root from the bordered box. The wrapper was a <label> with no text, so the aria-label on the control is what named it and still is. data-size=\"large\" is not optional: the bar's unmarked rung was large and Input's unmarked rung is medium.",
109
+ "before": "<label class=\"dk-component-search-bar\">\n <svg class=\"dk-component-search-bar__icon\">…</svg>\n <input type=\"search\" class=\"dk-component-search-bar__input\" aria-label=\"Search vehicles\">\n</label>",
110
+ "after": "<div class=\"dk-component-input\" data-raised data-size=\"large\">\n <div class=\"dk-component-input__field\">\n <span class=\"dk-component-input__leading\" aria-hidden=\"true\"><svg>…</svg></span>\n <input type=\"search\" aria-label=\"Search vehicles\">\n </div>\n</div>"
111
+ },
112
+ "classes": {
113
+ "dk-component-search-bar": "dk-component-input with data-raised, plus a dk-component-input__field inside it",
114
+ "dk-component-search-bar__icon": "a dk-component-input__leading span wrapping the glyph, which is the slot every Input already had",
115
+ "dk-component-search-bar__input": "no class at all: the field styles its own control"
116
+ },
117
+ "unchanged": {
118
+ "clearButton": "dk-component-input__action with data-input-clear was already Input's own element and is untouched, as is the dk.component.input.init() call that wires it.",
119
+ "size": "data-size=\"small\" and \"medium\" mean what they meant. The unmarked rung is the only one that moves, from large to medium."
120
+ },
121
+ "rename": {
122
+ "--dk-component-search-bar-shadow": "--dk-component-input-raised-shadow",
123
+ "--dk-component-search-bar-shadow-hover": "--dk-component-input-raised-shadow-hover"
124
+ },
125
+ "removedFromTheSystem": {
126
+ "note": "Six of the nine tokens were already aliases of an Input token, so there is nothing to point them at that Input does not say more directly. The seventh, the padding, is a deliberate change: data-raised is the shadow alone, and a raised field takes its inline padding from its size rung like every other field.",
127
+ "tokens": {
128
+ "--dk-component-search-bar-height": "--dk-component-input-large-height",
129
+ "--dk-component-search-bar-small-height": "--dk-component-input-small-height",
130
+ "--dk-component-search-bar-medium-height": "--dk-component-input-height",
131
+ "--dk-component-search-bar-font-weight": "--dk-component-input-font-weight",
132
+ "--dk-component-search-bar-transition-duration": "--dk-motion-duration-default",
133
+ "--dk-component-search-bar-transition-easing": "--dk-motion-easing-in-out",
134
+ "--dk-component-search-bar-padding-inline": "--dk-component-input-padding-inline, which is 10px rather than the bar's 16px. A raised field is the flush field with a shadow under it; nothing else about the box moves."
135
+ }
136
+ },
137
+ "react": {
138
+ "note": "There was never a SearchBar export. Input gained a raised prop, and label became optional in the type when an aria-label or aria-labelledby is present: InputProps is a union, so a field with no name at all does not compile.",
139
+ "example": "<Input type=\"search\" raised size=\"large\" aria-label=\"Search vehicles\" leading={<Search size={20} />} clearable />"
140
+ },
141
+ "alsoFixed": {
142
+ "focusRing": "The bar's hover shadow overwrote its focus ring, so a focused search bar under the pointer had no focus state at all. The raised rules write the lift and the ring in one box-shadow declaration.",
143
+ "clickTarget": "Clicking the box now focuses the control on every Input, not only a raised one. The field paints cursor: text across its whole width and did not honour it; the old wrapper being a <label> is the only reason the bar did.",
144
+ "leadingGlyph": "The magnifier still goes primary on focus and one rung on hover, and every leading slot does now. It reads --dk-border-field-focus rather than --dk-icon-primary-default, which a status tone remaps; the field palette escapes the tone and the glyph escapes it with the ring it matches."
145
+ }
146
+ }
147
+ ```
148
+
@@ -0,0 +1,140 @@
1
+ ## 15.0.0 - 2026-09-22
2
+
3
+ - **Switch is a real component.** It had one token, a hardcoded 34x20 track, a
4
+ white thumb read straight from the primitives, no label, no description, no
5
+ sizes, no group, no React component and no documentation beyond two examples.
6
+ It now carries Checkbox's grammar, because a switch row and a checkbox row in
7
+ one settings list are the same line of text:
8
+
9
+ ```text
10
+ <label class="dk-component-switch">
11
+ <input type="checkbox" role="switch" class="dk-component-switch__input">
12
+ <span class="dk-component-switch__track"></span>
13
+ <span class="dk-component-switch__text">
14
+ <span class="dk-component-switch__label">Weekly summary</span>
15
+ <span class="dk-component-switch__desc" id="x" aria-hidden="true">A digest.</span>
16
+ </span>
17
+ </label>
18
+ ```
19
+
20
+ 63 tokens where there was 1, three sizes, hover, press, focus and two disabled
21
+ looks, a group with a legend, a settings-row treatment, and a `Switch` and
22
+ `SwitchGroup` in the React package.
23
+ - **BREAKING: `dk-component-switch-field` is gone**, and only half of it came
24
+ across. The half that did is the ARRANGEMENT, the control at the far end of
25
+ the row, which is `data-align="end"` and belongs to this component because it
26
+ orders the component's own children. The half that did not is the recessed
27
+ BOX: a padded, bordered container is something a page draws around whatever it
28
+ likes, not something a control owns. Its fill and edge also read
29
+ `--dk-surface-neutral-subtle` and `--dk-border-raised-subtle`, neither of
30
+ which steps with the surface level, so the row was the same colour on the
31
+ page, on a card and inside a nested panel. A page drawing its own box with
32
+ `--dk-surface-nested-default` and `--dk-border-nested-default` gets that for
33
+ free.
34
+
35
+ ```text
36
+ <label class="dk-component-switch-field"> → <div style="padding: 12px 16px; background: var(--dk-surface-nested-default); …">
37
+ <span class="dk-component-switch">…</span> → <label class="dk-component-switch" data-align="end">
38
+ <span class="…-field__icon">…</span> → <input type="checkbox" role="switch" class="dk-component-switch__input">
39
+ <span class="…-field__label">…</span> → <span class="dk-component-switch__track"></span>
40
+ </label> → <span class="dk-component-switch__icon">…</span>
41
+ → <span class="dk-component-switch__text">…</span>
42
+ → </label>
43
+ → </div>
44
+ ```
45
+
46
+ - **`role="switch"`, so it announces "switch, on" rather than "checkbox,
47
+ checked".** The element stays a native checkbox, which keeps the keyboard
48
+ handling, the form participation and the focus behaviour that a
49
+ `<div role="switch">` would have to reimplement.
50
+ - **A filled neutral track with a white thumb when off**, and the choice
51
+ palette's fill when on. This is where a switch parts company with Checkbox and
52
+ Radio, whose unengaged state is an OUTLINED box flush with the page: a switch
53
+ cannot be, because the thumb sits on the track and is the only thing saying
54
+ which way the control points. `--dk-surface-choice-track` and
55
+ `--dk-icon-choice-track` are new. Light ships neutral-400 at **2.88:1**, which
56
+ is 0.12 under the 3:1 a graphical object carries, and that is a decision with
57
+ a mitigation rather than an oversight: neutral-500 clears the floor at 3.75
58
+ and reads too heavy for a control at rest, so the thumb's SHADOW draws its
59
+ edge instead, which is how a physical switch is legible too. Dark keeps
60
+ neutral-600 and clears both axes, because "lighter" means quieter in light and
61
+ louder in dark. Shipping: thumb against track 2.88:1 off and 4.25:1 on in
62
+ light, 5.00:1 and 2.82:1 in dark; track against page 2.88:1 and 3.21:1. The
63
+ old white-on-inset pair was **1.16:1** with nothing drawing the edge at all.
64
+ - **The thumb is white in every state, theme and tone.** It read
65
+ `--dk-icon-choice-selected` when on, which is Checkbox's TICK colour and goes
66
+ primary-900 in dark, so the disc turned near-black wherever the dark palette
67
+ was in play: in dark mode, and equally on an inverse panel inside a light
68
+ page. A 2px stroke and a 14px disc with a shadow under it are not the same
69
+ object, so the switch stops sharing that token and reads
70
+ `--dk-icon-choice-track` in both states. One disc that slides is one colour.
71
+ - **One disabled look for both states.** A disabled checkbox needs two, because
72
+ its tick is the only thing carrying the state; a switch has the thumb's
73
+ position, which survives being greyed out. Both states read the choice
74
+ palette's disabled pair at 2.59:1, where the enabled colours would have left a
75
+ disabled off switch showing a white thumb on a near-white track, 1.33:1.
76
+ - **The track height IS Checkbox's box size**, 16 / 20 / 24, so the two line up
77
+ in one settings list. The width is the height times 1.75, one ratio rather
78
+ than three numbers chosen by hand, and the thumb and its travel are derived
79
+ from the height, so nothing is restated per rung.
80
+ - **`data-align="end"` is presentation only.** The DOM order never changes; the
81
+ attribute reorders with `order`, so the reading order, the tab order and the
82
+ copyable markup are the same whichever way the row points. It is the only
83
+ layout attribute the component has, deliberately.
84
+ - **The thumb shrinks under the finger**, 0.85, which is what Checkbox's glyph
85
+ does and for the same measured reason.
86
+ - **FIXED, AND NOT ONLY IN SWITCH: a description was being read as part of the
87
+ control's name.** The root of all three choice components is a `<label>`, so
88
+ everything inside it joins the accessible NAME: a described checkbox announced
89
+ as "Email me product updates About once a month, and never your data." with no
90
+ description at all. `aria-describedby` alone makes it worse, the same words
91
+ then being announced twice. The `__desc` span now carries `aria-hidden="true"`
92
+ as well as the id the input points at, which hides it from the name traversal
93
+ while keeping it readable as the description, and keeps it on screen. Applied
94
+ to Switch, Checkbox and Radio, in the React components and in every documented
95
+ example. Forge's `check:contract` asserts the name and the description of a
96
+ described control, which is what caught it.
97
+ - `migrations/15.0.0-switch-field-into-switch.json` maps every class.
98
+
99
+ ### Rename map: `migrations/15.0.0-switch-field-into-switch.json`
100
+
101
+ ```json
102
+ {
103
+ "version": "15.0.0",
104
+ "summary": "Switch becomes a real component with Checkbox's grammar, and dk-component-switch-field is retired: its arrangement is data-align=\"end\" on the switch, and its recessed box is markup the page draws around it.",
105
+ "markup": {
106
+ "note": "The control keeps its shape: a <label> root, a visually hidden native checkbox, and a track beside it. What is new is role=\"switch\" on the input, the optional __icon and __text spans, and the attributes. Markup written before 15.0.0 still renders, because __input and __track are still adjacent siblings and a switch with no text is a supported shape; add role=\"switch\" to get the right announcement.",
107
+ "before": "<label class=\"dk-component-switch-field\">\n <span class=\"dk-component-switch\">\n <input type=\"checkbox\" class=\"dk-component-switch__input\">\n <span class=\"dk-component-switch__track\"></span>\n </span>\n <span class=\"dk-component-switch-field__icon\">…</span>\n <span class=\"dk-component-switch-field__label\">Use the Drift design system</span>\n</label>",
108
+ "after": "<div style=\"padding: 12px 16px; background: var(--dk-surface-nested-default); border: 1px solid var(--dk-border-nested-default); border-radius: var(--dk-border-radius-200);\">\n <label class=\"dk-component-switch\" data-align=\"end\">\n <input type=\"checkbox\" role=\"switch\" class=\"dk-component-switch__input\">\n <span class=\"dk-component-switch__track\"></span>\n <span class=\"dk-component-switch__icon\" aria-hidden=\"true\">…</span>\n <span class=\"dk-component-switch__text\">\n <span class=\"dk-component-switch__label\">Use the Drift design system</span>\n </span>\n </label>\n</div>"
109
+ },
110
+ "classes": {
111
+ "dk-component-switch-field": "dk-component-switch with data-align=\"end\" for the arrangement, plus a container of your own for the box. There is deliberately no data-row: a padded, bordered box is a layout decision a page makes, and the one that class made did not step with the surface level.",
112
+ "dk-component-switch-field__icon": "dk-component-switch__icon",
113
+ "dk-component-switch-field__label": "dk-component-switch__label, inside a dk-component-switch__text wrapper",
114
+ "the nested span.dk-component-switch": "gone: the input and the track are children of the root now, one element rather than two"
115
+ },
116
+ "added": {
117
+ "role=\"switch\"": "On the input. Without it the control announces as a checkbox. Required.",
118
+ "data-size": "small | medium | large. The track height is Checkbox's box size: 16 / 20 / 24 against 14 / 16 / 18 label text. The old track was 34x20 at every size, which is the new medium at 35x20.",
119
+ "data-align": "end. Presentation only, applied with `order`, so the markup does not change. The only layout attribute the component has.",
120
+ "dk-component-switch__text / __label / __desc": "The words. The description needs id + aria-hidden on the span and aria-describedby on the input, or it is read as part of the control's name.",
121
+ "dk-component-switch__icon": "A leading glyph for a settings row.",
122
+ "dk-component-switch-group / __legend / __desc": "Several settings under one heading, as a fieldset.",
123
+ "--dk-surface-choice-track / --dk-icon-choice-track": "New in the mapped choice palette: the unselected track and its thumb. Only a switch has an unselected FILL, since Checkbox and Radio paint their unengaged state as an outlined box from the field palette."
124
+ },
125
+ "rename": {
126
+ "--dk-component-switch-label-font-weight": "--dk-component-switch-label-font-weight (unchanged, and the only token this component had)"
127
+ },
128
+ "react": {
129
+ "note": "There was no Switch export before. There is now, plus SwitchGroup.",
130
+ "example": "<Switch label=\"Weekly summary\" description=\"A digest of everything that changed.\" defaultChecked />"
131
+ },
132
+ "alsoFixed": {
133
+ "descriptionInTheName": "Switch, Checkbox and Radio all render their description inside the <label> root, so it was being read as part of the control's accessible NAME and no description was exposed at all. The __desc span now carries aria-hidden=\"true\" alongside the id its input references, which takes it out of the name traversal while leaving it readable as the description and visible on screen. Fixed in the React components and in every documented example.",
134
+ "rowSurface": "The recessed row read --dk-surface-neutral-subtle and --dk-border-raised-subtle, neither of which steps with the surface level, so it was the same colour on the page, on a card and inside a nested panel. A page drawing its own box with --dk-surface-nested-default and --dk-border-nested-default gets that right by construction.",
135
+ "thumbContrast": "A white thumb on --dk-surface-inset-default measured 1.16:1 with nothing drawing its edge. The off track is a filled neutral now, --dk-surface-choice-track, with a white thumb from --dk-icon-choice-track and a 1px shadow under it: 2.88:1 in light and 5.00:1 in dark. The light pair sits 0.12 under the 3:1 graphical floor by decision, the rung that cleared it reading too heavy for a control at rest, and the shadow is what draws the disc's edge instead.",
136
+ "thumbColour": "The thumb is white in every state, theme and tone. It borrowed --dk-icon-choice-selected when on, which is Checkbox's tick colour and primary-900 in dark, so the disc went near-black in dark mode and on any inverse panel inside light. It reads --dk-icon-choice-track in both states now: one disc that slides is one colour."
137
+ }
138
+ }
139
+ ```
140
+