@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
@@ -1,13 +1,13 @@
1
1
  :root {
2
- --ct-control-bg: var(--color-bg-elevated);
3
- --ct-control-bg-hover: var(--color-bg-surface);
4
- --ct-control-bg-disabled: var(--color-bg-muted);
5
- --ct-control-border: var(--color-border-default);
6
- --ct-control-border-hover: var(--color-border-strong);
7
- --ct-control-border-focus: var(--color-focus-ring);
8
- --ct-control-text: var(--color-text-primary);
9
- --ct-control-placeholder: var(--color-text-muted);
10
- --ct-control-radius: var(--radius-control);
2
+ --ct-control-bg: var(--component-control-background);
3
+ --ct-control-bg-hover: var(--component-control-background-hover);
4
+ --ct-control-bg-disabled: var(--component-control-background-disabled);
5
+ --ct-control-border: var(--component-control-border);
6
+ --ct-control-border-hover: var(--component-control-border-hover);
7
+ --ct-control-border-focus: var(--component-control-border-focus);
8
+ --ct-control-text: var(--component-control-text);
9
+ --ct-control-placeholder: var(--component-control-placeholder);
10
+ --ct-control-radius: var(--component-control-radius);
11
11
  --ct-control-transition: border-color var(--duration-fast) var(--easing-standard),
12
12
  box-shadow var(--duration-fast) var(--easing-standard),
13
13
  background var(--duration-fast) var(--easing-standard),
@@ -93,6 +93,9 @@
93
93
  background: var(--ct-control-bg-disabled);
94
94
  border-color: var(--color-border-subtle);
95
95
  color: var(--color-text-muted);
96
+ /* Disabled is fully colour-encoded above; cancel foundations' global
97
+ :disabled opacity so controls are not double-dimmed. */
98
+ opacity: 1;
96
99
  }
97
100
 
98
101
  :where(.ct-input, .ct-select, .ct-textarea, .ct-control)[aria-invalid='true'],
@@ -106,11 +109,12 @@
106
109
  outline-color: var(--color-state-danger);
107
110
  }
108
111
 
109
- /* ── Touch-target minimums (WCAG 2.5.8: 44×44px) ────────────────── */
112
+ /* ── Construct 44×44 target policy (WCAG 2.2 SC 2.5.5 is AAA) ─── */
110
113
 
111
114
  @media (pointer: coarse) {
112
115
  :root {
113
116
  --control-height-sm: 44px;
117
+ --control-height-md: 44px;
114
118
  --control-height-calendar-day: 44px;
115
119
  }
116
120
  }
@@ -134,3 +138,11 @@
134
138
  --ct-control-font-size: var(--font-size-lg);
135
139
  --ct-control-line-height: var(--line-height-lg);
136
140
  }
141
+
142
+ /* Per-file kill switch: standalone/core.css imports this file without
143
+ foundations.css and its global reduced-motion rule. */
144
+ @media (prefers-reduced-motion: reduce) {
145
+ :where(.ct-input, .ct-select, .ct-textarea, .ct-control) {
146
+ transition: none;
147
+ }
148
+ }
@@ -3,6 +3,20 @@
3
3
  /* Crosses component boundary: shells orchestrate child layout. */
4
4
  /* ─────────────────────────────────────────────────────────────── */
5
5
 
6
+ /* Both layouts expose the canonical `--ct-shell-*` sizing and motion
7
+ controls. Their historical variable names are compatibility aliases. */
8
+
9
+ :is(.ct-app-shell, .ct-app-shell-v2) {
10
+ min-width: 0;
11
+ }
12
+
13
+ :is(.ct-app-shell__main, .ct-app-shell-v2__main),
14
+ :is(.ct-app-shell__sidebar, .ct-app-shell-v2__sidebar),
15
+ :is(.ct-app-shell__panel, .ct-app-shell-v2__panel) {
16
+ min-width: 0;
17
+ min-height: 0;
18
+ }
19
+
6
20
 
7
21
  /* ── Collapsed: Hide Labels & Badges ── */
8
22
 
@@ -16,8 +30,8 @@
16
30
  white-space: nowrap;
17
31
  /* Collapsing: labels fade out first, then width snaps */
18
32
  transition:
19
- opacity 120ms ease-in-out,
20
- width 0ms linear 120ms;
33
+ opacity var(--ct-shell-motion-duration) var(--ct-shell-motion-easing),
34
+ width 0ms linear var(--ct-shell-motion-duration);
21
35
  }
22
36
 
23
37
 
@@ -30,7 +44,9 @@
30
44
  opacity: 1;
31
45
  width: auto;
32
46
  /* Expanding: labels fade in after sidebar width finishes growing */
33
- transition: opacity 120ms ease-in-out 250ms;
47
+ transition:
48
+ opacity var(--ct-shell-motion-duration) var(--ct-shell-motion-easing)
49
+ var(--ct-shell-motion-duration);
34
50
  }
35
51
 
36
52
 
@@ -44,7 +60,7 @@
44
60
 
45
61
  /* ── Tablet: Auto-Collapse to Rail ── */
46
62
 
47
- @media (max-width: 1199px) {
63
+ @media (max-width: 1199px) { /* < lg breakpoint (1200px) */
48
64
  :is(.ct-app-shell, .ct-app-shell-v2):not([data-sidebar-state]) .ct-nav-item__label,
49
65
  :is(.ct-app-shell, .ct-app-shell-v2):not([data-sidebar-state]) .ct-nav-item__badge,
50
66
  :is(.ct-app-shell, .ct-app-shell-v2):not([data-sidebar-state]) .ct-sidebar__header span,
@@ -116,6 +116,14 @@
116
116
  padding: 0 var(--space-4) var(--space-4);
117
117
  }
118
118
 
119
+ /* ── Construct 44×44 target policy (WCAG 2.2 SC 2.5.5 is AAA) ── */
120
+
121
+ @media (pointer: coarse) {
122
+ .ct-accordion__trigger {
123
+ min-block-size: 44px;
124
+ }
125
+ }
126
+
119
127
  @media (prefers-reduced-motion: reduce) {
120
128
  .ct-accordion__icon,
121
129
  .ct-accordion__item::details-content {
@@ -17,24 +17,31 @@
17
17
  /* │ ╰────────╯ ╰───────────────────┴──────────────╯ │ */
18
18
  /* ╰─────────────────────────────────────────────────────╯ */
19
19
  /* */
20
- /* Shell breakpoints: */
21
- /* Mobile: <768px */
22
- /* Tablet: 768–1199px */
20
+ /* Shell breakpoints (from the public primitive scale): */
21
+ /* Compact: <900px (< md) */
22
+ /* Tablet: 900–1199px */
23
23
  /* Desktop: >=1200px */
24
24
  /* ─────────────────────────────────────────────────────────────── */
25
25
 
26
26
  .ct-app-shell-v2 {
27
- /* ── Public Tokens ── */
28
- --ct-v2-sidebar-width: 260px;
29
- --ct-v2-sidebar-rail-width: 64px;
30
- --ct-v2-panel-width: 340px;
27
+ /* ── Shared Public Tokens ── */
28
+ --ct-shell-sidebar-width: 260px;
29
+ --ct-shell-sidebar-rail-width: 64px;
30
+ --ct-shell-panel-width: 340px;
31
+ --ct-shell-motion-duration: 250ms;
32
+ --ct-shell-motion-easing: cubic-bezier(0.32, 0.72, 0, 1);
33
+
34
+ /* V2 aliases remain public for backward compatibility. */
35
+ --ct-v2-sidebar-width: var(--ct-shell-sidebar-width);
36
+ --ct-v2-sidebar-rail-width: var(--ct-shell-sidebar-rail-width);
37
+ --ct-v2-panel-width: var(--ct-shell-panel-width);
31
38
  --ct-v2-gap: var(--space-4);
32
39
  --ct-v2-radius: var(--radius-lg);
33
40
  --ct-v2-canvas-bg: var(--color-bg-muted);
34
41
  --ct-v2-surface-bg: var(--color-bg-canvas);
35
42
  --ct-v2-surface-shadow: var(--shadow-xs);
36
- --ct-v2-transition-duration: 250ms;
37
- --ct-v2-transition-easing: cubic-bezier(0.32, 0.72, 0, 1);
43
+ --ct-v2-transition-duration: var(--ct-shell-motion-duration);
44
+ --ct-v2-transition-easing: var(--ct-shell-motion-easing);
38
45
 
39
46
  /* ── Internal ── */
40
47
  --_v2-sidebar-w: var(--ct-v2-sidebar-width);
@@ -163,6 +170,13 @@
163
170
  background: var(--ct-v2-surface-bg);
164
171
  }
165
172
 
173
+ /* The shell owns its scroll container, so it can satisfy WCAG 2.4.11
174
+ itself: scrolled-to content clears the sticky toolbar. Override
175
+ --ct-shell-scroll-offset when the toolbar height is customized. */
176
+ .ct-app-shell-v2__main:has(.ct-app-shell-v2__toolbar--sticky) {
177
+ scroll-padding-block-start: var(--ct-shell-scroll-offset, 64px);
178
+ }
179
+
166
180
 
167
181
  /* ── Sidebar States ── */
168
182
 
@@ -362,13 +376,13 @@
362
376
 
363
377
  /* Delay grid shrink on collapse so labels fade out first */
364
378
  .ct-app-shell-v2[data-sidebar-state='collapsed'] {
365
- transition-delay: 120ms;
379
+ transition-delay: var(--ct-shell-motion-duration);
366
380
  }
367
381
 
368
382
 
369
- /* ── Responsive: Tablet (768–1199px) ── */
383
+ /* ── Responsive: Tablet (900–1199px) ── */
370
384
 
371
- @media (max-width: 1199px) {
385
+ @media (max-width: 1199px) { /* < lg breakpoint (1200px) */
372
386
  /* Auto-collapse sidebar to rail when no explicit state */
373
387
  .ct-app-shell-v2:not([data-sidebar-state]) {
374
388
  --_v2-sidebar-w: var(--ct-v2-sidebar-rail-width);
@@ -390,9 +404,9 @@
390
404
  }
391
405
 
392
406
 
393
- /* ── Responsive: Mobile (<768px) ── */
407
+ /* ── Responsive: Compact (< md breakpoint, 900px) ── */
394
408
 
395
- @media (max-width: 767px) {
409
+ @media (max-width: 899px) { /* < md breakpoint (900px) */
396
410
  .ct-app-shell-v2 {
397
411
  --ct-v2-gap: var(--space-3);
398
412
  }
@@ -1,23 +1,32 @@
1
- /* ── App Shell ─────────────────────────────────────────────────── */
1
+ /* ── App Shell (compatibility layout) ─────────────────────────── */
2
2
  /* CSS Grid layout shell that orchestrates Navbar, Sidebar, */
3
3
  /* Main content, optional Panel, and optional Footer. */
4
+ /* New applications should prefer App Shell V2. `.ct-app-shell` */
5
+ /* remains supported as the non-floating compatibility contract. */
4
6
  /* */
5
- /* Shell breakpoints (component-specific, differ from --bp-*): */
6
- /* Mobile: <768px */
7
- /* Tablet: 768–1199px */
7
+ /* Shell breakpoints (from the public primitive scale): */
8
+ /* Compact: <900px (< md) */
9
+ /* Tablet: 900–1199px */
8
10
  /* Desktop: 1200–1439px */
9
11
  /* Wide: >=1440px */
10
12
  /* ────────────────────────────────────────────────────────────── */
11
13
 
12
14
  .ct-app-shell {
13
- /* ── Public Tokens ── */
14
- --ct-sidebar-width: 260px;
15
- --ct-sidebar-rail-width: 56px;
16
- --ct-panel-width: 320px;
15
+ /* ── Shared Public Tokens ── */
16
+ --ct-shell-sidebar-width: 260px;
17
+ --ct-shell-sidebar-rail-width: 56px;
18
+ --ct-shell-panel-width: 320px;
19
+ --ct-shell-motion-duration: 200ms;
20
+ --ct-shell-motion-easing: ease-out;
21
+
22
+ /* Legacy aliases remain public for backward compatibility. */
23
+ --ct-sidebar-width: var(--ct-shell-sidebar-width);
24
+ --ct-sidebar-rail-width: var(--ct-shell-sidebar-rail-width);
25
+ --ct-panel-width: var(--ct-shell-panel-width);
17
26
  --ct-header-height: auto;
18
27
  --ct-footer-height: auto;
19
- --ct-shell-transition-duration: 200ms;
20
- --ct-shell-transition-easing: ease-out;
28
+ --ct-shell-transition-duration: var(--ct-shell-motion-duration);
29
+ --ct-shell-transition-easing: var(--ct-shell-motion-easing);
21
30
 
22
31
  /* ── Internal Variables ── */
23
32
  --_sidebar-width: var(--ct-sidebar-width);
@@ -221,6 +230,13 @@
221
230
  background: var(--color-bg-canvas);
222
231
  }
223
232
 
233
+ /* The shell owns its scroll container, so it can satisfy WCAG 2.4.11
234
+ itself: scrolled-to content clears the sticky page header. Override
235
+ --ct-shell-scroll-offset when the header height is customized. */
236
+ .ct-app-shell__main:has(.ct-app-shell__page-header--sticky) {
237
+ scroll-padding-block-start: var(--ct-shell-scroll-offset, 64px);
238
+ }
239
+
224
240
 
225
241
 
226
242
  /* ── Backdrop (Mobile Sidebar Overlay) ── */
@@ -249,7 +265,7 @@
249
265
 
250
266
  /* Collapsing: delay grid shrink so labels fade out first */
251
267
  .ct-app-shell[data-sidebar-state='collapsed'] {
252
- transition-delay: 120ms;
268
+ transition-delay: var(--ct-shell-motion-duration);
253
269
  }
254
270
 
255
271
 
@@ -260,7 +276,7 @@
260
276
  }
261
277
 
262
278
 
263
- /* ── Responsive: Tablet (768–1199px) ── */
279
+ /* ── Responsive: Tablet (900–1199px) ── */
264
280
 
265
281
  @media (max-width: 1199px) { /* < lg breakpoint (1200px) */
266
282
  /* Auto-collapse sidebar to rail when no explicit state is set */
@@ -286,9 +302,9 @@
286
302
  }
287
303
 
288
304
 
289
- /* ── Responsive: Mobile (<768px) ── */
305
+ /* ── Responsive: Compact (< md breakpoint, 900px) ── */
290
306
 
291
- @media (max-width: 767px) { /* < tablet breakpoint (768px) */
307
+ @media (max-width: 899px) { /* < md breakpoint (900px) */
292
308
  /* Auto-hide sidebar when no explicit state is set */
293
309
  .ct-app-shell:not([data-sidebar-state]) {
294
310
  --_sidebar-width: 0px;
@@ -38,6 +38,21 @@
38
38
  background: var(--ct-badge-accent);
39
39
  }
40
40
 
41
+ /* Explicit neutral alias for generated/conditional variant class lists. */
42
+ .ct-badge--neutral {
43
+ --ct-badge-bg: var(--color-bg-muted);
44
+ --ct-badge-border: var(--color-border-subtle);
45
+ --ct-badge-color: var(--color-text-secondary);
46
+ --ct-badge-accent: var(--ct-badge-color);
47
+ }
48
+
49
+ .ct-badge--sm {
50
+ gap: var(--space-1);
51
+ padding: 0 var(--space-2);
52
+ font-size: var(--font-size-2xs);
53
+ line-height: var(--line-height-xs);
54
+ }
55
+
41
56
  .ct-badge--info {
42
57
  --ct-badge-bg: var(--color-state-info-surface);
43
58
  --ct-badge-border: var(--color-state-info-border);
@@ -26,7 +26,8 @@
26
26
  font-size: var(--font-size-sm);
27
27
  line-height: var(--line-height-sm);
28
28
  transition: opacity var(--duration-normal) var(--easing-standard),
29
- transform var(--duration-normal) var(--easing-standard);
29
+ transform var(--duration-normal) var(--easing-standard),
30
+ visibility 0s linear 0s;
30
31
  }
31
32
 
32
33
  /* ---- Sub-elements ---- */
@@ -159,6 +160,8 @@
159
160
 
160
161
  /* ---- Positions ---- */
161
162
 
163
+ /* Consumers: set --ct-scroll-offset-top on :root (see foundations.css) so
164
+ scrolled-to content is not obscured behind the banner (WCAG 2.4.11). */
162
165
  .ct-banner--fixed-top {
163
166
  position: fixed;
164
167
  inset-block-start: 0;
@@ -167,6 +170,8 @@
167
170
  border-radius: 0;
168
171
  }
169
172
 
173
+ /* Consumers: set --ct-scroll-offset-bottom on :root (see foundations.css) so
174
+ scrolled-to content is not obscured behind the banner (WCAG 2.4.11). */
170
175
  .ct-banner--fixed-bottom {
171
176
  position: fixed;
172
177
  inset-block-end: 0;
@@ -175,6 +180,8 @@
175
180
  border-radius: 0;
176
181
  }
177
182
 
183
+ /* Consumers: set --ct-scroll-offset-top on :root (see foundations.css) so
184
+ scrolled-to content is not obscured behind the banner (WCAG 2.4.11). */
178
185
  .ct-banner--sticky {
179
186
  position: sticky;
180
187
  inset-block-start: 0;
@@ -211,9 +218,16 @@
211
218
 
212
219
  /* ---- State ---- */
213
220
 
221
+ /* Delayed visibility (drawer.css pattern) removes the faded-out banner
222
+ from the accessibility tree and tab order once the fade completes;
223
+ reopening restores it instantly via the base transition. */
214
224
  .ct-banner[data-state='closed'] {
215
225
  opacity: 0;
226
+ visibility: hidden;
216
227
  pointer-events: none;
228
+ transition: opacity var(--duration-normal) var(--easing-standard),
229
+ transform var(--duration-normal) var(--easing-standard),
230
+ visibility 0s linear var(--duration-normal);
217
231
  }
218
232
 
219
233
  .ct-banner--fixed-top[data-state='closed'],
@@ -237,6 +251,23 @@
237
251
  }
238
252
  }
239
253
 
254
+ /* ---- Construct 44×44 target policy (WCAG 2.2 SC 2.5.5 is AAA) ---- */
255
+
256
+ @media (pointer: coarse) {
257
+ /* The default close is --control-height-sm, which _shared.css remaps to
258
+ 44px; only the fixed 24px compact close needs the checkbox.css-style
259
+ hit area. */
260
+ .ct-banner--compact .ct-banner__close {
261
+ position: relative;
262
+ }
263
+
264
+ .ct-banner--compact .ct-banner__close::before {
265
+ content: '';
266
+ position: absolute;
267
+ inset: calc((var(--icon-lg) - 44px) / 2);
268
+ }
269
+ }
270
+
240
271
  /* ---- Reduced Motion ---- */
241
272
 
242
273
  @media (prefers-reduced-motion: reduce) {
@@ -1,14 +1,19 @@
1
1
  .ct-button {
2
- --ct-button-bg: var(--color-brand-primary);
3
- --ct-button-bg-hover: var(--color-brand-primary-hover);
4
- --ct-button-bg-active: var(--color-brand-primary-active);
5
- --ct-button-border: transparent;
6
- --ct-button-color: var(--color-text-on-primary);
7
- --ct-button-height: var(--control-height-md);
8
- --ct-button-padding-x: var(--space-6);
9
- --ct-button-font-size: var(--font-size-sm);
10
- --ct-button-line-height: var(--line-height-sm);
11
-
2
+ --ct-button-bg: var(--component-button-background);
3
+ --ct-button-bg-hover: var(--component-button-background-hover);
4
+ --ct-button-bg-active: var(--component-button-background-active);
5
+ --ct-button-border: var(--component-button-border);
6
+ --ct-button-color: var(--component-button-color);
7
+ --ct-button-height: var(--component-button-height);
8
+ --ct-button-padding-x: var(--component-button-padding-x);
9
+ --ct-button-font-size: var(--component-button-font-size);
10
+ --ct-button-line-height: var(--component-button-line-height);
11
+ --ct-button-font-weight: var(--component-button-font-weight);
12
+ --ct-button-radius: var(--component-button-radius);
13
+ --ct-button-gap: var(--component-button-gap);
14
+ --ct-button-loading-spinner-size: var(--component-button-loading-spinner-size);
15
+
16
+ position: relative;
12
17
  display: inline-flex;
13
18
  align-items: center;
14
19
  justify-content: center;
@@ -32,12 +37,12 @@
32
37
  }
33
38
 
34
39
  @media (hover: hover) {
35
- .ct-button:hover {
40
+ .ct-button:not(:disabled):not([aria-disabled='true']):not([aria-busy='true']):not([data-loading='true']):hover {
36
41
  background: var(--ct-button-bg-hover);
37
42
  }
38
43
  }
39
44
 
40
- .ct-button:active {
45
+ .ct-button:not(:disabled):not([aria-disabled='true']):not([aria-busy='true']):not([data-loading='true']):active {
41
46
  background: var(--ct-button-bg-active);
42
47
  transform: translateY(1px);
43
48
  }
@@ -52,6 +57,56 @@
52
57
  opacity: var(--opacity-disabled);
53
58
  transform: none;
54
59
  cursor: not-allowed;
60
+ pointer-events: none;
61
+ }
62
+
63
+ /* Loading contract:
64
+ - set `aria-busy="true"` and `data-loading="true"`;
65
+ - native buttons must also use `disabled` to block keyboard activation;
66
+ - wrap the stable accessible label in `.ct-button__content`;
67
+ - keep `.ct-button__loader` decorative (`aria-hidden="true"`). */
68
+ .ct-button__content {
69
+ display: inline-flex;
70
+ align-items: center;
71
+ justify-content: center;
72
+ gap: inherit;
73
+ opacity: 1;
74
+ transition: opacity var(--duration-fast) var(--easing-standard);
75
+ }
76
+
77
+ .ct-button__loader {
78
+ position: absolute;
79
+ inset: 0;
80
+ display: grid;
81
+ place-items: center;
82
+ opacity: 0;
83
+ pointer-events: none;
84
+ transition: opacity var(--duration-fast) var(--easing-standard);
85
+ }
86
+
87
+ .ct-button__loader::before {
88
+ content: '';
89
+ width: var(--ct-button-loading-spinner-size);
90
+ height: var(--ct-button-loading-spinner-size);
91
+ border: var(--border-medium) solid currentColor;
92
+ border-inline-end-color: transparent;
93
+ border-radius: var(--radius-round);
94
+ animation: ct-spin var(--ct-spinner-duration) linear infinite;
95
+ }
96
+
97
+ .ct-button:is([aria-busy='true'], [data-loading='true']) {
98
+ opacity: 1;
99
+ transform: none;
100
+ cursor: wait;
101
+ pointer-events: none;
102
+ }
103
+
104
+ .ct-button:is([aria-busy='true'], [data-loading='true']) .ct-button__content {
105
+ opacity: 0;
106
+ }
107
+
108
+ .ct-button:is([aria-busy='true'], [data-loading='true']) .ct-button__loader {
109
+ opacity: 1;
55
110
  }
56
111
 
57
112
  .ct-button--secondary {
@@ -88,8 +143,8 @@
88
143
 
89
144
  .ct-button--danger {
90
145
  --ct-button-bg: var(--color-state-danger);
91
- --ct-button-bg-hover: color-mix(in srgb, var(--color-state-danger) 85%, black);
92
- --ct-button-bg-active: color-mix(in srgb, var(--color-state-danger) 70%, black);
146
+ --ct-button-bg-hover: color-mix(in srgb, var(--color-state-danger) 85%, var(--color-state-danger-text));
147
+ --ct-button-bg-active: color-mix(in srgb, var(--color-state-danger) 70%, var(--color-state-danger-text));
93
148
  --ct-button-border: transparent;
94
149
  --ct-button-color: var(--color-state-on-solid);
95
150
  }
@@ -138,8 +193,41 @@
138
193
  justify-content: center;
139
194
  }
140
195
 
196
+ @media (pointer: coarse) {
197
+ .ct-button {
198
+ min-width: var(--control-height-sm);
199
+ min-height: var(--control-height-sm);
200
+ }
201
+
202
+ .ct-button--link {
203
+ min-height: var(--control-height-sm);
204
+ padding-inline: var(--space-2);
205
+ }
206
+ }
207
+
141
208
  @media (prefers-reduced-motion: reduce) {
142
- .ct-button:active {
209
+ .ct-button,
210
+ .ct-button__content,
211
+ .ct-button__loader {
212
+ transition: none;
213
+ }
214
+
215
+ .ct-button:not(:disabled):not([aria-disabled='true']):not([aria-busy='true']):not([data-loading='true']):active {
143
216
  transform: none;
144
217
  }
218
+
219
+ .ct-button__loader::before {
220
+ animation: none;
221
+ }
222
+ }
223
+
224
+ @media (forced-colors: active) {
225
+ .ct-button {
226
+ border-color: currentColor;
227
+ }
228
+
229
+ .ct-button__loader::before {
230
+ border-color: currentColor;
231
+ border-inline-end-color: transparent;
232
+ }
145
233
  }
@@ -1,10 +1,18 @@
1
1
  .ct-card {
2
- --ct-card-padding: var(--space-6);
3
- background: var(--color-bg-elevated);
4
- border: var(--border-thin) solid var(--color-border-subtle);
5
- border-radius: var(--radius-lg);
2
+ --ct-card-bg: var(--component-card-background);
3
+ --ct-card-border: var(--component-card-border);
4
+ --ct-card-radius: var(--component-card-radius);
5
+ --ct-card-padding: var(--component-card-padding);
6
+ --ct-card-shadow: var(--component-card-shadow);
7
+ --ct-card-shadow-hover: var(--component-card-shadow-hover);
8
+
9
+ background: var(--ct-card-bg);
10
+ border: var(--border-thin) solid var(--ct-card-border);
11
+ border-radius: var(--ct-card-radius);
6
12
  padding: var(--ct-card-padding);
7
- box-shadow: var(--shadow-sm);
13
+ box-shadow: var(--ct-card-shadow);
14
+ /* The datum edge slides in on activation instead of popping. */
15
+ transition: box-shadow var(--duration-fast) var(--easing-standard);
8
16
  }
9
17
 
10
18
  .ct-card--sm {
@@ -47,7 +55,7 @@
47
55
  @media (hover: hover) {
48
56
  :is(a, button, [role='button'], [role='link']).ct-card--interactive:hover {
49
57
  border-color: var(--color-border-strong);
50
- box-shadow: var(--shadow-md);
58
+ box-shadow: var(--ct-card-shadow-hover);
51
59
  }
52
60
  }
53
61
 
@@ -55,7 +63,7 @@
55
63
  outline: 2px solid var(--color-focus-ring);
56
64
  outline-offset: 2px;
57
65
  border-color: var(--color-border-strong);
58
- box-shadow: var(--shadow-md);
66
+ box-shadow: var(--ct-card-shadow-hover);
59
67
  }
60
68
 
61
69
  .ct-card--interactive[aria-selected='true'] {
@@ -64,9 +72,20 @@
64
72
  }
65
73
 
66
74
  .ct-card--interactive[aria-disabled='true'],
67
- .ct-card--interactive:disabled {
75
+ .ct-card--interactive:disabled,
76
+ .ct-card--interactive[data-state='disabled'] {
68
77
  opacity: var(--opacity-disabled);
69
78
  cursor: not-allowed;
79
+ pointer-events: none;
80
+ }
81
+
82
+ .ct-card[aria-busy='true'],
83
+ .ct-card[data-state='loading'] {
84
+ cursor: wait;
85
+ }
86
+
87
+ .ct-card[data-state='error'] {
88
+ border-color: var(--color-state-danger-border);
70
89
  }
71
90
 
72
91
  @media (prefers-reduced-motion: reduce) {
@@ -74,3 +93,43 @@
74
93
  transition: none;
75
94
  }
76
95
  }
96
+
97
+ /* ── Datum ──
98
+ The reference-line grammar applied to cards. `--datum` adds a corner
99
+ registration mark (like an engineering drawing); the active/current
100
+ state carries the same orange leading edge as list and sidebar items.
101
+ Both are opt-in and additive — they don't change default cards. */
102
+ .ct-card--datum {
103
+ position: relative;
104
+ }
105
+
106
+ .ct-card--datum::before {
107
+ content: '';
108
+ position: absolute;
109
+ inset-block-start: calc(-1 * var(--border-thin));
110
+ inset-inline-start: calc(-1 * var(--border-thin));
111
+ width: var(--component-datum-mark-size);
112
+ height: var(--component-datum-mark-size);
113
+ border-block-start: var(--border-medium) solid var(--color-brand-accent);
114
+ border-inline-start: var(--border-medium) solid var(--color-brand-accent);
115
+ border-start-start-radius: var(--radius-lg);
116
+ pointer-events: none;
117
+ }
118
+
119
+ .ct-card--active,
120
+ .ct-card[data-state='active'] {
121
+ /* The datum: an orange leading edge marks the active/current card,
122
+ consistent with .ct-list__item and .ct-sidebar active states. The
123
+ ambient shadow stays first so the list interpolates against the
124
+ base card shadow and the edge can animate in. */
125
+ box-shadow: var(--ct-card-shadow), inset var(--component-datum-edge-width) 0 0 0 var(--color-brand-accent);
126
+ }
127
+
128
+ @media (forced-colors: active) {
129
+ /* The datum edge is an inset box-shadow, which forced colors drops —
130
+ re-encode it as a border. */
131
+ .ct-card--active,
132
+ .ct-card[data-state='active'] {
133
+ border-inline-start: var(--component-datum-edge-width) solid SelectedItem;
134
+ }
135
+ }