@kenlo/sds-design-system 0.9.9 → 1.0.2

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 (45) hide show
  1. package/README.md +58 -7
  2. package/components/Avatar/Avatar.css +22 -0
  3. package/components/Button/Button.css +95 -8
  4. package/components/Card/Card.css +16 -6
  5. package/components/Card/card-variants.js +18 -0
  6. package/components/Card/card.marko +12 -2
  7. package/components/Checkbox/Checkbox.css +56 -0
  8. package/components/ColorPicker/ColorPicker.css +25 -2
  9. package/components/ColorPicker/color-picker.marko +25 -4
  10. package/components/DatePicker/DatePicker.css +22 -0
  11. package/components/Floating/focus-when-ready.js +59 -0
  12. package/components/Incrementer/Incrementer.css +22 -0
  13. package/components/Input/Input.css +25 -2
  14. package/components/Menu/Menu.css +21 -11
  15. package/components/Menu/menu-variants.js +12 -0
  16. package/components/Menu/menu.marko +11 -7
  17. package/components/Pill/Pill.css +46 -0
  18. package/components/Radio/Radio.css +75 -0
  19. package/components/Select/Select.css +21 -1
  20. package/components/Select/select-options.js +10 -1
  21. package/components/Select/select.marko +112 -5
  22. package/components/Table/Table.css +73 -2
  23. package/components/Table/table-column-picker.marko +161 -0
  24. package/components/Table/table-logic.js +56 -0
  25. package/components/Table/table.marko +50 -12
  26. package/components/TextEditor/TextEditor.css +2 -2
  27. package/components/Toggle/Toggle.css +40 -0
  28. package/components/Tooltip/Tooltip.css +2 -2
  29. package/components/Uploader/Uploader.css +23 -0
  30. package/foundations/custom-scheme.js +128 -10
  31. package/foundations/fonts/OFL.txt +114 -0
  32. package/foundations/fonts/fonts.json +152 -0
  33. package/foundations/fonts/manrope-cyrillic-ext.woff2 +0 -0
  34. package/foundations/fonts/manrope-cyrillic.woff2 +0 -0
  35. package/foundations/fonts/manrope-greek.woff2 +0 -0
  36. package/foundations/fonts/manrope-latin-ext.woff2 +0 -0
  37. package/foundations/fonts/manrope-latin.woff2 +0 -0
  38. package/foundations/fonts/manrope-vietnamese.woff2 +0 -0
  39. package/foundations/tokens.css +1095 -15
  40. package/icons/generated/fluent-chevron-down-filled.marko +3 -0
  41. package/icons/generated/fluent-chevron-up-filled.marko +3 -0
  42. package/icons/generated/fluent-column-triple-regular.marko +3 -0
  43. package/icons/manifest.json +3 -0
  44. package/marko.json +3 -0
  45. package/package.json +8 -3
@@ -215,6 +215,36 @@
215
215
  flex-direction: column;
216
216
  margin: -3px 0;
217
217
  }
218
+ /* ── sortIndicator="hover" — the pair on an unsorted header is hidden rather than removed.
219
+
220
+ Removed, the header would be narrower until the pointer arrived and then push the header
221
+ beside it sideways; hidden, it keeps its room and nothing moves. The two chevrons also
222
+ overlap into roughly one glyph's height here, so the clean header does not carry a
223
+ column-tall gap where a mark might appear.
224
+
225
+ The hover selector is on the cell and the focus one on the button, because in SDS the
226
+ header's control IS a button inside the cell — KDLS puts tabindex on the cell itself and
227
+ can write both on one selector. ── */
228
+ .sds-table__sort--compact {
229
+ color: var(--color-neutral-40);
230
+ opacity: 0;
231
+ transition: opacity 120ms ease;
232
+ }
233
+ .sds-table__sort--compact .sds-icon + .sds-icon {
234
+ margin-top: calc(-1 * var(--spacing-2));
235
+ }
236
+ .sds-table__col--sortable:hover .sds-table__sort--compact,
237
+ .sds-table__sort-button:focus-visible .sds-table__sort--compact {
238
+ opacity: 1;
239
+ }
240
+ /* ── loading — the rows that are on screen stay there, dimmed, while the next ones come.
241
+
242
+ Not a spinner in their place: a reader who asked for a different sort order would watch
243
+ the list vanish and come back, and lose where they were in it. The header keeps its full
244
+ colour, so the column titles stay readable while the body is provisional. ── */
245
+ .sds-table--loading .sds-table__row:not(.sds-table__row--header) {
246
+ opacity: 0.5;
247
+ }
218
248
  /* ── Ellipsis (single-line truncation + Tooltip on hover) ── */
219
249
  .sds-table__col--ellipsis .sds-tooltip-wrapper {
220
250
  display: flex;
@@ -254,8 +284,8 @@
254
284
  }
255
285
 
256
286
  .sds-table__sort-button:focus-visible {
257
- outline: var(--focus-ring-width) solid var(--focus-stroke-color);
258
- outline-offset: 2px;
287
+ outline: var(--focus-ring-outline);
288
+ outline-offset: var(--focus-ring-outline-offset);
259
289
  border-radius: var(--radius-xs);
260
290
  }
261
291
 
@@ -268,3 +298,44 @@
268
298
  font-family: var(--font-family-base);
269
299
  font-size: var(--font-size-body-md);
270
300
  }
301
+
302
+ /* ── Column picker ──────────────────────────────────────────────────────
303
+ sds-table-column-picker's panel. Ported from KDLS 2.9.0's Table.css.
304
+ BEM root: .sds-table__column-picker
305
+ ─────────────────────────────────────────────────────────────────── */
306
+ .sds-table__column-picker {
307
+ display: flex;
308
+ flex-direction: column;
309
+ gap: var(--spacing-4);
310
+ min-width: 240px;
311
+ }
312
+ /* The rows scroll and nothing else does: the reset link below stays put, and the cap is
313
+ the same seven and a half rows every other list in the system shows. The padding is
314
+ there so a focus ring on the first or last checkbox is not clipped by the scroller, and
315
+ the negative margin takes the space back so the rows still line up with the title. */
316
+ .sds-table__column-picker-list {
317
+ display: flex;
318
+ flex-direction: column;
319
+ gap: var(--spacing-6);
320
+ max-height: var(--menu-max-height);
321
+ overflow-y: auto;
322
+ padding: var(--spacing-1);
323
+ margin: calc(-1 * var(--spacing-1));
324
+ }
325
+ /* A <fieldset>, for the grouping a screen reader needs — and a fieldset arrives with a
326
+ browser border, a browser margin and a browser padding that no design asked for. */
327
+ .sds-table__column-picker-group {
328
+ display: flex;
329
+ flex-direction: column;
330
+ gap: var(--spacing-3);
331
+ min-width: 0;
332
+ margin: 0;
333
+ padding: 0;
334
+ border: 0;
335
+ }
336
+ /* Same reason: a <legend> is laid out as part of the border it no longer has, so without
337
+ this the group title sits indented and cannot be reached by the flex gap above. */
338
+ .sds-table__column-picker-group-title {
339
+ display: contents;
340
+ padding: 0;
341
+ }
@@ -0,0 +1,161 @@
1
+ <!-- sds-table-column-picker — ported from kdls-design-system components/Table/TableColumnPicker.tsx
2
+ (added there in 2.9.0 by Valdir Primo; read at commit 245652b).
3
+
4
+ A pill that opens the table's columns, grouped, one per line, to show or hide. It
5
+ applies on the tick — there is no "Aplicar" button, because a reader who ticks a box
6
+ and watches nothing happen ticks it again.
7
+
8
+ Divergences from KDLS, all of them consequences of how SDS already works:
9
+
10
+ The checkboxes report to ONE listener here, not one handler each. sds-checkbox does not
11
+ hydrate on purpose — it takes no onChange and lets the native change event bubble — so
12
+ the picker listens once on the panel and reads which box fired out of the input's own
13
+ name and value. sds-table does exactly this, for the same reason: a component per
14
+ checkbox is a component serialised per checkbox.
15
+
16
+ The ordering rule lives in table-logic.js as `toggleColumn`, tested without a DOM.
17
+ KDLS writes it inline in the component, where the rule that the caller gets its keys
18
+ back in the groups' order can only be checked by rendering React.
19
+
20
+ KDLS watches the floating portal with a MutationObserver to know when the panel has
21
+ been placed, because a portalled panel is hidden until then and a hidden element drops
22
+ focus(). SDS has no portal — Marko 5 has none — but it has the same wait for a
23
+ different reason: sds-floating renders its panel `visibility: hidden` until it has
24
+ measured and placed it, and focus() on a hidden element does nothing and reports
25
+ nothing. Confirmed in Chromium on 2026-09-17, where opening this picker left the caret
26
+ on `<body>`. The rule and its measurement are in Floating/focus-when-ready.js, shared
27
+ with sds-select, which met it too.
28
+
29
+ Escape is handled twice on purpose: sds-floating already closes on it, and this adds
30
+ the half KDLS also adds — putting focus back on the trigger. Closing a panel while the
31
+ focus stays inside it leaves a keyboard reader standing on nothing. -->
32
+
33
+ import { pickerKeys, toggleColumn } from "./table-logic.js";
34
+ import { focusWhenReady } from "../Floating/focus-when-ready.js";
35
+ import columnTriple from "../../icons/generated/fluent-column-triple-regular.marko";
36
+
37
+ class {
38
+ onCreate() {
39
+ this.state = { open: false };
40
+ }
41
+
42
+ setOpen(open) {
43
+ this.state.open = open;
44
+ }
45
+
46
+ /**
47
+ * Every box in the panel reports here.
48
+ *
49
+ * The name is the picker's own, so a checkbox belonging to something else that happens
50
+ * to sit inside the panel is not mistaken for a column.
51
+ */
52
+ handleChange(event) {
53
+ const target = event.target;
54
+ if (target.type !== "checkbox" || target.name !== this.checkboxName) return;
55
+ const { groups = [], value = [], onChange } = this.input;
56
+ onChange?.(toggleColumn(groups, value, target.value, target.checked));
57
+ }
58
+
59
+ get checkboxName() {
60
+ return `sds-table-column-picker-${this.id}`;
61
+ }
62
+
63
+ /**
64
+ * The first column the reader can actually change, so the keyboard lands on a control
65
+ * rather than on the panel — and it keeps asking until the panel can take the caret.
66
+ */
67
+ focusFirst() {
68
+ focusWhenReady(() => {
69
+ if (!this.state.open) return null;
70
+ const panel = this.getEl("panel");
71
+ return panel && panel.querySelector("input[type=checkbox]:not(:disabled)");
72
+ });
73
+ }
74
+
75
+ handleKey(event) {
76
+ if (event.key !== "Escape") return;
77
+ this.setOpen(false);
78
+ this.getEl("trigger")?.querySelector("button, [role=button]")?.focus();
79
+ }
80
+
81
+ onUpdate() {
82
+ if (this.state.open && !this.focused) {
83
+ this.focused = true;
84
+ this.focusFirst();
85
+ }
86
+ if (!this.state.open) this.focused = false;
87
+ }
88
+ }
89
+
90
+ $ {
91
+ const {
92
+ label = "Colunas",
93
+ groups,
94
+ value = [],
95
+ onReset,
96
+ class: extraClass = "",
97
+ } = input;
98
+
99
+ if (!Array.isArray(groups) || !groups.length) {
100
+ throw new Error("sds-table-column-picker needs `groups`.");
101
+ }
102
+
103
+ const open = state.open;
104
+ const offered = pickerKeys(groups);
105
+ const name = component.checkboxName;
106
+ }
107
+
108
+ <sds-popover
109
+ title=label
110
+ placement="bottomRight"
111
+ trigger="click"
112
+ arrow=false
113
+ open=open
114
+ onOpenChange=(next) => component.setOpen(next)
115
+ class=extraClass
116
+ >
117
+ <@content>
118
+ <div key="panel" class="sds-table__column-picker" on-change("handleChange") on-keydown("handleKey")>
119
+ <!-- The list scrolls, the reset link does not: `--menu-max-height` caps the rows so
120
+ the same seven and a half of them show whether or not there is a link below. -->
121
+ <div class="sds-table__column-picker-list sds-scroll sds-scroll--thin">
122
+ <for|group, index| of=groups>
123
+ <!-- A fieldset, so a screen reader announces the group a box belongs to instead
124
+ of reading fourteen unrelated checkboxes. -->
125
+ <fieldset class="sds-table__column-picker-group">
126
+ <if(group.title)>
127
+ <legend class="sds-table__column-picker-group-title">
128
+ <sds-text as="span" variant="labelSemiBold">${group.title}</sds-text>
129
+ </legend>
130
+ </if>
131
+ <for|column| of=group.columns>
132
+ <sds-checkbox
133
+ label=column.label
134
+ width="fill"
135
+ name=name
136
+ value=column.key
137
+ checked=value.includes(column.key)
138
+ disabled=column.disabled
139
+ hint=column.hint
140
+ />
141
+ </for>
142
+ </fieldset>
143
+ </for>
144
+ </div>
145
+ <if(onReset)>
146
+ <sds-button variant="link" size="md" label="Restaurar padrão" onClick=() => onReset()/>
147
+ </if>
148
+ </div>
149
+ </@content>
150
+
151
+ <span key="trigger">
152
+ <sds-pill
153
+ label=label
154
+ active=open
155
+ aria-haspopup="dialog"
156
+ aria-expanded=(open ? "true" : "false")
157
+ >
158
+ <@icon-left><${columnTriple}/></@icon-left>
159
+ </sds-pill>
160
+ </span>
161
+ </sds-popover>
@@ -29,6 +29,28 @@ export const SORT_DIRECTIONS = [
29
29
  { label: 'Decrescente', value: 'desc' },
30
30
  ];
31
31
 
32
+ /**
33
+ * How a sortable header wears its sort mark. From KDLS 2.8.0.
34
+ *
35
+ * `always` draws the up/down pair on every sortable header — the reader can see which
36
+ * columns sort before touching any of them, at the cost of a row of chevrons.
37
+ * `hover` keeps the unsorted headers clean and shows the pair only while a header is
38
+ * pointed at or focused, in the room it already occupies so nothing moves; the sorted
39
+ * header gets a single filled chevron instead of a coloured half of a pair.
40
+ *
41
+ * `always` stays the default: it is what the table did before the choice existed, and a
42
+ * new option that changes an existing table's appearance is not an option, it is a change.
43
+ */
44
+ export const SORT_INDICATORS = ['always', 'hover'];
45
+
46
+ export function assertSortIndicator(value) {
47
+ if (!SORT_INDICATORS.includes(value)) {
48
+ throw new Error(
49
+ `Unknown table sortIndicator: "${value}". Allowed: ${SORT_INDICATORS.join(', ')}`,
50
+ );
51
+ }
52
+ }
53
+
32
54
  /**
33
55
  * The mobile layout triggers off the table's own width, not the viewport's.
34
56
  *
@@ -127,3 +149,37 @@ export function ariaSortFor(column, sort) {
127
149
  if (sort?.key !== column.key) return 'none';
128
150
  return sort.direction === 'asc' ? 'ascending' : 'descending';
129
151
  }
152
+
153
+ /* ── Column picker ──────────────────────────────────────────────────────────
154
+ The arithmetic behind sds-table-column-picker. From KDLS 2.9.0, where it lives inside
155
+ the component; here it is a pair of pure functions so the ordering rule can be tested
156
+ without a DOM, and so the component stays a template. ─────────────────────── */
157
+
158
+ /** Every key the picker offers, in the order the groups list them. */
159
+ export function pickerKeys(groups = []) {
160
+ return groups.flatMap((group) => group.columns.map((column) => column.key));
161
+ }
162
+
163
+ /**
164
+ * The visible columns after one box is ticked or unticked.
165
+ *
166
+ * Two rules, and both matter to what the reader sees:
167
+ *
168
+ * The result comes back in the GROUPS' order, never the order the boxes were clicked. The
169
+ * caller renders the columns in the order it is handed, so appending would let the table's
170
+ * column order depend on the sequence somebody happened to tick things in.
171
+ *
172
+ * A key the caller shows but no group offers is kept, at the end. That is the fixed first
173
+ * column, or an actions column — a column the picker deliberately does not let anyone turn
174
+ * off, and therefore one it must not turn off by omission either.
175
+ */
176
+ export function toggleColumn(groups, value = [], key, checked) {
177
+ const offered = pickerKeys(groups);
178
+ const next = new Set(value);
179
+ if (checked) next.add(key);
180
+ else next.delete(key);
181
+ return [
182
+ ...offered.filter((k) => next.has(k)),
183
+ ...value.filter((k) => !offered.includes(k) && next.has(k)),
184
+ ];
185
+ }
@@ -29,11 +29,13 @@
29
29
 
30
30
  import {
31
31
  gridTemplate, nextSort, sortRows, rowId, toggleId, toggleAll, headerCheckState,
32
- sortFieldOptions, ariaSortFor, SORT_DIRECTIONS, NONE_SORT, MOBILE_BREAKPOINT,
33
- SELECT_ALL_NAME, SELECT_ROW_NAME,
32
+ sortFieldOptions, ariaSortFor, assertSortIndicator, SORT_DIRECTIONS, NONE_SORT,
33
+ MOBILE_BREAKPOINT, SELECT_ALL_NAME, SELECT_ROW_NAME,
34
34
  } from "./table-logic.js";
35
35
  import chevronUp from "../../icons/generated/fluent-chevron-up-regular.marko";
36
36
  import chevronDown from "../../icons/generated/fluent-chevron-down-regular.marko";
37
+ import chevronUpFilled from "../../icons/generated/fluent-chevron-up-filled.marko";
38
+ import chevronDownFilled from "../../icons/generated/fluent-chevron-down-filled.marko";
37
39
 
38
40
  class {
39
41
  onCreate(input) {
@@ -132,6 +134,8 @@ $ {
132
134
  rowSelection = false,
133
135
  selectedIds = [],
134
136
  onRowClick,
137
+ sortIndicator = "always",
138
+ loading = false,
135
139
  pagination,
136
140
  emptyMessage = "Nenhum resultado",
137
141
  label = "Tabela",
@@ -142,6 +146,7 @@ $ {
142
146
  if (variant !== "default" && variant !== "cardLines") {
143
147
  throw new Error(`Unknown table variant: "${variant}". Allowed: default, cardLines`);
144
148
  }
149
+ assertSortIndicator(sortIndicator);
145
150
 
146
151
  const sort = component.sort;
147
152
  const ordered = sortRows(rows, columns, sort);
@@ -164,11 +169,20 @@ $ {
164
169
  "sds-table",
165
170
  variant !== "default" ? `sds-table--${variant}` : "",
166
171
  bordered ? "sds-table--bordered" : "",
172
+ loading ? "sds-table--loading" : "",
167
173
  extraClass,
168
174
  ].filter(Boolean).join(" ");
169
175
  }
170
176
 
171
- <div key="root" class=rootClass role="table" aria-label=label aria-rowcount=String(ordered.length) on-change("handleChange")>
177
+ <div
178
+ key="root"
179
+ class=rootClass
180
+ role="table"
181
+ aria-label=label
182
+ aria-rowcount=String(ordered.length)
183
+ aria-busy=(loading ? "true" : null)
184
+ on-change("handleChange")
185
+ >
172
186
  <if(input.header)>
173
187
  <${input.header}/>
174
188
  </if>
@@ -245,15 +259,39 @@ $ {
245
259
  <span class="sds-table__col-title">${column.title ?? ""}</span>
246
260
  <!-- Both arrows, with the live one coloured — KDLS's shape, and the
247
261
  right one: a single arrow leaves a visitor guessing whether the
248
- column is sorted at all or just sortable. -->
249
- <span class="sds-table__sort" aria-hidden="true">
250
- <sds-icon size="sm" color=(sort?.key === column.key && sort.direction === "asc" ? "primary" : undefined)>
251
- <${chevronUp}/>
252
- </sds-icon>
253
- <sds-icon size="sm" color=(sort?.key === column.key && sort.direction === "desc" ? "primary" : undefined)>
254
- <${chevronDown}/>
255
- </sds-icon>
256
- </span>
262
+ column is sorted at all or just sortable.
263
+
264
+ Under sortIndicator="hover" the sorted column is the exception: it
265
+ already answers "is it sorted" by being the only marked header, so it
266
+ shows one filled arrow instead of a pair with one half greyed. Every
267
+ other header keeps the pair, hidden by opacity rather than removed —
268
+ taking it out of the DOM would let the header resize when the pointer
269
+ arrives, and a header that grows on hover pushes the one beside it. -->
270
+ <if(sortIndicator === "hover" && sort?.key === column.key)>
271
+ <span class="sds-table__sort" aria-hidden="true">
272
+ <sds-icon size="sm" color="primary">
273
+ <if(sort.direction === "asc")>
274
+ <${chevronUpFilled}/>
275
+ </if>
276
+ <else>
277
+ <${chevronDownFilled}/>
278
+ </else>
279
+ </sds-icon>
280
+ </span>
281
+ </if>
282
+ <else>
283
+ <span
284
+ class=["sds-table__sort", sortIndicator === "hover" ? "sds-table__sort--compact" : ""].filter(Boolean).join(" ")
285
+ aria-hidden="true"
286
+ >
287
+ <sds-icon size="sm" color=(sort?.key === column.key && sort.direction === "asc" ? "primary" : undefined)>
288
+ <${chevronUp}/>
289
+ </sds-icon>
290
+ <sds-icon size="sm" color=(sort?.key === column.key && sort.direction === "desc" ? "primary" : undefined)>
291
+ <${chevronDown}/>
292
+ </sds-icon>
293
+ </span>
294
+ </else>
257
295
  </button>
258
296
  </if>
259
297
  <else>
@@ -71,8 +71,8 @@
71
71
  KDLS gives it no focus style: a contenteditable div with no ring is a control a keyboard
72
72
  user cannot tell they have reached. */
73
73
  .sds-text-editor__body:focus-visible {
74
- outline: var(--focus-ring-width) solid var(--focus-stroke-color);
75
- outline-offset: -2px;
74
+ outline: var(--focus-ring-outline);
75
+ outline-offset: calc(var(--focus-ring-outline-offset) * -1);
76
76
  }
77
77
 
78
78
  /* The toolbar's own buttons are sds-icon-button and sds-menu, which carry their own. */
@@ -167,3 +167,43 @@
167
167
  box-shadow: var(--focus-ring);
168
168
  }
169
169
 
170
+ /* ── The site's gradient, on the stroke ────────────────────────────────────────
171
+
172
+ `border-color` rejects a gradient, so a stroke normally reads the `-solid` companion —
173
+ the gradient's first stop, flat. These rules paint it properly instead: the gradient on
174
+ `border-box`, the surface colour on `padding-box`, over a transparent border. Only the
175
+ border's own 2px show the gradient, and `border-radius` still works, which is what
176
+ `border-image` would cost.
177
+
178
+ `[data-primary-gradient]` is written by `applyCustomGradient` and removed by
179
+ `applyCustomColor` — a stylesheet cannot ask what a custom property contains, so the
180
+ code that fills the slot says so. KDLS wrote the same rules under
181
+ `[data-color-scheme="lya"]`, its own brand gradient; SDS deleted every `lya` block with
182
+ the brand and these went with them. Keyed on the marker they serve any site's gradient.
183
+ ─────────────────────────────────────────────────────────────────────────── */
184
+
185
+ [data-primary-gradient] .sds-toggle:hover:not(.sds-toggle--disabled) {
186
+ background:
187
+ linear-gradient(var(--component-bg-color), var(--component-bg-color)) padding-box,
188
+ var(--color-primary-50) border-box;
189
+ border-color: transparent;
190
+ }
191
+
192
+ /* ── And when the gradient FILLS this, it has no border to fight ──────────────
193
+
194
+ Filled from the slot and ringed with the `-solid` companion: the same colour while the
195
+ slot is solid, and a flat hoop of the first stop once it is a gradient. `border: none`
196
+ because under a gradient the fill IS the whole control.
197
+
198
+ Touched states brighten or darken rather than stepping to tone 60 or 70 — stepping a
199
+ gradient's tone moves every stop at once and reads as a different colour, not as the
200
+ same control being pressed. Same answer as the checked checkbox. ─────────── */
201
+ [data-primary-gradient] .sds-toggle__item--active {
202
+ background: var(--color-primary-50);
203
+ border: none;
204
+ }
205
+ [data-primary-gradient] .sds-toggle__item--active:hover:not(:disabled) {
206
+ background: var(--color-primary-50);
207
+ border: none;
208
+ filter: brightness(1.1);
209
+ }
@@ -110,6 +110,6 @@
110
110
  }
111
111
 
112
112
  .sds-tooltip-wrapper__trigger:focus-visible {
113
- outline: var(--focus-ring-width) solid var(--focus-stroke-color);
114
- outline-offset: 2px;
113
+ outline: var(--focus-ring-outline);
114
+ outline-offset: var(--focus-ring-outline-offset);
115
115
  }
@@ -178,3 +178,26 @@ button.sds-uploader {
178
178
  .sds-uploader__error {
179
179
  color: var(--color-error-50);
180
180
  }
181
+
182
+ /* ── The site's gradient, on the stroke ────────────────────────────────────────
183
+
184
+ `border-color` rejects a gradient, so a stroke normally reads the `-solid` companion —
185
+ the gradient's first stop, flat. These rules paint it properly instead: the gradient on
186
+ `border-box`, the surface colour on `padding-box`, over a transparent border. Only the
187
+ border's own 2px show the gradient, and `border-radius` still works, which is what
188
+ `border-image` would cost.
189
+
190
+ `[data-primary-gradient]` is written by `applyCustomGradient` and removed by
191
+ `applyCustomColor` — a stylesheet cannot ask what a custom property contains, so the
192
+ code that fills the slot says so. KDLS wrote the same rules under
193
+ `[data-color-scheme="lya"]`, its own brand gradient; SDS deleted every `lya` block with
194
+ the brand and these went with them. Keyed on the marker they serve any site's gradient.
195
+ ─────────────────────────────────────────────────────────────────────────── */
196
+
197
+ [data-primary-gradient] .sds-uploader:hover,
198
+ [data-primary-gradient] .sds-uploader--dragging {
199
+ background:
200
+ var(--color-primary-10) padding-box,
201
+ var(--color-primary-50) border-box;
202
+ border-color: transparent;
203
+ }