@neuravision/construct 2.1.1 → 2.1.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.
- package/CHANGELOG.md +46 -0
- package/README.md +13 -0
- package/behaviors/combobox.js +43 -4
- package/behaviors/dropdown.js +16 -2
- package/behaviors/index.d.ts +2 -0
- package/behaviors/internal/dialog-controller.js +79 -9
- package/behaviors/internal/dom.js +38 -7
- package/behaviors/internal/overlay.js +4 -4
- package/behaviors/internal/popup-layer.js +2 -0
- package/behaviors/popover.js +21 -12
- package/behaviors/select-menu.js +20 -2
- package/behaviors/tabs.js +13 -2
- package/behaviors/toggle-group.js +37 -5
- package/behaviors/tooltip.js +26 -2
- package/components/README.md +40 -0
- package/components/_shared.css +11 -0
- package/components/accordion.css +8 -0
- package/components/app-shell-v2.css +7 -0
- package/components/app-shell.css +7 -0
- package/components/banner.css +32 -1
- package/components/card.css +23 -9
- package/components/chart.css +26 -0
- package/components/checkbox.css +32 -0
- package/components/chip.css +29 -1
- package/components/combobox.css +21 -0
- package/components/datepicker.css +55 -8
- package/components/datum.css +84 -11
- package/components/dropdown.css +23 -3
- package/components/field.css +6 -4
- package/components/file-upload.css +1 -1
- package/components/list.css +28 -2
- package/components/navbar.css +14 -1
- package/components/pagination.css +18 -0
- package/components/progress-bar.css +22 -1
- package/components/select-menu.css +8 -0
- package/components/select.css +8 -0
- package/components/sidebar.css +15 -2
- package/components/skip-link.css +6 -2
- package/components/slider.css +5 -0
- package/components/switch.css +62 -2
- package/components/table.css +53 -4
- package/components/tabs.css +55 -3
- package/components/toast.css +10 -1
- package/components/toggle-group.css +16 -5
- package/components/toolbar.css +17 -1
- package/components/tree.css +30 -0
- package/docs/behaviors.md +26 -5
- package/docs/guidelines.md +30 -2
- package/foundations.css +6 -0
- package/package.json +7 -3
- package/schemas/components.schema.json +16 -1
- package/schemas/semantic.schema.json +2 -1
- package/schemas/theme.schema.json +1 -1
- package/scripts/build-tokens.mjs +87 -5
- package/themes/README.md +37 -0
- package/themes/walnut.theme.json +106 -0
- package/tokens/README.md +10 -2
- package/tokens/components.json +10 -0
- package/tokens/semantic.dark.json +4 -3
- package/tokens/semantic.high-contrast-dark.json +94 -0
- package/tokens/semantic.high-contrast.json +3 -2
- package/tokens/semantic.light.json +2 -1
- package/tokens/tokens.css +431 -33
- package/tokens/tokens.js +432 -133
- package/tokens/tokens.json +432 -133
- package/tokens/tokens.ts +432 -133
package/components/datum.css
CHANGED
|
@@ -6,9 +6,12 @@
|
|
|
6
6
|
promote that signature from focus rings and the intro page into
|
|
7
7
|
reusable structural vocabulary every surface can speak.
|
|
8
8
|
|
|
9
|
-
Accessibility contract (must hold across
|
|
10
|
-
· orange as TEXT
|
|
11
|
-
|
|
9
|
+
Accessibility contract (must hold across all built-in themes):
|
|
10
|
+
· orange as TEXT → never; use --color-text-* neutrals (orange fails
|
|
11
|
+
4.5:1 as small text on several theme surfaces)
|
|
12
|
+
· orange as FILL vs muted tracks → --color-brand-accent-strong
|
|
13
|
+
(validated ≥3:1 against bg.muted/canvas/surface per theme)
|
|
14
|
+
· orange as EDGE/RULE (decorative, on canvas/surface) → --color-brand-accent
|
|
12
15
|
============================================================ */
|
|
13
16
|
|
|
14
17
|
/* ── Eyebrow ──
|
|
@@ -32,27 +35,76 @@
|
|
|
32
35
|
|
|
33
36
|
.ct-eyebrow::before {
|
|
34
37
|
content: '';
|
|
35
|
-
width:
|
|
36
|
-
height:
|
|
38
|
+
width: var(--component-datum-tick-width);
|
|
39
|
+
height: var(--component-datum-tick-height);
|
|
37
40
|
border-radius: var(--radius-xs);
|
|
38
41
|
background: var(--color-brand-accent);
|
|
39
42
|
}
|
|
40
43
|
|
|
44
|
+
/* ── Numbered eyebrow ──
|
|
45
|
+
Sheet numbers, like an engineering drawing set: `--numbered` inserts
|
|
46
|
+
an auto-incremented, zero-padded counter between tick and label.
|
|
47
|
+
Scope numbering with a `.ct-eyebrow-set` ancestor (or any element
|
|
48
|
+
that declares `counter-reset: ct-eyebrow`). */
|
|
49
|
+
.ct-eyebrow-set {
|
|
50
|
+
counter-reset: ct-eyebrow;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
.ct-eyebrow--numbered {
|
|
54
|
+
counter-increment: ct-eyebrow;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
.ct-eyebrow--numbered::before {
|
|
58
|
+
order: -2;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
.ct-eyebrow--numbered::after {
|
|
62
|
+
content: counter(ct-eyebrow, decimal-leading-zero);
|
|
63
|
+
order: -1;
|
|
64
|
+
color: var(--color-text-muted);
|
|
65
|
+
font-variant-numeric: tabular-nums;
|
|
66
|
+
}
|
|
67
|
+
|
|
41
68
|
/* ── Datum scale ──
|
|
42
69
|
A measured reference line: a hairline baseline with hanging ruler
|
|
43
70
|
ticks (minor every 12px, major every 60px) and an orange "origin"
|
|
44
71
|
segment marking zero. Decorative — mark it role="presentation". */
|
|
45
72
|
.ct-datum-scale {
|
|
46
|
-
|
|
73
|
+
--ct-datum-scale-accent: var(--color-brand-accent);
|
|
74
|
+
--ct-datum-scale-origin: var(--component-datum-origin-length);
|
|
75
|
+
|
|
76
|
+
height: var(--component-datum-scale-height);
|
|
47
77
|
background:
|
|
48
78
|
/* orange origin segment — the datum's zero */
|
|
49
|
-
linear-gradient(var(--
|
|
79
|
+
linear-gradient(var(--ct-datum-scale-accent), var(--ct-datum-scale-accent)) 0 0 / var(--ct-datum-scale-origin) 2px no-repeat,
|
|
50
80
|
/* full-width baseline hairline */
|
|
51
81
|
linear-gradient(var(--color-border-strong), var(--color-border-strong)) 0 0 / 100% 1px no-repeat,
|
|
52
82
|
/* major ticks */
|
|
53
|
-
repeating-linear-gradient(90deg, var(--color-border-strong) 0 1px, transparent 1px
|
|
83
|
+
repeating-linear-gradient(90deg, var(--color-border-strong) 0 1px, transparent 1px var(--component-datum-scale-major-step)) 0 0 / 100% 8px no-repeat,
|
|
54
84
|
/* minor ticks */
|
|
55
|
-
repeating-linear-gradient(90deg, var(--color-border-default) 0 1px, transparent 1px
|
|
85
|
+
repeating-linear-gradient(90deg, var(--color-border-default) 0 1px, transparent 1px var(--component-datum-scale-minor-step)) 0 0 / 100% 5px no-repeat;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/* RTL: the origin and tick phase anchor to the inline start. */
|
|
89
|
+
.ct-datum-scale:is([dir='rtl'] *, :dir(rtl)) {
|
|
90
|
+
background:
|
|
91
|
+
linear-gradient(var(--ct-datum-scale-accent), var(--ct-datum-scale-accent)) 100% 0 / var(--ct-datum-scale-origin) 2px no-repeat,
|
|
92
|
+
linear-gradient(var(--color-border-strong), var(--color-border-strong)) 0 0 / 100% 1px no-repeat,
|
|
93
|
+
repeating-linear-gradient(270deg, var(--color-border-strong) 0 1px, transparent 1px var(--component-datum-scale-major-step)) 0 0 / 100% 8px no-repeat,
|
|
94
|
+
repeating-linear-gradient(270deg, var(--color-border-default) 0 1px, transparent 1px var(--component-datum-scale-minor-step)) 0 0 / 100% 5px no-repeat;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/* ── Progress variant ──
|
|
98
|
+
The origin segment becomes meaning-bearing: its length reports
|
|
99
|
+
progress. Set `--ct-datum-scale-progress` (a percentage) on the
|
|
100
|
+
element and give it `role="progressbar"` with the matching
|
|
101
|
+
`aria-valuenow/min/max`. Meaning-bearing fill uses accent-strong,
|
|
102
|
+
validated ≥3:1 against muted/canvas/surface tracks per theme. */
|
|
103
|
+
.ct-datum-scale--progress {
|
|
104
|
+
--ct-datum-scale-accent: var(--color-brand-accent-strong);
|
|
105
|
+
--ct-datum-scale-origin: var(--ct-datum-scale-progress, 0%);
|
|
106
|
+
|
|
107
|
+
transition: background-size var(--duration-normal) var(--easing-standard);
|
|
56
108
|
}
|
|
57
109
|
|
|
58
110
|
/* ── Spec datasheet ──
|
|
@@ -103,7 +155,7 @@
|
|
|
103
155
|
:is(.ct-metric--active, .ct-metric[data-state='active'], .ct-spec--origin) {
|
|
104
156
|
--ct-metric-accent: var(--color-brand-accent);
|
|
105
157
|
|
|
106
|
-
box-shadow: inset 0 var(--
|
|
158
|
+
box-shadow: inset 0 var(--component-datum-edge-width) 0 0 var(--ct-metric-accent);
|
|
107
159
|
}
|
|
108
160
|
|
|
109
161
|
:is(.ct-metric__value, .ct-spec__value) {
|
|
@@ -186,7 +238,7 @@
|
|
|
186
238
|
|
|
187
239
|
.ct-metric[data-status],
|
|
188
240
|
.ct-metric[data-state='error'] {
|
|
189
|
-
box-shadow: inset var(--
|
|
241
|
+
box-shadow: inset var(--component-datum-edge-width) 0 0 0 var(--ct-metric-accent);
|
|
190
242
|
}
|
|
191
243
|
|
|
192
244
|
.ct-metric:is([aria-busy='true'], [data-state='loading']) {
|
|
@@ -197,6 +249,27 @@
|
|
|
197
249
|
opacity: var(--opacity-disabled);
|
|
198
250
|
}
|
|
199
251
|
|
|
252
|
+
/* ── Datum frame ──
|
|
253
|
+
The corner registration mark generalised beyond cards: an opt-in
|
|
254
|
+
"exhibit" treatment for figures, code samples, and media. Decorative;
|
|
255
|
+
forced colors keeps the borders via CanvasText automatically. */
|
|
256
|
+
.ct-datum-frame {
|
|
257
|
+
position: relative;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
.ct-datum-frame::before {
|
|
261
|
+
content: '';
|
|
262
|
+
position: absolute;
|
|
263
|
+
inset-block-start: 0;
|
|
264
|
+
inset-inline-start: 0;
|
|
265
|
+
width: var(--component-datum-mark-size);
|
|
266
|
+
height: var(--component-datum-mark-size);
|
|
267
|
+
border-block-start: var(--border-medium) solid var(--color-brand-accent);
|
|
268
|
+
border-inline-start: var(--border-medium) solid var(--color-brand-accent);
|
|
269
|
+
border-start-start-radius: var(--radius-lg);
|
|
270
|
+
pointer-events: none;
|
|
271
|
+
}
|
|
272
|
+
|
|
200
273
|
@media (forced-colors: active) {
|
|
201
274
|
:is(.ct-metric--active, .ct-metric[data-state='active'], .ct-metric[data-status], .ct-metric[data-state='error'], .ct-spec--origin) {
|
|
202
275
|
border-block-start: var(--border-medium) solid currentColor;
|
package/components/dropdown.css
CHANGED
|
@@ -146,7 +146,7 @@
|
|
|
146
146
|
/* ── Danger item ─────────────────────────────────────────────────── */
|
|
147
147
|
|
|
148
148
|
.ct-dropdown__item--danger {
|
|
149
|
-
color: var(--color-state-danger);
|
|
149
|
+
color: var(--color-state-danger-text);
|
|
150
150
|
}
|
|
151
151
|
|
|
152
152
|
@media (hover: hover) {
|
|
@@ -158,11 +158,11 @@
|
|
|
158
158
|
.ct-dropdown__item--danger:focus-visible,
|
|
159
159
|
.ct-dropdown__item--danger[data-highlighted] {
|
|
160
160
|
background: var(--color-state-danger-surface);
|
|
161
|
-
outline-color: var(--color-state-danger);
|
|
161
|
+
outline-color: var(--color-state-danger-text);
|
|
162
162
|
}
|
|
163
163
|
|
|
164
164
|
.ct-dropdown__item--danger .ct-dropdown__item-icon {
|
|
165
|
-
color: var(--color-state-danger);
|
|
165
|
+
color: var(--color-state-danger-text);
|
|
166
166
|
}
|
|
167
167
|
|
|
168
168
|
.ct-dropdown__item--danger .ct-dropdown__item-shortcut {
|
|
@@ -389,6 +389,15 @@
|
|
|
389
389
|
--ct-dropdown-item-gap: var(--space-4);
|
|
390
390
|
}
|
|
391
391
|
|
|
392
|
+
/* ── Construct 44×44 target policy (WCAG 2.2 SC 2.5.5 is AAA) ────── */
|
|
393
|
+
|
|
394
|
+
@media (pointer: coarse) {
|
|
395
|
+
.ct-dropdown__item,
|
|
396
|
+
.ct-dropdown__sub-trigger {
|
|
397
|
+
min-block-size: 44px;
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
|
|
392
401
|
/* ── Reduced motion ──────────────────────────────────────────────── */
|
|
393
402
|
|
|
394
403
|
@media (prefers-reduced-motion: reduce) {
|
|
@@ -399,3 +408,14 @@
|
|
|
399
408
|
transition: none;
|
|
400
409
|
}
|
|
401
410
|
}
|
|
411
|
+
|
|
412
|
+
/* ── Forced colors ───────────────────────────────────────────────── */
|
|
413
|
+
|
|
414
|
+
@media (forced-colors: active) {
|
|
415
|
+
/* The radio-item dot is background-drawn and must survive forced
|
|
416
|
+
backgrounds. */
|
|
417
|
+
.ct-dropdown__item[aria-checked='true'] .ct-dropdown__item-radio::before {
|
|
418
|
+
background: SelectedItem;
|
|
419
|
+
forced-color-adjust: none;
|
|
420
|
+
}
|
|
421
|
+
}
|
package/components/field.css
CHANGED
|
@@ -16,12 +16,14 @@
|
|
|
16
16
|
|
|
17
17
|
.ct-field__error {
|
|
18
18
|
font-size: var(--font-size-sm);
|
|
19
|
-
color: var(--color-state-danger);
|
|
19
|
+
color: var(--color-state-danger-text);
|
|
20
20
|
}
|
|
21
21
|
|
|
22
22
|
.ct-field__label--required::after {
|
|
23
|
-
|
|
24
|
-
|
|
23
|
+
/* Alt-text syntax hides the decorative asterisk from assistive tech;
|
|
24
|
+
the `required` attribute is the programmatic signal. */
|
|
25
|
+
content: ' *' / '';
|
|
26
|
+
color: var(--color-state-danger-text);
|
|
25
27
|
}
|
|
26
28
|
|
|
27
29
|
.ct-field__counter {
|
|
@@ -31,7 +33,7 @@
|
|
|
31
33
|
}
|
|
32
34
|
|
|
33
35
|
.ct-field__counter--limit {
|
|
34
|
-
color: var(--color-state-danger);
|
|
36
|
+
color: var(--color-state-danger-text);
|
|
35
37
|
}
|
|
36
38
|
|
|
37
39
|
.ct-field__row {
|
package/components/list.css
CHANGED
|
@@ -90,6 +90,9 @@
|
|
|
90
90
|
min-height: 0;
|
|
91
91
|
text-decoration: none;
|
|
92
92
|
color: inherit;
|
|
93
|
+
/* The datum edge slides in on selection instead of popping. */
|
|
94
|
+
transition: background var(--duration-fast) var(--easing-standard),
|
|
95
|
+
box-shadow var(--duration-fast) var(--easing-standard);
|
|
93
96
|
}
|
|
94
97
|
|
|
95
98
|
/* ── Item Slots ── */
|
|
@@ -222,7 +225,7 @@
|
|
|
222
225
|
.ct-list__item[aria-selected='true'] {
|
|
223
226
|
background: var(--ct-list-item-bg-active);
|
|
224
227
|
/* The datum: an orange leading edge marks the active/selected item. */
|
|
225
|
-
box-shadow: inset var(--
|
|
228
|
+
box-shadow: inset var(--component-datum-edge-width) 0 0 0 var(--color-brand-accent);
|
|
226
229
|
}
|
|
227
230
|
|
|
228
231
|
.ct-list__item--active .ct-list__item-title,
|
|
@@ -351,7 +354,8 @@
|
|
|
351
354
|
.ct-list--nav .ct-list__item {
|
|
352
355
|
cursor: pointer;
|
|
353
356
|
transition: background var(--duration-fast) var(--easing-standard),
|
|
354
|
-
color var(--duration-fast) var(--easing-standard)
|
|
357
|
+
color var(--duration-fast) var(--easing-standard),
|
|
358
|
+
box-shadow var(--duration-fast) var(--easing-standard);
|
|
355
359
|
}
|
|
356
360
|
|
|
357
361
|
@media (hover: hover) {
|
|
@@ -475,3 +479,25 @@
|
|
|
475
479
|
animation: none;
|
|
476
480
|
}
|
|
477
481
|
}
|
|
482
|
+
|
|
483
|
+
/* ── Forced Colors ── */
|
|
484
|
+
|
|
485
|
+
@media (forced-colors: active) {
|
|
486
|
+
/* The datum edge is an inset box-shadow, which forced colors drops —
|
|
487
|
+
re-encode it as a border. */
|
|
488
|
+
.ct-list__item--active,
|
|
489
|
+
.ct-list__item[data-state='active'],
|
|
490
|
+
.ct-list__item[aria-selected='true'] {
|
|
491
|
+
border-inline-start: var(--component-datum-edge-width) solid SelectedItem;
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
/* Skeleton rows hide their placeholder text with `color: transparent`;
|
|
495
|
+
keep the decorative loading state hidden when forced colors would
|
|
496
|
+
otherwise reveal it. */
|
|
497
|
+
.ct-list__item--skeleton .ct-list__item-leading,
|
|
498
|
+
.ct-list__item--skeleton .ct-list__item-title,
|
|
499
|
+
.ct-list__item--skeleton .ct-list__item-description,
|
|
500
|
+
.ct-list__item--skeleton .ct-list__item-trailing {
|
|
501
|
+
forced-color-adjust: none;
|
|
502
|
+
}
|
|
503
|
+
}
|
package/components/navbar.css
CHANGED
|
@@ -31,7 +31,8 @@
|
|
|
31
31
|
gap: var(--ct-navbar-gap);
|
|
32
32
|
width: 100%;
|
|
33
33
|
min-width: 0;
|
|
34
|
-
height:
|
|
34
|
+
/* min-height, not height: the bar must grow with text scaling. */
|
|
35
|
+
min-height: var(--ct-navbar-height);
|
|
35
36
|
padding: var(--ct-navbar-padding-y) var(--ct-navbar-padding-x);
|
|
36
37
|
background: var(--ct-navbar-bg);
|
|
37
38
|
border-bottom: var(--border-thin) solid var(--ct-navbar-border-color);
|
|
@@ -462,6 +463,8 @@
|
|
|
462
463
|
|
|
463
464
|
/* ── Sticky ────────────────────────────────────────────────────── */
|
|
464
465
|
|
|
466
|
+
/* Consumers: set --ct-scroll-offset-top on :root (see foundations.css) so
|
|
467
|
+
scrolled-to content is not obscured behind the bar (WCAG 2.4.11). */
|
|
465
468
|
.ct-navbar--sticky {
|
|
466
469
|
position: sticky;
|
|
467
470
|
inset-block-start: 0;
|
|
@@ -469,6 +472,8 @@
|
|
|
469
472
|
|
|
470
473
|
/* ── Fixed ─────────────────────────────────────────────────────── */
|
|
471
474
|
|
|
475
|
+
/* Consumers: set --ct-scroll-offset-top on :root (see foundations.css) so
|
|
476
|
+
scrolled-to content is not obscured behind the bar (WCAG 2.4.11). */
|
|
472
477
|
.ct-navbar--fixed {
|
|
473
478
|
position: fixed;
|
|
474
479
|
inset-block-start: 0;
|
|
@@ -673,6 +678,14 @@
|
|
|
673
678
|
}
|
|
674
679
|
}
|
|
675
680
|
|
|
681
|
+
/* ── Construct 44×44 target policy (WCAG 2.2 SC 2.5.5 is AAA) ──── */
|
|
682
|
+
|
|
683
|
+
@media (pointer: coarse) {
|
|
684
|
+
.ct-navbar__link {
|
|
685
|
+
min-height: 44px;
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
|
|
676
689
|
/* ── Reduced Motion ────────────────────────────────────────────── */
|
|
677
690
|
|
|
678
691
|
@media (prefers-reduced-motion: reduce) {
|
|
@@ -110,6 +110,15 @@
|
|
|
110
110
|
--ct-pagination-line-height: var(--line-height-md);
|
|
111
111
|
}
|
|
112
112
|
|
|
113
|
+
/* Construct 44×44 target policy: --control-height-xs is not coarse-remapped
|
|
114
|
+
in _shared.css, so the small variant is bumped explicitly. */
|
|
115
|
+
@media (pointer: coarse) {
|
|
116
|
+
.ct-pagination--sm {
|
|
117
|
+
--ct-pagination-height: 44px;
|
|
118
|
+
--ct-pagination-min-width: 44px;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
113
122
|
/* ── Pagination: Responsive ───────────────────────────────── */
|
|
114
123
|
|
|
115
124
|
@media (max-width: 599px) { /* < sm breakpoint (600px) */
|
|
@@ -129,3 +138,12 @@
|
|
|
129
138
|
transition: none;
|
|
130
139
|
}
|
|
131
140
|
}
|
|
141
|
+
|
|
142
|
+
/* ── Pagination: Forced Colors ────────────────────────────── */
|
|
143
|
+
|
|
144
|
+
@media (forced-colors: active) {
|
|
145
|
+
.ct-pagination__link[aria-current='page'] {
|
|
146
|
+
outline: var(--border-medium) solid currentColor;
|
|
147
|
+
outline-offset: calc(-1 * var(--border-medium));
|
|
148
|
+
}
|
|
149
|
+
}
|
|
@@ -4,12 +4,16 @@
|
|
|
4
4
|
--ct-progress-height: 8px;
|
|
5
5
|
--ct-progress-radius: var(--radius-pill);
|
|
6
6
|
--ct-progress-bg: var(--color-bg-muted);
|
|
7
|
-
|
|
7
|
+
/* accent-strong, not accent: the fill must hold 3:1 against the muted
|
|
8
|
+
track (1.4.11) — validated as "strong accent on muted" per theme. */
|
|
9
|
+
--ct-progress-fill: var(--color-brand-accent-strong);
|
|
8
10
|
|
|
9
11
|
width: 100%;
|
|
10
12
|
height: var(--ct-progress-height);
|
|
11
13
|
border-radius: var(--ct-progress-radius);
|
|
12
14
|
background: var(--ct-progress-bg);
|
|
15
|
+
/* the track boundary, so the component is identifiable on any surface */
|
|
16
|
+
border: var(--border-thin) solid var(--color-border-default);
|
|
13
17
|
overflow: hidden;
|
|
14
18
|
}
|
|
15
19
|
|
|
@@ -52,5 +56,22 @@
|
|
|
52
56
|
@media (prefers-reduced-motion: reduce) {
|
|
53
57
|
.ct-progress-bar--indeterminate .ct-progress-bar__track {
|
|
54
58
|
animation: none;
|
|
59
|
+
/* A frozen 30% sweep reads as determinate-and-stuck; a full-width
|
|
60
|
+
dimmed fill reads as "busy, extent unknown". */
|
|
61
|
+
width: 100%;
|
|
62
|
+
opacity: 0.4;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
@media (forced-colors: active) {
|
|
67
|
+
/* Backgrounds are forced to Canvas — without these the whole bar
|
|
68
|
+
disappears (mirrors the meter's forced-colors strategy). */
|
|
69
|
+
.ct-progress-bar {
|
|
70
|
+
border: 1px solid ButtonText;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
.ct-progress-bar__track {
|
|
74
|
+
background: SelectedItem;
|
|
75
|
+
forced-color-adjust: none;
|
|
55
76
|
}
|
|
56
77
|
}
|
|
@@ -257,6 +257,14 @@
|
|
|
257
257
|
--ct-select-menu-option-padding-y: var(--space-4);
|
|
258
258
|
}
|
|
259
259
|
|
|
260
|
+
/* ── Construct 44×44 target policy (WCAG 2.2 SC 2.5.5 is AAA) ───── */
|
|
261
|
+
|
|
262
|
+
@media (pointer: coarse) {
|
|
263
|
+
.ct-select-menu__option {
|
|
264
|
+
min-block-size: 44px;
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
260
268
|
/* ── Reduced motion ─────────────────────────────────────────────── */
|
|
261
269
|
|
|
262
270
|
@media (prefers-reduced-motion: reduce) {
|
package/components/select.css
CHANGED
|
@@ -28,3 +28,11 @@
|
|
|
28
28
|
appearance: none;
|
|
29
29
|
padding-inline-end: calc(var(--space-4) + 0.75em + var(--space-4));
|
|
30
30
|
}
|
|
31
|
+
|
|
32
|
+
@media (forced-colors: active) {
|
|
33
|
+
/* The native arrow is removed via appearance: none, so the mask-drawn
|
|
34
|
+
chevron must be repainted with system ink. */
|
|
35
|
+
.ct-select-wrap::after {
|
|
36
|
+
background-color: ButtonText;
|
|
37
|
+
}
|
|
38
|
+
}
|
package/components/sidebar.css
CHANGED
|
@@ -40,6 +40,8 @@
|
|
|
40
40
|
box-shadow: var(--shadow-lg);
|
|
41
41
|
/* LTR: slide in from the left */
|
|
42
42
|
translate: -100% 0;
|
|
43
|
+
/* keep off-canvas content out of tab order and the accessibility tree */
|
|
44
|
+
visibility: hidden;
|
|
43
45
|
}
|
|
44
46
|
|
|
45
47
|
/* RTL: overlay sidebar starts at inline-start (right edge) — slide in from the right */
|
|
@@ -49,6 +51,7 @@
|
|
|
49
51
|
|
|
50
52
|
.ct-sidebar--over[data-state='open'] {
|
|
51
53
|
translate: 0 0;
|
|
54
|
+
visibility: visible;
|
|
52
55
|
}
|
|
53
56
|
|
|
54
57
|
/* Push variant: slides off-screen and collapses flex space via negative margin */
|
|
@@ -129,7 +132,8 @@
|
|
|
129
132
|
font-size: var(--font-size-sm);
|
|
130
133
|
font-weight: var(--font-weight-medium);
|
|
131
134
|
transition: background var(--duration-fast) var(--easing-standard),
|
|
132
|
-
color var(--duration-fast) var(--easing-standard)
|
|
135
|
+
color var(--duration-fast) var(--easing-standard),
|
|
136
|
+
box-shadow var(--duration-fast) var(--easing-standard);
|
|
133
137
|
cursor: pointer;
|
|
134
138
|
}
|
|
135
139
|
|
|
@@ -151,7 +155,7 @@
|
|
|
151
155
|
color: var(--color-brand-primary);
|
|
152
156
|
font-weight: var(--font-weight-semibold);
|
|
153
157
|
/* The datum: an orange leading edge marks the current location. */
|
|
154
|
-
box-shadow: inset var(--
|
|
158
|
+
box-shadow: inset var(--component-datum-edge-width) 0 0 0 var(--color-brand-accent);
|
|
155
159
|
}
|
|
156
160
|
|
|
157
161
|
.ct-nav-item__icon {
|
|
@@ -183,3 +187,12 @@
|
|
|
183
187
|
background: var(--color-brand-accent);
|
|
184
188
|
color: var(--color-brand-on-accent);
|
|
185
189
|
}
|
|
190
|
+
|
|
191
|
+
@media (forced-colors: active) {
|
|
192
|
+
/* The datum edge is an inset box-shadow, which forced colors drops —
|
|
193
|
+
re-encode it as a border. */
|
|
194
|
+
.ct-nav-item--active,
|
|
195
|
+
.ct-nav-item[aria-current='page'] {
|
|
196
|
+
border-inline-start: var(--component-datum-edge-width) solid SelectedItem;
|
|
197
|
+
}
|
|
198
|
+
}
|
package/components/skip-link.css
CHANGED
|
@@ -12,7 +12,8 @@
|
|
|
12
12
|
z-index: var(--z-tooltip);
|
|
13
13
|
padding: var(--space-4) var(--space-6);
|
|
14
14
|
background: var(--color-brand-primary);
|
|
15
|
-
|
|
15
|
+
/* on-primary, not inverse: inverse is near-invisible on the light primary of the dark theme */
|
|
16
|
+
color: var(--color-text-on-primary);
|
|
16
17
|
font-family: var(--font-family-body);
|
|
17
18
|
font-size: var(--font-size-sm);
|
|
18
19
|
font-weight: var(--font-weight-semibold);
|
|
@@ -27,7 +28,10 @@
|
|
|
27
28
|
.ct-skip-link:focus,
|
|
28
29
|
.ct-skip-link:focus-visible {
|
|
29
30
|
transform: translateY(0);
|
|
30
|
-
|
|
31
|
+
/* on-primary, not the focus ring: the inset outline draws on the primary
|
|
32
|
+
background, and on-primary is the pairing validated against it in every
|
|
33
|
+
theme (the orange ring fails 3:1 on the dark theme's light primary). */
|
|
34
|
+
outline: 2px solid var(--color-text-on-primary);
|
|
31
35
|
outline-offset: -2px;
|
|
32
36
|
}
|
|
33
37
|
|
package/components/slider.css
CHANGED
|
@@ -23,6 +23,9 @@
|
|
|
23
23
|
appearance: none;
|
|
24
24
|
width: 100%;
|
|
25
25
|
height: var(--ct-slider-thumb-size);
|
|
26
|
+
/* WCAG 2.2 SC 2.5.8: small thumbs shrink the strip below 24px — keep
|
|
27
|
+
the pointer target at least that tall in fine-pointer contexts. */
|
|
28
|
+
min-height: 24px;
|
|
26
29
|
background: transparent;
|
|
27
30
|
cursor: pointer;
|
|
28
31
|
margin: 0;
|
|
@@ -199,6 +202,8 @@
|
|
|
199
202
|
position: relative;
|
|
200
203
|
flex: 1;
|
|
201
204
|
height: var(--ct-slider-thumb-size);
|
|
205
|
+
/* Same 24px pointer-target floor as the single-thumb input strip. */
|
|
206
|
+
min-height: 24px;
|
|
202
207
|
}
|
|
203
208
|
|
|
204
209
|
.ct-slider--range .ct-slider__track::after {
|
package/components/switch.css
CHANGED
|
@@ -54,6 +54,9 @@
|
|
|
54
54
|
.ct-switch__input:disabled {
|
|
55
55
|
background: var(--color-border-subtle);
|
|
56
56
|
cursor: not-allowed;
|
|
57
|
+
/* disabled is encoded via the muted track color; opting out of the global
|
|
58
|
+
:disabled opacity avoids double dimming (same pattern as the slider) */
|
|
59
|
+
opacity: 1;
|
|
57
60
|
}
|
|
58
61
|
|
|
59
62
|
.ct-switch__input:focus-visible {
|
|
@@ -61,12 +64,19 @@
|
|
|
61
64
|
outline-offset: 2px;
|
|
62
65
|
}
|
|
63
66
|
|
|
67
|
+
/* WCAG 2.2 SC 2.5.8: the small track is only 20px tall, so every pointer
|
|
68
|
+
context gets at least a 24px-tall hit area. */
|
|
69
|
+
.ct-switch__input::before {
|
|
70
|
+
content: '';
|
|
71
|
+
position: absolute;
|
|
72
|
+
inset-block: calc((var(--ct-switch-height) - 24px) / 2);
|
|
73
|
+
inset-inline: 0;
|
|
74
|
+
}
|
|
75
|
+
|
|
64
76
|
/* Construct 44×44 target policy: meets the size in WCAG 2.2 SC 2.5.5
|
|
65
77
|
(AAA) and exceeds the AA minimum in SC 2.5.8. */
|
|
66
78
|
@media (pointer: coarse) {
|
|
67
79
|
.ct-switch__input::before {
|
|
68
|
-
content: '';
|
|
69
|
-
position: absolute;
|
|
70
80
|
inset-block: calc((var(--ct-switch-height) - 44px) / 2);
|
|
71
81
|
inset-inline: calc((var(--ct-switch-width) - 44px) / 2);
|
|
72
82
|
}
|
|
@@ -81,3 +91,53 @@
|
|
|
81
91
|
--ct-switch-width: 52px;
|
|
82
92
|
--ct-switch-height: 28px;
|
|
83
93
|
}
|
|
94
|
+
|
|
95
|
+
@media (prefers-reduced-motion: reduce) {
|
|
96
|
+
.ct-switch__input,
|
|
97
|
+
.ct-switch__input::after {
|
|
98
|
+
transition: none;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
@media (forced-colors: active) {
|
|
103
|
+
/* Track and thumb are shadow/background-drawn; redraw both with
|
|
104
|
+
system-palette borders so on/off state survives without shadows. */
|
|
105
|
+
.ct-switch__input {
|
|
106
|
+
border: 1px solid ButtonText;
|
|
107
|
+
background: Canvas;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
.ct-switch__input::after {
|
|
111
|
+
border: 1px solid ButtonText;
|
|
112
|
+
background: Canvas;
|
|
113
|
+
box-shadow: none;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/* The checked fill must survive forced backgrounds; the extra hover
|
|
117
|
+
selector matches the specificity of the base hover rule. */
|
|
118
|
+
.ct-switch__input:checked,
|
|
119
|
+
.ct-switch__input:checked:not(:disabled):hover {
|
|
120
|
+
background: SelectedItem;
|
|
121
|
+
border-color: ButtonText;
|
|
122
|
+
forced-color-adjust: none;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
.ct-switch__input:checked::after {
|
|
126
|
+
background: SelectedItemText;
|
|
127
|
+
border-color: SelectedItemText;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
.ct-switch__input:disabled,
|
|
131
|
+
.ct-switch__input:disabled::after {
|
|
132
|
+
border-color: GrayText;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
.ct-switch__input:checked:disabled {
|
|
136
|
+
background: GrayText;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
.ct-switch__input:checked:disabled::after {
|
|
140
|
+
background: Canvas;
|
|
141
|
+
border-color: GrayText;
|
|
142
|
+
}
|
|
143
|
+
}
|
package/components/table.css
CHANGED
|
@@ -97,14 +97,18 @@
|
|
|
97
97
|
mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 5'%3E%3Cpath d='M4 5L0 0h8z'/%3E%3C/svg%3E");
|
|
98
98
|
}
|
|
99
99
|
|
|
100
|
-
th[aria-sort='ascending'] .ct-table__sort-indicator::before
|
|
100
|
+
th[aria-sort='ascending'] .ct-table__sort-indicator::before,
|
|
101
|
+
th[aria-sort='descending'] .ct-table__sort-indicator::after {
|
|
101
102
|
background-color: var(--color-text-secondary);
|
|
102
103
|
opacity: 1;
|
|
103
104
|
}
|
|
104
105
|
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
106
|
+
/* Direction is shape-encoded: the unsorted state keeps the dimmed
|
|
107
|
+
both-ways glyph, while a sorted column shows only its own arrow so the
|
|
108
|
+
state never relies on colour or opacity alone. */
|
|
109
|
+
th[aria-sort='ascending'] .ct-table__sort-indicator::after,
|
|
110
|
+
th[aria-sort='descending'] .ct-table__sort-indicator::before {
|
|
111
|
+
display: none;
|
|
108
112
|
}
|
|
109
113
|
|
|
110
114
|
.ct-table__cell--numeric .ct-table__sort {
|
|
@@ -137,3 +141,48 @@ th[aria-sort='descending'] .ct-table__sort-indicator::after {
|
|
|
137
141
|
font-size: var(--font-size-xs);
|
|
138
142
|
color: var(--color-text-muted);
|
|
139
143
|
}
|
|
144
|
+
|
|
145
|
+
/* ── Selected / current rows ──
|
|
146
|
+
The datum: an orange leading edge marks the selected or current row,
|
|
147
|
+
consistent with list, sidebar, and card active states. The edge sits
|
|
148
|
+
on the row's first cell — box-shadow on <tr> is unreliable with
|
|
149
|
+
collapsed borders. Row text stays neutral; only the edge is orange. */
|
|
150
|
+
.ct-table tbody tr > :first-child {
|
|
151
|
+
transition: box-shadow var(--duration-fast) var(--easing-standard);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
.ct-table tbody tr:is([aria-selected='true'], [aria-current='true'], [data-state='selected']) {
|
|
155
|
+
background: var(--color-bg-muted);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
.ct-table tbody tr:is([aria-selected='true'], [aria-current='true'], [data-state='selected']) > :first-child {
|
|
159
|
+
box-shadow: inset var(--component-datum-edge-width) 0 0 0 var(--color-brand-accent);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/* ── Sorted column ──
|
|
163
|
+
The datum underlines the actively sorted column header, like the tab
|
|
164
|
+
indicator. Direction stays shape-encoded via the arrow glyphs. */
|
|
165
|
+
.ct-table thead th:is([aria-sort='ascending'], [aria-sort='descending']) {
|
|
166
|
+
box-shadow: inset 0 calc(-1 * var(--component-datum-edge-width)) 0 0 var(--color-brand-accent);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
@media (forced-colors: active) {
|
|
170
|
+
/* Mask-drawn arrows paint via background-color; the sorted selectors
|
|
171
|
+
match the specificity of the full-strength rules above. */
|
|
172
|
+
.ct-table__sort-indicator::before,
|
|
173
|
+
.ct-table__sort-indicator::after,
|
|
174
|
+
th[aria-sort='ascending'] .ct-table__sort-indicator::before,
|
|
175
|
+
th[aria-sort='descending'] .ct-table__sort-indicator::after {
|
|
176
|
+
background-color: CanvasText;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/* The datum edges are inset box-shadows, which forced colors drops —
|
|
180
|
+
re-encode them as borders. */
|
|
181
|
+
.ct-table tbody tr:is([aria-selected='true'], [aria-current='true'], [data-state='selected']) > :first-child {
|
|
182
|
+
border-inline-start: var(--component-datum-edge-width) solid SelectedItem;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
.ct-table thead th:is([aria-sort='ascending'], [aria-sort='descending']) {
|
|
186
|
+
border-block-end: var(--component-datum-edge-width) solid SelectedItem;
|
|
187
|
+
}
|
|
188
|
+
}
|