@karmo-co/drift-kit-reference 0.0.0-stage → 23.4.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.
- package/LICENSE +7 -0
- package/README.md +24 -2
- package/bin/drift-kit-ref.js +169 -0
- package/manifest/components/accordion.md +47 -0
- package/manifest/components/action-bar.md +51 -0
- package/manifest/components/alert.md +53 -0
- package/manifest/components/app-shell.md +43 -0
- package/manifest/components/avatar.md +42 -0
- package/manifest/components/body.md +48 -0
- package/manifest/components/box.md +58 -0
- package/manifest/components/breadcrumb.md +59 -0
- package/manifest/components/button.md +46 -0
- package/manifest/components/calendar.md +69 -0
- package/manifest/components/card.md +71 -0
- package/manifest/components/checkbox.md +43 -0
- package/manifest/components/choice-card.md +61 -0
- package/manifest/components/code-input.md +56 -0
- package/manifest/components/date-picker.md +66 -0
- package/manifest/components/day-of-month.md +54 -0
- package/manifest/components/detail-list.md +37 -0
- package/manifest/components/divider.md +33 -0
- package/manifest/components/dropdown.md +52 -0
- package/manifest/components/dropzone.md +56 -0
- package/manifest/components/eyebrow.md +45 -0
- package/manifest/components/feedback-badge.md +35 -0
- package/manifest/components/glider.md +34 -0
- package/manifest/components/heading.md +47 -0
- package/manifest/components/hero.md +46 -0
- package/manifest/components/icon-button.md +41 -0
- package/manifest/components/icon-tile.md +44 -0
- package/manifest/components/icon.md +30 -0
- package/manifest/components/input.md +63 -0
- package/manifest/components/link.md +42 -0
- package/manifest/components/loading-spinner.md +42 -0
- package/manifest/components/logo.md +38 -0
- package/manifest/components/modal.md +53 -0
- package/manifest/components/pagination.md +54 -0
- package/manifest/components/payment-form.md +29 -0
- package/manifest/components/price-summary.md +30 -0
- package/manifest/components/radio.md +42 -0
- package/manifest/components/section-heading.md +41 -0
- package/manifest/components/select.md +99 -0
- package/manifest/components/shield.md +34 -0
- package/manifest/components/sidebar.md +73 -0
- package/manifest/components/stepper.md +53 -0
- package/manifest/components/switch.md +45 -0
- package/manifest/components/table.md +77 -0
- package/manifest/components/tabs.md +43 -0
- package/manifest/components/tag.md +43 -0
- package/manifest/components/textarea.md +52 -0
- package/manifest/components/toast.md +52 -0
- package/manifest/components/tooltip.md +42 -0
- package/manifest/components/top-bar.md +60 -0
- package/manifest/components/vehicle.md +43 -0
- package/manifest/index.json +402 -0
- package/manifest/migrations/1.0.0.md +50 -0
- package/manifest/migrations/10.0.0.md +154 -0
- package/manifest/migrations/11.0.0.md +24 -0
- package/manifest/migrations/12.0.0.md +38 -0
- package/manifest/migrations/13.0.0.md +50 -0
- package/manifest/migrations/14.0.0.md +148 -0
- package/manifest/migrations/15.0.0.md +140 -0
- package/manifest/migrations/16.0.0.md +196 -0
- package/manifest/migrations/17.0.0.md +19 -0
- package/manifest/migrations/18.0.0.md +23 -0
- package/manifest/migrations/19.0.0.md +15 -0
- package/manifest/migrations/2.0.0.md +18 -0
- package/manifest/migrations/20.0.0.md +43 -0
- package/manifest/migrations/21.0.0.md +97 -0
- package/manifest/migrations/22.0.0.md +17 -0
- package/manifest/migrations/23.0.0.md +86 -0
- package/manifest/migrations/3.0.0.md +34 -0
- package/manifest/migrations/4.0.0.md +147 -0
- package/manifest/migrations/5.0.0.md +73 -0
- package/manifest/migrations/6.0.0.md +67 -0
- package/manifest/migrations/7.0.0.md +71 -0
- package/manifest/migrations/8.0.0.md +16 -0
- package/manifest/migrations/9.0.0.md +107 -0
- package/manifest/rules.md +37 -0
- package/manifest/setup.md +28 -0
- package/manifest/tokens/border-width.md +12 -0
- package/manifest/tokens/border.md +147 -0
- package/manifest/tokens/font.md +90 -0
- package/manifest/tokens/icon.md +84 -0
- package/manifest/tokens/interactive.md +37 -0
- package/manifest/tokens/motion.md +69 -0
- package/manifest/tokens/radius.md +20 -0
- package/manifest/tokens/shadow.md +28 -0
- package/manifest/tokens/spacing.md +35 -0
- package/manifest/tokens/surface.md +138 -0
- package/manifest/tokens/text.md +92 -0
- package/package.json +20 -4
|
@@ -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
|
+
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
## 16.0.0 - 2026-09-22
|
|
2
|
+
|
|
3
|
+
- **Select's keyboard exists.** Until now the arrow keys opened the menu and
|
|
4
|
+
then nothing worked: ArrowUp, ArrowDown, Home and typing all left focus on the
|
|
5
|
+
trigger with no way to reach an option, and Tab walked out of the page while
|
|
6
|
+
the menu stayed open. There was no way to choose a value without a mouse. It
|
|
7
|
+
is the APG's select-only combobox now, with focus staying on the trigger and
|
|
8
|
+
`aria-activedescendant` naming the highlighted option:
|
|
9
|
+
|
|
10
|
+
```text
|
|
11
|
+
ArrowDown / ArrowUp open, or move the active option
|
|
12
|
+
Alt+ArrowDown open without moving
|
|
13
|
+
Home / End first / last
|
|
14
|
+
PageDown / PageUp ten at a time
|
|
15
|
+
Enter / Space choose
|
|
16
|
+
Escape close, keep the value
|
|
17
|
+
Tab close and let the page have it
|
|
18
|
+
any printable key typeahead, 700ms buffer, cycles, skips disabled
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
Opening lands on the CURRENT SELECTION, which is where a reader expects to be
|
|
22
|
+
and what a native select does. Hovering moves the active option rather than
|
|
23
|
+
painting a second highlight, so the pointer and the keyboard never light two
|
|
24
|
+
different rows.
|
|
25
|
+
- **Forge's `check:contract` asserts the keyboard**, for any component that
|
|
26
|
+
declares one: open with a key, move with a key, choose with a key, and leave
|
|
27
|
+
the value alone on Escape. The old behaviour passed every gate in the repo
|
|
28
|
+
while being unusable, which is the whole argument for adding it. Verified by
|
|
29
|
+
breaking the arrow keys on purpose and watching it fail.
|
|
30
|
+
- **Its own token collection: 74 where there were 12.** Eight of the twelve
|
|
31
|
+
pointed straight at `--dk-component-input-*` and the stylesheet read Input's
|
|
32
|
+
collection another eleven times, which is component-to-component, a tier the
|
|
33
|
+
kit does not have, and the same fault Input's label was moved off
|
|
34
|
+
`--dk-component-eyebrow-*` for. The rungs are derived from the same primitives
|
|
35
|
+
by the same arithmetic rather than quoted, so both move together, and
|
|
36
|
+
`check:contract`'s field cohort is what asserts a select and a text field
|
|
37
|
+
still paint the same box.
|
|
38
|
+
- **BREAKING: `.dk-component-select--disabled` is gone.** The last BEM-style
|
|
39
|
+
modifier in a kit that uses attributes everywhere else, on a component whose
|
|
40
|
+
trigger is a real `<button>`. It was worse than untidy: it painted a trigger
|
|
41
|
+
without disabling it, so a select that looked locked still opened, still took
|
|
42
|
+
focus and still changed its value. Use the native `disabled` attribute on
|
|
43
|
+
`.dk-component-select__trigger`.
|
|
44
|
+
- **The field grammar it never had**: a label, a caption, an error with
|
|
45
|
+
`aria-invalid`, and the required asterisk, so a select and a text field in one
|
|
46
|
+
form can say the same things.
|
|
47
|
+
- **The menu can carry more**: option groups under real headings (a labelled
|
|
48
|
+
`role="group"`, which the keyboard walks past), a second line on an option, a
|
|
49
|
+
disabled option that is actually skipped, and an empty state for a menu with
|
|
50
|
+
nothing in it.
|
|
51
|
+
- **The floating surface is named for the shape, not the component.**
|
|
52
|
+
`--dk-component-menu-*` replaces `--dk-component-select-menu-radius` and
|
|
53
|
+
`-shadow` and picks up the literals beside them. Forge hand-rolls this box
|
|
54
|
+
five times over (seven `role="menu"` popups across the prototypes, the card
|
|
55
|
+
actions menu, the icon picker), and a Dropdown built later is menu semantics
|
|
56
|
+
on a surface that already exists rather than a second stylesheet that drifts.
|
|
57
|
+
- **On a phone the menu is a bottom sheet, by default and not by request.**
|
|
58
|
+
Below the kit's mobile breakpoint it stops being a dropdown: full width, off
|
|
59
|
+
the bottom edge where the thumb already is, with a scrim, a heading taken from
|
|
60
|
+
the label, 44px rows, and the page behind it locked from scrolling. A dropdown
|
|
61
|
+
anchored to a 100px trigger on a 375px screen is the worst of both, flipping
|
|
62
|
+
when there is no room below and clipping against the viewport.
|
|
63
|
+
|
|
64
|
+
It is not an option because a design system's defaults are what ship: an
|
|
65
|
+
opt-in would mean most call sites never opt in and mobile users keep the
|
|
66
|
+
cramped menu. `data-sheet="never"` is the escape hatch for the one case that
|
|
67
|
+
is genuinely wrong, a select inside something that is already a sheet or a
|
|
68
|
+
full-screen modal, and `data-sheet="always"` forces it at any width, which is
|
|
69
|
+
how the docs demonstrate it on a desktop.
|
|
70
|
+
|
|
71
|
+
THE BREAKPOINT IS DECLARED ONCE, in the stylesheet, and the behaviour reads it
|
|
72
|
+
back as a custom property when the menu opens. A media query cannot switch
|
|
73
|
+
JavaScript on, and a width hardcoded in both languages drifts the first time
|
|
74
|
+
one of them moves.
|
|
75
|
+
- **The sheet leaves the way it arrived.** It slid up and then vanished on the
|
|
76
|
+
frame the selection landed, which reads as the screen glitching rather than as
|
|
77
|
+
a panel closing: the thing you just touched is gone before you have finished
|
|
78
|
+
touching it. It slides out now, faster than it came in, and the scrim fades
|
|
79
|
+
with it, because the scrim is the sheet's own shadow and fading the element
|
|
80
|
+
fades the dim in one gesture. `aria-expanded` flips to false immediately: the
|
|
81
|
+
state a screen reader reads should not wait on an animation.
|
|
82
|
+
- **A sheet on its way out answers "closed" to everything that asks.** Tapping
|
|
83
|
+
the trigger mid-exit was swallowed, because the click handler asked whether
|
|
84
|
+
the menu was open, got true from an attribute that is still set while the
|
|
85
|
+
sheet slides, and called close, which returned since it was already leaving.
|
|
86
|
+
Tapping something and having nothing happen is the worst answer available.
|
|
87
|
+
Now the exit is cancelled and the sheet comes back up rather than a second one
|
|
88
|
+
animating in behind it.
|
|
89
|
+
- **The scrim is a box-shadow, not an element**: a 100vmax spread paints the
|
|
90
|
+
whole viewport in one declaration, so there is no node to create, none to
|
|
91
|
+
leak, and nothing to clean up if a page tears the menu out from under us. It
|
|
92
|
+
is not a hit target and does not need to be, since the outside-click handler
|
|
93
|
+
that closes the menu was already listening on the document.
|
|
94
|
+
- **New: `--dk-surface-backdrop-soft`**, 40% black, beside upstream's 80%.
|
|
95
|
+
A modal is the page for as long as it is open and a backdrop that heavy says
|
|
96
|
+
so; a select's sheet is a menu that happens to be full width, and dimming the
|
|
97
|
+
form behind it to black takes away the context you are choosing for. Declared
|
|
98
|
+
below the upstream fence, in both themes.
|
|
99
|
+
- **The highlight belongs to whichever input moved last.** Hovering an option
|
|
100
|
+
and then moving the pointer out of the menu left the row lit: one rule painted
|
|
101
|
+
both the hovered option and the ACTIVE one, and the active one is the
|
|
102
|
+
keyboard's cursor, which must exist the whole time the menu is open because
|
|
103
|
+
`aria-activedescendant` has to point at something. So the pointer's position
|
|
104
|
+
became permanent. The menu records `data-nav` now: on `pointer` the `:hover`
|
|
105
|
+
paints and stops the moment the pointer leaves, with the active option still
|
|
106
|
+
set silently for the screen reader and the next keypress; on `keyboard` the
|
|
107
|
+
active option paints and `:hover` is suppressed, or arrowing past a row the
|
|
108
|
+
pointer happens to be resting on would light two at once. A menu opened by
|
|
109
|
+
click starts in pointer mode and paints nothing until you are over a row.
|
|
110
|
+
- **The options are inset and rounded**, rather than running edge to edge. The
|
|
111
|
+
menu carries the inset, `--dk-component-menu-padding-inline`, and the option
|
|
112
|
+
carries the rest, and the two are chosen so their SUM is unchanged: nothing
|
|
113
|
+
moves but the highlight, which now stops short of the menu's edge and rounds.
|
|
114
|
+
The radius is concentric rather than chosen by eye, `--dk-border-radius-100`
|
|
115
|
+
against the menu's `-300` across a `spacing-2` inset, so the two curves sit
|
|
116
|
+
parallel; a rounder option looks deliberate on its own and wrong inside the
|
|
117
|
+
menu's own corner. The group headings keep their text on the option text's
|
|
118
|
+
line.
|
|
119
|
+
- **`data-raised` on the trigger**, the same attribute Input carries and the
|
|
120
|
+
same trade: the shadow is the whole of it, since padding belongs to the size
|
|
121
|
+
rung. It stacks with the focus ring rather than replacing it, in both of the
|
|
122
|
+
states that draw one, focused and open, because a select has two where a field
|
|
123
|
+
has one. Readable on the root or on the trigger, so a lone trigger can be
|
|
124
|
+
raised too, and the forced states carry the lift so the docs' state control
|
|
125
|
+
does not show a raised select losing its shadow on hover. React takes
|
|
126
|
+
`raised`.
|
|
127
|
+
- **The type rides with the box: 14 / 16 / 18**, on the same rungs a field
|
|
128
|
+
reads. The trigger was a rung low at small and medium, so a select and a text
|
|
129
|
+
field in one form were the same height and not the same type; the heights
|
|
130
|
+
matching all along is what let it go unseen, since the box arithmetic reads a
|
|
131
|
+
line-height rung and the value reads a text rung, and only one of the two was
|
|
132
|
+
Input's. And an option is the text you are about to get, so it is set at the
|
|
133
|
+
rung the trigger will show it at: options were fixed at 16/24 at every size, a
|
|
134
|
+
rung larger than the trigger's value with four more pixels of leading, so a
|
|
135
|
+
14px control opened a 16px list. The menu is portalled, so `init()` mirrors
|
|
136
|
+
`data-size` onto it the way `dk.scope.adopt` mirrors the token legs.
|
|
137
|
+
- **The menu's width is its own, not the trigger's.** It took the trigger's
|
|
138
|
+
width outright, which reads fine on a 320px field and badly everywhere else:
|
|
139
|
+
Forge's own control rail opens 100px selects, and a 100px menu truncates every
|
|
140
|
+
option in it. A trigger only ever holds one line, so it is a poor guide to how
|
|
141
|
+
much room a list needs. The rule is now a floor, a fit and a cap: never
|
|
142
|
+
narrower than its trigger, as wide as its widest option needs,
|
|
143
|
+
`--dk-component-menu-max-width` (328px) and the viewport behind it.
|
|
144
|
+
- **`dk.position` gains `align`**, and finding that it needed one is the second
|
|
145
|
+
half of the same story. Alignment was inferred from `matchWidth`, which worked
|
|
146
|
+
only because a menu exactly as wide as its trigger centres and start-aligns to
|
|
147
|
+
the same number. The moment the menu could be wider, a 152px menu on a 117px
|
|
148
|
+
trigger sat 17px to the left of it. `align: 'start' | 'center' | 'end'` is
|
|
149
|
+
explicit now and `matchWidth` means only what its name says; Tooltip keeps the
|
|
150
|
+
centred default.
|
|
151
|
+
- **React `Select`**, which did not exist: the docs' TSX pointed at
|
|
152
|
+
`@karmo-co/component-library`, a different product. Uncontrolled by design,
|
|
153
|
+
because the menu is built and mutated by `behaviour.js` in DOM React does not
|
|
154
|
+
own; `defaultValue` seeds it and `onChange` reports it.
|
|
155
|
+
|
|
156
|
+
### Rename map: `migrations/16.0.0-select-keyboard.json`
|
|
157
|
+
|
|
158
|
+
```json
|
|
159
|
+
{
|
|
160
|
+
"version": "16.0.0",
|
|
161
|
+
"summary": "Select gains the keyboard it never had, its own token collection, and the field grammar (label, caption, error, required). The only breaking change is the removal of .dk-component-select--disabled.",
|
|
162
|
+
"breaking": {
|
|
163
|
+
"dk-component-select--disabled": {
|
|
164
|
+
"replacement": "the native disabled attribute on .dk-component-select__trigger",
|
|
165
|
+
"why": "It painted a trigger without disabling it, so a select that looked locked still opened, still took focus and still changed its value. It was also the last BEM-style modifier in a kit that uses attributes everywhere else.",
|
|
166
|
+
"before": "<div class=\"dk-component-select dk-component-select--disabled\">…<button class=\"dk-component-select__trigger\">…",
|
|
167
|
+
"after": "<div class=\"dk-component-select\">…<button class=\"dk-component-select__trigger\" disabled>…",
|
|
168
|
+
"javascript": "root.classList.toggle('dk-component-select--disabled', locked) → root.querySelector('.dk-component-select__trigger').disabled = locked"
|
|
169
|
+
}
|
|
170
|
+
},
|
|
171
|
+
"rename": {
|
|
172
|
+
"--dk-component-select-menu-radius": "--dk-component-menu-radius",
|
|
173
|
+
"--dk-component-select-menu-shadow": "--dk-component-menu-shadow"
|
|
174
|
+
},
|
|
175
|
+
"unchanged": {
|
|
176
|
+
"markup": "Every class is the same and every existing select keeps working: the root, __trigger, __value, __chevron, __menu and __option all mean what they meant, and the hidden input still carries the value. role=\"combobox\", aria-controls, aria-activedescendant and the option ids are written by init(), so markup from before 16.0.0 gets the keyboard without being touched.",
|
|
177
|
+
"api": "root.__dcSelect keeps setOptions / select / value / open / close, and the bubbling dk-component-select:change CustomEvent keeps its detail { value }. setOptions gained three optional fields per option: description, group and disabled.",
|
|
178
|
+
"sizes": "data-size=\"small|large\" mean what they meant, 38 and 52 against the unmarked 44."
|
|
179
|
+
},
|
|
180
|
+
"added": {
|
|
181
|
+
"keyboard": "Arrows, Home, End, PageUp, PageDown, Enter, Space, Escape, Tab and typeahead. Focus stays on the trigger; aria-activedescendant names the active option.",
|
|
182
|
+
"dk-component-select__label / __caption / __error": "The field grammar. Pair the error with aria-invalid=\"true\" on the trigger and aria-describedby pointing at the message.",
|
|
183
|
+
"dk-component-select__group / __group-label": "Options under a heading. The heading is a labelled role=\"group\", not an option, so the keyboard walks past it.",
|
|
184
|
+
"dk-component-select__option-text / __option-desc": "A second line on an option. The trigger shows the label alone.",
|
|
185
|
+
"dk-component-select__empty": "What a menu with nothing in it says.",
|
|
186
|
+
"--dk-component-menu-*": "The floating surface, named for the shape rather than for this component so a Dropdown can share it.",
|
|
187
|
+
"React Select": "import { Select } from '@karmo-co/drift-kit/react'. Uncontrolled: defaultValue seeds it, onChange reports it.",
|
|
188
|
+
"--dk-component-menu-max-width / --dk-component-menu-viewport-gap": "The menu sizes to its content now, floored by the trigger's width and capped by these two. It used to take the trigger's width outright, so a narrow select opened a narrow menu that truncated every option in it.",
|
|
189
|
+
"dk.position({ align })": "'start' | 'center' | 'end'. Alignment used to be inferred from matchWidth, which only ever agreed with the intent because the menu was exactly as wide as its trigger. Tooltip keeps the centred default.",
|
|
190
|
+
"data-raised": "The trigger lifted onto an elevation, the same attribute Input carries. The shadow is the whole of it: padding and height still come from the size rung. It stacks with the focus ring rather than replacing it.",
|
|
191
|
+
"data-sheet": "auto | always | never. On a phone the menu is a bottom sheet by default: full width, off the bottom edge, with a scrim, a heading, 44px rows and the page locked from scrolling. 'never' opts out for a select inside something that is already a sheet; 'always' forces it at any width. Nothing to change in existing markup: the default is the new behaviour.",
|
|
192
|
+
"--dk-surface-backdrop-soft": "40% black, beside upstream's 80% --dk-surface-backdrop-black. A sheet dims the page without owning it the way a modal does."
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
```
|
|
196
|
+
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
## 17.0.0 - 2026-09-24
|
|
2
|
+
|
|
3
|
+
### Migration
|
|
4
|
+
|
|
5
|
+
Two renames, both on Modal, and both mechanical:
|
|
6
|
+
|
|
7
|
+
- **`data-size` on `.dk-component-modal__box`: `sm` / `md` / `lg` become
|
|
8
|
+
`small` / `medium` / `large`**, and `xlarge` joins them at 800px. Every other
|
|
9
|
+
component in the kit says small / medium / large; the modal was the one that
|
|
10
|
+
did not. `medium` is still the unmarked default, so a box with no `data-size`
|
|
11
|
+
is unaffected.
|
|
12
|
+
- **`.is-closing` on the overlay becomes `[data-closing]`.** It was the last
|
|
13
|
+
state-carrying class in a kit that spells every other state as an attribute.
|
|
14
|
+
Anything that ADDS the class to play the exit, or reads it to know a dialog is
|
|
15
|
+
leaving, has to move; `dk.component.modal.open()` and `.close()` already do.
|
|
16
|
+
The keyframe names are deliberately unchanged, because an animation's name is
|
|
17
|
+
an identifier consumers observe (`animationName === "dk-component-modal-slide-out"`),
|
|
18
|
+
and renaming it would break that silently.
|
|
19
|
+
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
## 18.0.0 - 2026-10-01
|
|
2
|
+
|
|
3
|
+
### Migration
|
|
4
|
+
|
|
5
|
+
- **Table's loading row uses the Loading spinner.** `.dk-component-table__spinner`
|
|
6
|
+
and its tokens (`spinner-size`, `spinner-track`, `spinner-head`,
|
|
7
|
+
`spinner-duration`) are gone; put a
|
|
8
|
+
`<div class="dk-component-loading-spinner" data-variant="ring" data-size="small" data-animation="enter">`
|
|
9
|
+
with a `__message` in the loading cell. React `loadingLabel` is now a string.
|
|
10
|
+
- **Table's `sort-icon-color-active` token now means pressed**, not sorted; the
|
|
11
|
+
sorted colour is `sort-icon-color-sorted`.
|
|
12
|
+
- **Dropzone's root is a container now.** The `<label>` that was the root
|
|
13
|
+
becomes `label.dk-component-dropzone__area` inside a
|
|
14
|
+
`div.dk-component-dropzone`, which can also hold a field label, a file list,
|
|
15
|
+
a caption and an error. Wrap the old markup in the div and rename the
|
|
16
|
+
label's class. `root.__dcDropzone.reset()` and `refresh()` keep working.
|
|
17
|
+
- **Tag's `data-pill` (React `pill`) is removed.** A tag's corner now always
|
|
18
|
+
comes from the theme's `data-border-radius` (sharp, normal, bubbly), like
|
|
19
|
+
every other component's. Delete the attribute or prop; nothing replaces it.
|
|
20
|
+
Under bubbly a tag is already nearly round, and a filter chip that must look
|
|
21
|
+
different will be its own clickable component. This is breaking, so the next
|
|
22
|
+
release is a major.
|
|
23
|
+
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
## 19.0.0 - 2026-10-06
|
|
2
|
+
|
|
3
|
+
### Migration
|
|
4
|
+
|
|
5
|
+
- **Stepper's `data-labels="beside"` is removed** (React `labels` is now
|
|
6
|
+
`'visible' | 'hidden'`). Each variant puts its labels where they read best:
|
|
7
|
+
beside the number for `data-variant="inline"`, below the indicator for the
|
|
8
|
+
rest. For the inline look, use `data-variant="inline"`; `data-labels="hidden"`
|
|
9
|
+
is unchanged.
|
|
10
|
+
- **Stepper's narrow summary line is removed** (`.dk-component-stepper__summary`
|
|
11
|
+
and `messages.summary`). On a narrow stepper the current step now keeps its
|
|
12
|
+
own words in place instead; see Changes.
|
|
13
|
+
- **Stepper's `--dk-component-stepper-connector-gap` token is removed**:
|
|
14
|
+
connectors now touch the indicators in both orientations.
|
|
15
|
+
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
## 2.0.0 — 2026-09-09
|
|
2
|
+
|
|
3
|
+
- The `dk` vocabulary: every token is `--dk-…`, tier words `primitive` /
|
|
4
|
+
`responsive` / `dynamic` / `component`, classes `.dk-component-{name}`, JS
|
|
5
|
+
namespace `dk.component.{name}.{fn}`. Component-library / Figma name parity
|
|
6
|
+
dropped by decision; `refresh:tokens`, `check:upstream` and the upstream
|
|
7
|
+
parity test removed. Map in `migrations/2.0.0-dk-vocabulary.json`.
|
|
8
|
+
- Components added since 1.0.0: body, heading, hero, eyebrow, icon (with the
|
|
9
|
+
Karmo glyph runtime); surface levels and tone scopes (`data-surface`,
|
|
10
|
+
`data-tone`, `data-tone-fill`); fixed Karmo alias rungs for body, overline and
|
|
11
|
+
input type.
|
|
12
|
+
|
|
13
|
+
### Rename map: `migrations/2.0.0-dk-vocabulary.json`
|
|
14
|
+
|
|
15
|
+
Token names before → after the dk vocabulary. Classes: dc-{x} → dk-component-{x}. JS: window.dc{X} → dk.component.{x}.{fn}.
|
|
16
|
+
|
|
17
|
+
The full map is `migrations/2.0.0-dk-vocabulary.json` in the drift-kit repo.
|
|
18
|
+
|