@neuravision/construct 2.0.3 → 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.
Files changed (138) hide show
  1. package/CHANGELOG.md +318 -0
  2. package/README.md +158 -149
  3. package/behaviors/combobox.js +486 -0
  4. package/behaviors/drawer.js +18 -0
  5. package/behaviors/dropdown.js +310 -0
  6. package/behaviors/index.d.ts +335 -0
  7. package/behaviors/index.js +9 -0
  8. package/behaviors/internal/dialog-controller.js +421 -0
  9. package/behaviors/internal/dom.js +408 -0
  10. package/behaviors/internal/overlay.js +241 -0
  11. package/behaviors/internal/popup-layer.js +190 -0
  12. package/behaviors/modal.js +17 -0
  13. package/behaviors/popover.js +278 -0
  14. package/behaviors/select-menu.js +344 -0
  15. package/behaviors/tabs.js +241 -0
  16. package/behaviors/toggle-group.js +266 -0
  17. package/behaviors/tooltip.js +257 -0
  18. package/components/README.md +319 -83
  19. package/components/_shared.css +22 -10
  20. package/components/_shell-shared.css +20 -4
  21. package/components/accordion.css +8 -0
  22. package/components/app-shell-v2.css +28 -14
  23. package/components/app-shell.css +30 -14
  24. package/components/badge.css +15 -0
  25. package/components/banner.css +32 -1
  26. package/components/button.css +103 -15
  27. package/components/card.css +67 -8
  28. package/components/chart.css +26 -0
  29. package/components/checkbox.css +35 -2
  30. package/components/chip.css +30 -2
  31. package/components/combobox.css +21 -0
  32. package/components/datepicker.css +58 -9
  33. package/components/datum.css +277 -0
  34. package/components/dropdown.css +23 -3
  35. package/components/field.css +6 -4
  36. package/components/file-upload.css +1 -1
  37. package/components/index.css +5 -0
  38. package/components/list.css +28 -2
  39. package/components/meter.css +182 -0
  40. package/components/navbar.css +102 -4
  41. package/components/pagination.css +18 -0
  42. package/components/progress-bar.css +22 -1
  43. package/components/select-menu.css +8 -0
  44. package/components/select.css +8 -0
  45. package/components/sidebar.css +15 -2
  46. package/components/skip-link.css +6 -2
  47. package/components/slider.css +7 -1
  48. package/components/status.css +107 -0
  49. package/components/switch.css +66 -5
  50. package/components/table.css +53 -4
  51. package/components/tabs.css +55 -3
  52. package/components/toast.css +10 -1
  53. package/components/toggle-group.css +16 -5
  54. package/components/toolbar.css +17 -1
  55. package/components/tooltip.css +4 -0
  56. package/components/tree.css +30 -0
  57. package/docs/architecture.md +79 -0
  58. package/docs/behaviors.md +122 -0
  59. package/docs/fonts.md +70 -0
  60. package/docs/governance.md +85 -0
  61. package/docs/guidelines.md +72 -25
  62. package/examples/react/README.md +20 -0
  63. package/examples/react/SettingsModal.jsx +66 -0
  64. package/examples/react/construct.css +5 -0
  65. package/fonts.css +10 -0
  66. package/foundations.css +14 -1
  67. package/package.json +76 -21
  68. package/schemas/components.schema.json +122 -0
  69. package/schemas/examples/theme.example.json +27 -0
  70. package/schemas/primitives.schema.json +135 -0
  71. package/schemas/semantic.schema.json +224 -0
  72. package/schemas/theme.schema.json +63 -0
  73. package/scripts/build-tokens.mjs +1499 -0
  74. package/scripts/construct-theme.mjs +43 -0
  75. package/scripts/validate-tokens.mjs +25 -0
  76. package/standalone/accordion.css +3 -0
  77. package/standalone/alert.css +3 -0
  78. package/standalone/app-shell-v2.css +4 -0
  79. package/standalone/app-shell.css +4 -0
  80. package/standalone/avatar.css +3 -0
  81. package/standalone/badge.css +3 -0
  82. package/standalone/banner.css +3 -0
  83. package/standalone/breadcrumbs.css +3 -0
  84. package/standalone/button.css +3 -0
  85. package/standalone/card.css +3 -0
  86. package/standalone/chart.css +3 -0
  87. package/standalone/checkbox.css +3 -0
  88. package/standalone/chip.css +3 -0
  89. package/standalone/combobox.css +3 -0
  90. package/standalone/core.css +4 -0
  91. package/standalone/data-table.css +3 -0
  92. package/standalone/datepicker.css +3 -0
  93. package/standalone/datum.css +3 -0
  94. package/standalone/divider.css +3 -0
  95. package/standalone/drawer.css +3 -0
  96. package/standalone/dropdown.css +3 -0
  97. package/standalone/empty-state.css +3 -0
  98. package/standalone/field.css +3 -0
  99. package/standalone/file-upload.css +3 -0
  100. package/standalone/icon.css +3 -0
  101. package/standalone/input.css +3 -0
  102. package/standalone/list.css +3 -0
  103. package/standalone/meter.css +3 -0
  104. package/standalone/modal.css +3 -0
  105. package/standalone/navbar.css +3 -0
  106. package/standalone/pagination.css +3 -0
  107. package/standalone/popover.css +3 -0
  108. package/standalone/progress-bar.css +3 -0
  109. package/standalone/select-menu.css +3 -0
  110. package/standalone/select.css +3 -0
  111. package/standalone/sidebar.css +3 -0
  112. package/standalone/skeleton.css +3 -0
  113. package/standalone/skip-link.css +3 -0
  114. package/standalone/slider.css +3 -0
  115. package/standalone/spinner.css +3 -0
  116. package/standalone/status.css +3 -0
  117. package/standalone/switch.css +3 -0
  118. package/standalone/table.css +3 -0
  119. package/standalone/tabs.css +3 -0
  120. package/standalone/textarea.css +3 -0
  121. package/standalone/toast.css +3 -0
  122. package/standalone/toggle-group.css +3 -0
  123. package/standalone/toolbar.css +3 -0
  124. package/standalone/tooltip.css +3 -0
  125. package/standalone/tree.css +3 -0
  126. package/themes/README.md +37 -0
  127. package/themes/walnut.theme.json +106 -0
  128. package/tokens/README.md +135 -123
  129. package/tokens/components.json +64 -0
  130. package/tokens/primitives.json +268 -0
  131. package/tokens/semantic.dark.json +94 -0
  132. package/tokens/semantic.high-contrast-dark.json +94 -0
  133. package/tokens/semantic.high-contrast.json +94 -0
  134. package/tokens/semantic.light.json +179 -0
  135. package/tokens/tokens.css +960 -58
  136. package/tokens/tokens.js +729 -120
  137. package/tokens/tokens.json +1158 -116
  138. package/tokens/tokens.ts +729 -120
@@ -4,6 +4,9 @@
4
4
  @import './_shared.css';
5
5
  @import './_keyframes.css';
6
6
 
7
+ /* Structural grammar — the "datum" reference-line vocabulary */
8
+ @import './datum.css';
9
+
7
10
  /* Components */
8
11
  @import './button.css';
9
12
  @import './field.css';
@@ -33,6 +36,7 @@
33
36
  @import './skeleton.css';
34
37
  @import './icon.css';
35
38
  @import './progress-bar.css';
39
+ @import './meter.css';
36
40
  @import './toolbar.css';
37
41
  @import './sidebar.css';
38
42
  @import './toggle-group.css';
@@ -52,3 +56,4 @@
52
56
  @import './list.css';
53
57
  @import './tree.css';
54
58
  @import './chart.css';
59
+ @import './status.css';
@@ -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(--border-thick) 0 0 0 var(--color-brand-accent);
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
+ }
@@ -0,0 +1,182 @@
1
+ /* ── Segmented Meter / Stacked Progress ────────────────────────── */
2
+
3
+ .ct-meter {
4
+ --ct-meter-height: var(--component-meter-height);
5
+ --ct-meter-radius: var(--component-meter-radius);
6
+ --ct-meter-track: var(--component-meter-track);
7
+ --ct-meter-gap: var(--space-3);
8
+
9
+ display: grid;
10
+ gap: var(--ct-meter-gap);
11
+ width: 100%;
12
+ }
13
+
14
+ .ct-meter__header {
15
+ display: flex;
16
+ align-items: baseline;
17
+ justify-content: space-between;
18
+ gap: var(--space-3);
19
+ }
20
+
21
+ .ct-meter__label {
22
+ color: var(--color-text-primary);
23
+ font-size: var(--font-size-sm);
24
+ font-weight: var(--font-weight-semibold);
25
+ }
26
+
27
+ .ct-meter__summary {
28
+ color: var(--color-text-secondary);
29
+ font-size: var(--font-size-xs);
30
+ font-variant-numeric: tabular-nums lining-nums;
31
+ }
32
+
33
+ .ct-meter__track {
34
+ display: flex;
35
+ width: 100%;
36
+ height: var(--ct-meter-height);
37
+ overflow: hidden;
38
+ border: var(--border-thin) solid var(--color-border-subtle);
39
+ border-radius: var(--ct-meter-radius);
40
+ background: var(--ct-meter-track);
41
+ }
42
+
43
+ .ct-meter__segment {
44
+ --ct-meter-segment-color: var(--color-brand-primary);
45
+
46
+ flex: 0 0 var(--ct-meter-segment-size, auto);
47
+ min-width: 0;
48
+ height: 100%;
49
+ background: var(--ct-meter-segment-color);
50
+ transition: flex-basis var(--duration-normal) var(--easing-standard);
51
+ }
52
+
53
+ .ct-meter__segment[data-variant='accent'] {
54
+ --ct-meter-segment-color: var(--color-brand-accent);
55
+ }
56
+
57
+ .ct-meter__segment[data-variant='info'] {
58
+ --ct-meter-segment-color: var(--color-state-info);
59
+ }
60
+
61
+ .ct-meter__segment[data-variant='success'] {
62
+ --ct-meter-segment-color: var(--color-state-success);
63
+ }
64
+
65
+ .ct-meter__segment[data-variant='warning'] {
66
+ --ct-meter-segment-color: var(--color-state-warning);
67
+ }
68
+
69
+ .ct-meter__segment[data-variant='danger'] {
70
+ --ct-meter-segment-color: var(--color-state-danger);
71
+ }
72
+
73
+ .ct-meter__segment[data-variant='muted'] {
74
+ --ct-meter-segment-color: var(--color-border-strong);
75
+ }
76
+
77
+ .ct-meter__legend {
78
+ display: flex;
79
+ flex-wrap: wrap;
80
+ gap: var(--space-2) var(--space-5);
81
+ margin: 0;
82
+ padding: 0;
83
+ color: var(--color-text-secondary);
84
+ font-size: var(--font-size-xs);
85
+ list-style: none;
86
+ }
87
+
88
+ .ct-meter__legend-item {
89
+ display: inline-flex;
90
+ align-items: center;
91
+ gap: var(--space-2);
92
+ min-width: 0;
93
+ }
94
+
95
+ .ct-meter__marker {
96
+ --ct-meter-segment-color: var(--color-brand-primary);
97
+
98
+ width: var(--space-3);
99
+ height: var(--space-3);
100
+ flex: none;
101
+ border: var(--border-thin) solid currentColor;
102
+ border-radius: var(--radius-xs);
103
+ background: var(--ct-meter-segment-color);
104
+ color: var(--ct-meter-segment-color);
105
+ }
106
+
107
+ .ct-meter__marker[data-variant='accent'] {
108
+ --ct-meter-segment-color: var(--color-brand-accent);
109
+ }
110
+
111
+ .ct-meter__marker[data-variant='info'] {
112
+ --ct-meter-segment-color: var(--color-state-info);
113
+ }
114
+
115
+ .ct-meter__marker[data-variant='success'] {
116
+ --ct-meter-segment-color: var(--color-state-success);
117
+ }
118
+
119
+ .ct-meter__marker[data-variant='warning'] {
120
+ --ct-meter-segment-color: var(--color-state-warning);
121
+ }
122
+
123
+ .ct-meter__marker[data-variant='danger'] {
124
+ --ct-meter-segment-color: var(--color-state-danger);
125
+ }
126
+
127
+ .ct-meter__marker[data-variant='muted'] {
128
+ --ct-meter-segment-color: var(--color-border-strong);
129
+ }
130
+
131
+ .ct-meter__legend-value {
132
+ color: var(--color-text-primary);
133
+ font-weight: var(--font-weight-semibold);
134
+ font-variant-numeric: tabular-nums lining-nums;
135
+ }
136
+
137
+ .ct-meter__message {
138
+ margin: 0;
139
+ color: var(--color-text-secondary);
140
+ font-size: var(--font-size-xs);
141
+ }
142
+
143
+ .ct-meter:is([aria-busy='true'], [data-state='loading']) {
144
+ cursor: wait;
145
+ }
146
+
147
+ .ct-meter:is([aria-disabled='true'], [data-state='disabled']) {
148
+ opacity: var(--opacity-disabled);
149
+ }
150
+
151
+ .ct-meter[data-state='error'] .ct-meter__track {
152
+ border-color: var(--color-state-danger-border);
153
+ }
154
+
155
+ .ct-meter[data-state='error'] .ct-meter__message {
156
+ color: var(--color-state-danger-text);
157
+ }
158
+
159
+ .ct-meter--sm {
160
+ --ct-meter-height: var(--space-2);
161
+ }
162
+
163
+ .ct-meter--lg {
164
+ --ct-meter-height: var(--space-5);
165
+ }
166
+
167
+ @media (prefers-reduced-motion: reduce) {
168
+ .ct-meter__segment {
169
+ transition: none;
170
+ }
171
+ }
172
+
173
+ @media (forced-colors: active) {
174
+ .ct-meter__track,
175
+ .ct-meter__marker {
176
+ border-color: currentColor;
177
+ }
178
+
179
+ .ct-meter__segment + .ct-meter__segment {
180
+ border-inline-start: var(--border-medium) solid currentColor;
181
+ }
182
+ }
@@ -29,7 +29,10 @@
29
29
  display: flex;
30
30
  align-items: center;
31
31
  gap: var(--ct-navbar-gap);
32
- height: var(--ct-navbar-height);
32
+ width: 100%;
33
+ min-width: 0;
34
+ /* min-height, not height: the bar must grow with text scaling. */
35
+ min-height: var(--ct-navbar-height);
33
36
  padding: var(--ct-navbar-padding-y) var(--ct-navbar-padding-x);
34
37
  background: var(--ct-navbar-bg);
35
38
  border-bottom: var(--border-thin) solid var(--ct-navbar-border-color);
@@ -44,6 +47,7 @@
44
47
  display: flex;
45
48
  align-items: center;
46
49
  flex-shrink: 0;
50
+ min-width: 0;
47
51
  }
48
52
 
49
53
  .ct-navbar__brand {
@@ -56,6 +60,12 @@
56
60
  flex-shrink: 0;
57
61
  }
58
62
 
63
+ .ct-navbar__brand:focus-visible {
64
+ outline: var(--border-medium) solid var(--color-focus-ring);
65
+ outline-offset: var(--border-medium);
66
+ border-radius: var(--ct-navbar-item-radius);
67
+ }
68
+
59
69
  .ct-navbar__logo {
60
70
  height: var(--ct-navbar-brand-img-height);
61
71
  width: auto;
@@ -63,8 +73,11 @@
63
73
  }
64
74
 
65
75
  .ct-navbar__title {
76
+ min-width: 0;
66
77
  font-size: var(--font-size-md);
67
78
  font-weight: var(--font-weight-semibold);
79
+ overflow: hidden;
80
+ text-overflow: ellipsis;
68
81
  white-space: nowrap;
69
82
  }
70
83
 
@@ -345,6 +358,8 @@
345
358
  border-bottom: var(--border-thin) solid var(--ct-navbar-border-color);
346
359
  box-shadow: var(--shadow-md);
347
360
  padding: var(--space-4);
361
+ max-height: calc(100dvh - var(--ct-navbar-height));
362
+ overflow-y: auto;
348
363
  display: flex;
349
364
  flex-direction: column;
350
365
  gap: var(--space-1);
@@ -420,8 +435,36 @@
420
435
  font-size: var(--font-size-lg);
421
436
  }
422
437
 
438
+ /* ── Compact Product Header ────────────────────────────────────── */
439
+
440
+ .ct-navbar--compact {
441
+ --ct-navbar-height: var(--control-height-lg);
442
+ --ct-navbar-padding-x: var(--space-4);
443
+ --ct-navbar-padding-y: var(--space-2);
444
+ --ct-navbar-gap: var(--space-2);
445
+ --ct-navbar-actions-gap: var(--space-1);
446
+ --ct-navbar-brand-img-height: var(--space-8);
447
+ --ct-navbar-item-padding-x: var(--space-3);
448
+ --ct-navbar-item-padding-y: var(--space-2);
449
+ }
450
+
451
+ .ct-navbar--compact .ct-navbar__brand {
452
+ min-width: 0;
453
+ }
454
+
455
+ .ct-navbar--compact .ct-navbar__title {
456
+ max-width: 18ch;
457
+ font-size: var(--font-size-sm);
458
+ }
459
+
460
+ .ct-navbar--compact .ct-navbar__actions {
461
+ flex-shrink: 0;
462
+ }
463
+
423
464
  /* ── Sticky ────────────────────────────────────────────────────── */
424
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). */
425
468
  .ct-navbar--sticky {
426
469
  position: sticky;
427
470
  inset-block-start: 0;
@@ -429,6 +472,8 @@
429
472
 
430
473
  /* ── Fixed ─────────────────────────────────────────────────────── */
431
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). */
432
477
  .ct-navbar--fixed {
433
478
  position: fixed;
434
479
  inset-block-start: 0;
@@ -463,7 +508,7 @@
463
508
 
464
509
  @media (hover: hover) {
465
510
  .ct-navbar--transparent .ct-navbar__link:hover {
466
- background: rgb(255 255 255 / 0.15);
511
+ background: color-mix(in srgb, var(--color-text-inverse) 15%, transparent);
467
512
  color: var(--color-text-inverse);
468
513
  }
469
514
  }
@@ -486,7 +531,7 @@
486
531
 
487
532
  @media (hover: hover) {
488
533
  .ct-navbar--transparent .ct-navbar__toggle:hover {
489
- background: rgb(255 255 255 / 0.15);
534
+ background: color-mix(in srgb, var(--color-text-inverse) 15%, transparent);
490
535
  }
491
536
  }
492
537
 
@@ -590,15 +635,57 @@
590
635
  position: relative;
591
636
  }
592
637
 
593
- .ct-navbar__nav {
638
+ .ct-navbar:has(.ct-navbar__toggle) .ct-navbar__nav {
594
639
  display: none;
595
640
  }
596
641
 
642
+ /* Progressive fallback: when no mobile toggle/menu is supplied, keep
643
+ primary navigation reachable as a horizontally scrollable row. */
644
+ .ct-navbar:not(:has(.ct-navbar__toggle)) nav {
645
+ min-width: 0;
646
+ overflow-x: auto;
647
+ scrollbar-width: thin;
648
+ }
649
+
650
+ .ct-navbar:not(:has(.ct-navbar__toggle)) .ct-navbar__nav {
651
+ display: flex;
652
+ width: max-content;
653
+ }
654
+
597
655
  .ct-navbar__toggle {
598
656
  display: inline-flex;
599
657
  }
600
658
  }
601
659
 
660
+ @media (max-width: 599px) { /* < sm breakpoint (600px) */
661
+ .ct-navbar,
662
+ .ct-navbar--compact {
663
+ --ct-navbar-padding-x: var(--space-3);
664
+ --ct-navbar-gap: var(--space-1);
665
+ --ct-navbar-actions-gap: var(--space-1);
666
+ }
667
+
668
+ .ct-navbar--compact .ct-navbar__title {
669
+ max-width: 10ch;
670
+ }
671
+
672
+ .ct-navbar--compact .ct-navbar__actions [data-priority='secondary'] {
673
+ display: none;
674
+ }
675
+
676
+ .ct-navbar__mobile-menu {
677
+ padding: var(--space-3);
678
+ }
679
+ }
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
+
602
689
  /* ── Reduced Motion ────────────────────────────────────────────── */
603
690
 
604
691
  @media (prefers-reduced-motion: reduce) {
@@ -612,3 +699,14 @@
612
699
  transition: none;
613
700
  }
614
701
  }
702
+
703
+ @media (forced-colors: active) {
704
+ .ct-navbar {
705
+ border-color: currentColor;
706
+ }
707
+
708
+ .ct-navbar__link[aria-current='page'] {
709
+ outline: var(--border-medium) solid currentColor;
710
+ outline-offset: calc(-1 * var(--border-medium));
711
+ }
712
+ }
@@ -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
- --ct-progress-fill: var(--color-brand-accent);
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) {
@@ -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
+ }
@@ -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(--border-thick) 0 0 0 var(--color-brand-accent);
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
+ }
@@ -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
- color: var(--color-text-inverse);
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
- outline: 2px solid var(--color-focus-ring);
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
 
@@ -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 {
@@ -269,7 +274,8 @@
269
274
  opacity: 1;
270
275
  }
271
276
 
272
- /* Touch-target minimums (WCAG 2.5.8: 44×44px minimum) */
277
+ /* Construct 44×44 target policy: meets the size in WCAG 2.2 SC 2.5.5
278
+ (AAA) and exceeds the AA minimum in SC 2.5.8. */
273
279
 
274
280
  @media (pointer: coarse) {
275
281
  .ct-slider {