@neuravision/construct 2.0.2 → 2.1.1

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 (115) hide show
  1. package/CHANGELOG.md +272 -0
  2. package/README.md +148 -152
  3. package/behaviors/combobox.js +447 -0
  4. package/behaviors/drawer.js +18 -0
  5. package/behaviors/dropdown.js +296 -0
  6. package/behaviors/index.d.ts +333 -0
  7. package/behaviors/index.js +9 -0
  8. package/behaviors/internal/dialog-controller.js +351 -0
  9. package/behaviors/internal/dom.js +377 -0
  10. package/behaviors/internal/overlay.js +241 -0
  11. package/behaviors/internal/popup-layer.js +188 -0
  12. package/behaviors/modal.js +17 -0
  13. package/behaviors/popover.js +269 -0
  14. package/behaviors/select-menu.js +326 -0
  15. package/behaviors/tabs.js +230 -0
  16. package/behaviors/toggle-group.js +234 -0
  17. package/behaviors/tooltip.js +233 -0
  18. package/components/README.md +279 -83
  19. package/components/_shared.css +11 -10
  20. package/components/_shell-shared.css +20 -4
  21. package/components/app-shell-v2.css +21 -14
  22. package/components/app-shell.css +23 -14
  23. package/components/badge.css +15 -0
  24. package/components/button.css +103 -15
  25. package/components/card.css +53 -8
  26. package/components/checkbox.css +3 -2
  27. package/components/chip.css +1 -1
  28. package/components/datepicker.css +3 -1
  29. package/components/datum.css +204 -0
  30. package/components/index.css +5 -0
  31. package/components/meter.css +182 -0
  32. package/components/navbar.css +88 -3
  33. package/components/slider.css +2 -1
  34. package/components/status.css +107 -0
  35. package/components/switch.css +4 -3
  36. package/components/tooltip.css +4 -0
  37. package/docs/architecture.md +79 -0
  38. package/docs/behaviors.md +101 -0
  39. package/docs/fonts.md +70 -0
  40. package/docs/governance.md +85 -0
  41. package/docs/guidelines.md +42 -23
  42. package/examples/react/README.md +20 -0
  43. package/examples/react/SettingsModal.jsx +66 -0
  44. package/examples/react/construct.css +5 -0
  45. package/fonts.css +10 -0
  46. package/foundations.css +8 -1
  47. package/package.json +72 -21
  48. package/schemas/components.schema.json +107 -0
  49. package/schemas/examples/theme.example.json +27 -0
  50. package/schemas/primitives.schema.json +135 -0
  51. package/schemas/semantic.schema.json +223 -0
  52. package/schemas/theme.schema.json +63 -0
  53. package/scripts/build-tokens.mjs +1417 -0
  54. package/scripts/construct-theme.mjs +43 -0
  55. package/scripts/validate-tokens.mjs +25 -0
  56. package/standalone/accordion.css +3 -0
  57. package/standalone/alert.css +3 -0
  58. package/standalone/app-shell-v2.css +4 -0
  59. package/standalone/app-shell.css +4 -0
  60. package/standalone/avatar.css +3 -0
  61. package/standalone/badge.css +3 -0
  62. package/standalone/banner.css +3 -0
  63. package/standalone/breadcrumbs.css +3 -0
  64. package/standalone/button.css +3 -0
  65. package/standalone/card.css +3 -0
  66. package/standalone/chart.css +3 -0
  67. package/standalone/checkbox.css +3 -0
  68. package/standalone/chip.css +3 -0
  69. package/standalone/combobox.css +3 -0
  70. package/standalone/core.css +4 -0
  71. package/standalone/data-table.css +3 -0
  72. package/standalone/datepicker.css +3 -0
  73. package/standalone/datum.css +3 -0
  74. package/standalone/divider.css +3 -0
  75. package/standalone/drawer.css +3 -0
  76. package/standalone/dropdown.css +3 -0
  77. package/standalone/empty-state.css +3 -0
  78. package/standalone/field.css +3 -0
  79. package/standalone/file-upload.css +3 -0
  80. package/standalone/icon.css +3 -0
  81. package/standalone/input.css +3 -0
  82. package/standalone/list.css +3 -0
  83. package/standalone/meter.css +3 -0
  84. package/standalone/modal.css +3 -0
  85. package/standalone/navbar.css +3 -0
  86. package/standalone/pagination.css +3 -0
  87. package/standalone/popover.css +3 -0
  88. package/standalone/progress-bar.css +3 -0
  89. package/standalone/select-menu.css +3 -0
  90. package/standalone/select.css +3 -0
  91. package/standalone/sidebar.css +3 -0
  92. package/standalone/skeleton.css +3 -0
  93. package/standalone/skip-link.css +3 -0
  94. package/standalone/slider.css +3 -0
  95. package/standalone/spinner.css +3 -0
  96. package/standalone/status.css +3 -0
  97. package/standalone/switch.css +3 -0
  98. package/standalone/table.css +3 -0
  99. package/standalone/tabs.css +3 -0
  100. package/standalone/textarea.css +3 -0
  101. package/standalone/toast.css +3 -0
  102. package/standalone/toggle-group.css +3 -0
  103. package/standalone/toolbar.css +3 -0
  104. package/standalone/tooltip.css +3 -0
  105. package/standalone/tree.css +3 -0
  106. package/tokens/README.md +130 -126
  107. package/tokens/components.json +54 -0
  108. package/tokens/primitives.json +268 -0
  109. package/tokens/semantic.dark.json +93 -0
  110. package/tokens/semantic.high-contrast.json +93 -0
  111. package/tokens/semantic.light.json +178 -0
  112. package/tokens/tokens.css +532 -28
  113. package/tokens/tokens.js +335 -25
  114. package/tokens/tokens.json +764 -21
  115. package/tokens/tokens.ts +335 -25
@@ -0,0 +1,204 @@
1
+ /* ============================================================
2
+ Datum — the orange reference-line grammar.
3
+
4
+ Construct's signature is one orange reference line ("the datum")
5
+ that marks what is focused / active / current. These primitives
6
+ promote that signature from focus rings and the intro page into
7
+ reusable structural vocabulary every surface can speak.
8
+
9
+ Accessibility contract (must hold across light/dark/high-contrast):
10
+ · orange as TEXT → --color-brand-accent-active (orange.700, ≥4.5:1)
11
+ · orange as EDGE/RULE (non-text) → --color-brand-accent (orange.500)
12
+ ============================================================ */
13
+
14
+ /* ── Eyebrow ──
15
+ A mono kicker with a leading datum tick. Sits above a heading to
16
+ label a section in the engineered "data annotation" voice. */
17
+ .ct-eyebrow {
18
+ display: inline-flex;
19
+ align-items: center;
20
+ gap: var(--space-4);
21
+ margin: 0 0 var(--space-5);
22
+ font-family: var(--font-family-mono);
23
+ font-size: var(--font-size-xs);
24
+ font-weight: var(--font-weight-semibold);
25
+ text-transform: uppercase;
26
+ letter-spacing: 0.16em;
27
+ /* Text stays a theme-safe neutral (AA on every surface in all three
28
+ themes); the orange datum lives in the non-text tick below. Orange
29
+ as small text fails contrast on dark/elevated and high-contrast. */
30
+ color: var(--color-text-secondary);
31
+ }
32
+
33
+ .ct-eyebrow::before {
34
+ content: '';
35
+ width: 24px;
36
+ height: 3px;
37
+ border-radius: var(--radius-xs);
38
+ background: var(--color-brand-accent);
39
+ }
40
+
41
+ /* ── Datum scale ──
42
+ A measured reference line: a hairline baseline with hanging ruler
43
+ ticks (minor every 12px, major every 60px) and an orange "origin"
44
+ segment marking zero. Decorative — mark it role="presentation". */
45
+ .ct-datum-scale {
46
+ height: 9px;
47
+ background:
48
+ /* orange origin segment — the datum's zero */
49
+ linear-gradient(var(--color-brand-accent), var(--color-brand-accent)) 0 0 / 48px 2px no-repeat,
50
+ /* full-width baseline hairline */
51
+ linear-gradient(var(--color-border-strong), var(--color-border-strong)) 0 0 / 100% 1px no-repeat,
52
+ /* major ticks */
53
+ repeating-linear-gradient(90deg, var(--color-border-strong) 0 1px, transparent 1px 60px) 0 0 / 100% 8px no-repeat,
54
+ /* minor ticks */
55
+ repeating-linear-gradient(90deg, var(--color-border-default) 0 1px, transparent 1px 12px) 0 0 / 100% 5px no-repeat;
56
+ }
57
+
58
+ /* ── Spec datasheet ──
59
+ Engineering datasheet for key figures — hairline-gridded cells
60
+ instead of free-floating big numbers. Mark the lead cell with
61
+ `--origin` to carry the datum. */
62
+ :is(.ct-metrics, .ct-specs) {
63
+ --ct-metric-bg: var(--component-metric-background);
64
+ --ct-metric-border: var(--component-metric-border);
65
+ --ct-metric-radius: var(--component-metric-radius);
66
+ --ct-metric-padding: var(--component-metric-padding);
67
+ --ct-metric-value-font-size: var(--component-metric-value-font-size);
68
+ --ct-metric-label-font-size: var(--component-metric-label-font-size);
69
+
70
+ display: grid;
71
+ grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
72
+ gap: var(--border-thin);
73
+ border: var(--border-thin) solid var(--ct-metric-border);
74
+ border-radius: var(--ct-metric-radius);
75
+ background: var(--ct-metric-border);
76
+ overflow: hidden;
77
+ }
78
+
79
+ /* Compatibility profile: retain the original datasheet scale while the
80
+ canonical metric pattern uses the denser component-token defaults. */
81
+ .ct-specs {
82
+ --ct-metric-radius: var(--radius-lg);
83
+ --ct-metric-padding: var(--space-7);
84
+ --ct-metric-value-font-size: var(--font-size-4xl);
85
+ --ct-metric-label-font-size: var(--font-size-xs);
86
+ }
87
+
88
+ :is(.ct-metric, .ct-spec) {
89
+ --ct-metric-accent: transparent;
90
+ --ct-metric-status-color: var(--color-text-secondary);
91
+
92
+ position: relative;
93
+ min-width: 0;
94
+ margin: 0;
95
+ padding: var(--ct-metric-padding);
96
+ background: var(--ct-metric-bg);
97
+ }
98
+
99
+ .ct-metric {
100
+ display: grid;
101
+ }
102
+
103
+ :is(.ct-metric--active, .ct-metric[data-state='active'], .ct-spec--origin) {
104
+ --ct-metric-accent: var(--color-brand-accent);
105
+
106
+ box-shadow: inset 0 var(--border-thick) 0 0 var(--ct-metric-accent);
107
+ }
108
+
109
+ :is(.ct-metric__value, .ct-spec__value) {
110
+ margin: 0;
111
+ font-family: var(--font-family-brand);
112
+ font-size: var(--ct-metric-value-font-size);
113
+ font-weight: var(--font-weight-extrabold);
114
+ line-height: 1;
115
+ color: var(--color-text-primary);
116
+ font-variant-numeric: tabular-nums lining-nums;
117
+ }
118
+
119
+ .ct-metric__value {
120
+ order: 1;
121
+ }
122
+
123
+ :is(.ct-metric__value, .ct-spec__value) small {
124
+ font-size: var(--font-size-xl);
125
+ font-weight: var(--font-weight-bold);
126
+ }
127
+
128
+ :is(.ct-metric__label, .ct-spec__label) {
129
+ display: block;
130
+ margin-top: var(--space-4);
131
+ font-family: var(--font-family-mono);
132
+ font-size: var(--ct-metric-label-font-size);
133
+ font-weight: var(--font-weight-semibold);
134
+ text-transform: uppercase;
135
+ letter-spacing: 0.14em;
136
+ color: var(--color-text-secondary);
137
+ }
138
+
139
+ .ct-metric__label {
140
+ order: 2;
141
+ }
142
+
143
+ .ct-metric__meta {
144
+ display: flex;
145
+ align-items: center;
146
+ justify-content: space-between;
147
+ gap: var(--space-3);
148
+ margin-top: var(--space-4);
149
+ color: var(--color-text-secondary);
150
+ font-size: var(--font-size-xs);
151
+ font-family: var(--font-family-body);
152
+ font-weight: var(--font-weight-regular);
153
+ line-height: var(--line-height-xs);
154
+ text-transform: none;
155
+ letter-spacing: normal;
156
+ order: 3;
157
+ }
158
+
159
+ .ct-metric__status {
160
+ color: var(--ct-metric-status-color);
161
+ font-weight: var(--font-weight-semibold);
162
+ }
163
+
164
+ /* Status wording stays neutral and AA-safe on every metric surface;
165
+ semantic colour is carried by the non-text datum edge. */
166
+
167
+ .ct-metric[data-status='success'] {
168
+ --ct-metric-accent: var(--color-state-success);
169
+ --ct-metric-status-color: var(--color-text-primary);
170
+ }
171
+
172
+ .ct-metric[data-status='info'] {
173
+ --ct-metric-accent: var(--color-state-info);
174
+ --ct-metric-status-color: var(--color-text-primary);
175
+ }
176
+
177
+ .ct-metric[data-status='warning'] {
178
+ --ct-metric-accent: var(--color-state-warning);
179
+ --ct-metric-status-color: var(--color-text-primary);
180
+ }
181
+
182
+ .ct-metric:is([data-status='danger'], [data-status='error'], [data-state='error']) {
183
+ --ct-metric-accent: var(--color-state-danger);
184
+ --ct-metric-status-color: var(--color-text-primary);
185
+ }
186
+
187
+ .ct-metric[data-status],
188
+ .ct-metric[data-state='error'] {
189
+ box-shadow: inset var(--border-thick) 0 0 0 var(--ct-metric-accent);
190
+ }
191
+
192
+ .ct-metric:is([aria-busy='true'], [data-state='loading']) {
193
+ cursor: wait;
194
+ }
195
+
196
+ .ct-metric:is([aria-disabled='true'], [data-state='disabled']) {
197
+ opacity: var(--opacity-disabled);
198
+ }
199
+
200
+ @media (forced-colors: active) {
201
+ :is(.ct-metric--active, .ct-metric[data-state='active'], .ct-metric[data-status], .ct-metric[data-state='error'], .ct-spec--origin) {
202
+ border-block-start: var(--border-medium) solid currentColor;
203
+ }
204
+ }
@@ -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';
@@ -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,6 +29,8 @@
29
29
  display: flex;
30
30
  align-items: center;
31
31
  gap: var(--ct-navbar-gap);
32
+ width: 100%;
33
+ min-width: 0;
32
34
  height: var(--ct-navbar-height);
33
35
  padding: var(--ct-navbar-padding-y) var(--ct-navbar-padding-x);
34
36
  background: var(--ct-navbar-bg);
@@ -44,6 +46,7 @@
44
46
  display: flex;
45
47
  align-items: center;
46
48
  flex-shrink: 0;
49
+ min-width: 0;
47
50
  }
48
51
 
49
52
  .ct-navbar__brand {
@@ -56,6 +59,12 @@
56
59
  flex-shrink: 0;
57
60
  }
58
61
 
62
+ .ct-navbar__brand:focus-visible {
63
+ outline: var(--border-medium) solid var(--color-focus-ring);
64
+ outline-offset: var(--border-medium);
65
+ border-radius: var(--ct-navbar-item-radius);
66
+ }
67
+
59
68
  .ct-navbar__logo {
60
69
  height: var(--ct-navbar-brand-img-height);
61
70
  width: auto;
@@ -63,8 +72,11 @@
63
72
  }
64
73
 
65
74
  .ct-navbar__title {
75
+ min-width: 0;
66
76
  font-size: var(--font-size-md);
67
77
  font-weight: var(--font-weight-semibold);
78
+ overflow: hidden;
79
+ text-overflow: ellipsis;
68
80
  white-space: nowrap;
69
81
  }
70
82
 
@@ -345,6 +357,8 @@
345
357
  border-bottom: var(--border-thin) solid var(--ct-navbar-border-color);
346
358
  box-shadow: var(--shadow-md);
347
359
  padding: var(--space-4);
360
+ max-height: calc(100dvh - var(--ct-navbar-height));
361
+ overflow-y: auto;
348
362
  display: flex;
349
363
  flex-direction: column;
350
364
  gap: var(--space-1);
@@ -420,6 +434,32 @@
420
434
  font-size: var(--font-size-lg);
421
435
  }
422
436
 
437
+ /* ── Compact Product Header ────────────────────────────────────── */
438
+
439
+ .ct-navbar--compact {
440
+ --ct-navbar-height: var(--control-height-lg);
441
+ --ct-navbar-padding-x: var(--space-4);
442
+ --ct-navbar-padding-y: var(--space-2);
443
+ --ct-navbar-gap: var(--space-2);
444
+ --ct-navbar-actions-gap: var(--space-1);
445
+ --ct-navbar-brand-img-height: var(--space-8);
446
+ --ct-navbar-item-padding-x: var(--space-3);
447
+ --ct-navbar-item-padding-y: var(--space-2);
448
+ }
449
+
450
+ .ct-navbar--compact .ct-navbar__brand {
451
+ min-width: 0;
452
+ }
453
+
454
+ .ct-navbar--compact .ct-navbar__title {
455
+ max-width: 18ch;
456
+ font-size: var(--font-size-sm);
457
+ }
458
+
459
+ .ct-navbar--compact .ct-navbar__actions {
460
+ flex-shrink: 0;
461
+ }
462
+
423
463
  /* ── Sticky ────────────────────────────────────────────────────── */
424
464
 
425
465
  .ct-navbar--sticky {
@@ -463,7 +503,7 @@
463
503
 
464
504
  @media (hover: hover) {
465
505
  .ct-navbar--transparent .ct-navbar__link:hover {
466
- background: rgb(255 255 255 / 0.15);
506
+ background: color-mix(in srgb, var(--color-text-inverse) 15%, transparent);
467
507
  color: var(--color-text-inverse);
468
508
  }
469
509
  }
@@ -486,7 +526,7 @@
486
526
 
487
527
  @media (hover: hover) {
488
528
  .ct-navbar--transparent .ct-navbar__toggle:hover {
489
- background: rgb(255 255 255 / 0.15);
529
+ background: color-mix(in srgb, var(--color-text-inverse) 15%, transparent);
490
530
  }
491
531
  }
492
532
 
@@ -590,15 +630,49 @@
590
630
  position: relative;
591
631
  }
592
632
 
593
- .ct-navbar__nav {
633
+ .ct-navbar:has(.ct-navbar__toggle) .ct-navbar__nav {
594
634
  display: none;
595
635
  }
596
636
 
637
+ /* Progressive fallback: when no mobile toggle/menu is supplied, keep
638
+ primary navigation reachable as a horizontally scrollable row. */
639
+ .ct-navbar:not(:has(.ct-navbar__toggle)) nav {
640
+ min-width: 0;
641
+ overflow-x: auto;
642
+ scrollbar-width: thin;
643
+ }
644
+
645
+ .ct-navbar:not(:has(.ct-navbar__toggle)) .ct-navbar__nav {
646
+ display: flex;
647
+ width: max-content;
648
+ }
649
+
597
650
  .ct-navbar__toggle {
598
651
  display: inline-flex;
599
652
  }
600
653
  }
601
654
 
655
+ @media (max-width: 599px) { /* < sm breakpoint (600px) */
656
+ .ct-navbar,
657
+ .ct-navbar--compact {
658
+ --ct-navbar-padding-x: var(--space-3);
659
+ --ct-navbar-gap: var(--space-1);
660
+ --ct-navbar-actions-gap: var(--space-1);
661
+ }
662
+
663
+ .ct-navbar--compact .ct-navbar__title {
664
+ max-width: 10ch;
665
+ }
666
+
667
+ .ct-navbar--compact .ct-navbar__actions [data-priority='secondary'] {
668
+ display: none;
669
+ }
670
+
671
+ .ct-navbar__mobile-menu {
672
+ padding: var(--space-3);
673
+ }
674
+ }
675
+
602
676
  /* ── Reduced Motion ────────────────────────────────────────────── */
603
677
 
604
678
  @media (prefers-reduced-motion: reduce) {
@@ -612,3 +686,14 @@
612
686
  transition: none;
613
687
  }
614
688
  }
689
+
690
+ @media (forced-colors: active) {
691
+ .ct-navbar {
692
+ border-color: currentColor;
693
+ }
694
+
695
+ .ct-navbar__link[aria-current='page'] {
696
+ outline: var(--border-medium) solid currentColor;
697
+ outline-offset: calc(-1 * var(--border-medium));
698
+ }
699
+ }
@@ -269,7 +269,8 @@
269
269
  opacity: 1;
270
270
  }
271
271
 
272
- /* Touch-target minimums (WCAG 2.5.8: 44×44px minimum) */
272
+ /* Construct 44×44 target policy: meets the size in WCAG 2.2 SC 2.5.5
273
+ (AAA) and exceeds the AA minimum in SC 2.5.8. */
273
274
 
274
275
  @media (pointer: coarse) {
275
276
  .ct-slider {
@@ -0,0 +1,107 @@
1
+ /* ── Live / Connection Status ──────────────────────────────────── */
2
+
3
+ .ct-status {
4
+ --ct-status-gap: var(--component-status-gap);
5
+ --ct-status-font-size: var(--component-status-font-size);
6
+ --ct-status-icon-size: var(--component-status-icon-size);
7
+ --ct-status-color: var(--color-text-secondary);
8
+ --ct-status-indicator-color: var(--color-border-strong);
9
+
10
+ display: inline-flex;
11
+ align-items: center;
12
+ gap: var(--ct-status-gap);
13
+ color: var(--ct-status-color);
14
+ font-size: var(--ct-status-font-size);
15
+ font-weight: var(--font-weight-medium);
16
+ line-height: var(--line-height-sm);
17
+ }
18
+
19
+ .ct-status__indicator,
20
+ .ct-status__icon {
21
+ width: var(--ct-status-icon-size);
22
+ height: var(--ct-status-icon-size);
23
+ flex: none;
24
+ }
25
+
26
+ .ct-status__indicator {
27
+ border: var(--border-thin) solid currentColor;
28
+ border-radius: var(--radius-round);
29
+ background: var(--ct-status-indicator-color);
30
+ color: var(--ct-status-indicator-color);
31
+ }
32
+
33
+ .ct-status__icon {
34
+ display: inline-flex;
35
+ align-items: center;
36
+ justify-content: center;
37
+ color: var(--ct-status-indicator-color);
38
+ }
39
+
40
+ .ct-status__label {
41
+ min-width: 0;
42
+ }
43
+
44
+ .ct-status__detail {
45
+ color: var(--color-text-muted);
46
+ font-weight: var(--font-weight-regular);
47
+ }
48
+
49
+ /* Labels use surface-safe neutral text because status can appear on any
50
+ surface. Semantic colour is reserved for the non-text indicator, while
51
+ the required visible wording carries meaning without colour. */
52
+
53
+ .ct-status:is([data-status='online'], [data-status='success']) {
54
+ --ct-status-color: var(--color-text-primary);
55
+ --ct-status-indicator-color: var(--color-state-success);
56
+ }
57
+
58
+ .ct-status:is([data-status='connecting'], [data-status='info'], [data-state='loading']) {
59
+ --ct-status-color: var(--color-text-primary);
60
+ --ct-status-indicator-color: var(--color-state-info);
61
+ }
62
+
63
+ .ct-status:is([data-status='degraded'], [data-status='warning']) {
64
+ --ct-status-color: var(--color-text-primary);
65
+ --ct-status-indicator-color: var(--color-state-warning);
66
+ }
67
+
68
+ .ct-status:is([data-status='offline'], [data-status='neutral']) {
69
+ --ct-status-color: var(--color-text-secondary);
70
+ --ct-status-indicator-color: var(--color-border-strong);
71
+ }
72
+
73
+ .ct-status:is([data-status='error'], [data-status='danger'], [data-state='error']) {
74
+ --ct-status-color: var(--color-text-primary);
75
+ --ct-status-indicator-color: var(--color-state-danger);
76
+ }
77
+
78
+ .ct-status:is([aria-busy='true'], [data-status='connecting'], [data-state='loading']) {
79
+ cursor: wait;
80
+ }
81
+
82
+ .ct-status:is([aria-busy='true'], [data-status='connecting'], [data-state='loading']) .ct-status__indicator {
83
+ background: transparent;
84
+ border: var(--border-medium) solid currentColor;
85
+ border-inline-end-color: transparent;
86
+ animation: ct-spin var(--ct-spinner-duration) linear infinite;
87
+ }
88
+
89
+ .ct-status:is([aria-disabled='true'], [data-state='disabled']) {
90
+ opacity: var(--opacity-disabled);
91
+ }
92
+
93
+ @media (prefers-reduced-motion: reduce) {
94
+ .ct-status__indicator {
95
+ animation: none;
96
+ }
97
+ }
98
+
99
+ @media (forced-colors: active) {
100
+ .ct-status__indicator {
101
+ border: var(--border-medium) solid currentColor;
102
+ }
103
+
104
+ .ct-status:is([aria-busy='true'], [data-status='connecting'], [data-state='loading']) .ct-status__indicator {
105
+ border-inline-end-style: dotted;
106
+ }
107
+ }