@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
@@ -1,6 +1,6 @@
1
- # Construct Components v1
1
+ # Construct Components v2
2
2
 
3
- Framework-agnostic component styles built on Construct design tokens. Use these styles directly with HTML or wrap them in Angular, React, or Svelte components. Interactive states are controlled via data attributes and ARIA, making them easy to enhance with JavaScript.
3
+ Framework-agnostic component styles built on Construct design tokens. Use these styles directly with HTML or wrap them in Angular, React, or Svelte components. Visual state follows native, ARIA, and documented data attributes; optional typed controllers provide the corresponding keyboard and focus behavior.
4
4
 
5
5
  ## Usage
6
6
 
@@ -18,18 +18,26 @@ Example:
18
18
  ## State Conventions
19
19
 
20
20
  - `data-state="open"` / `data-state="closed"` for popovers, modals, tooltips
21
+ - `data-state="active"`, `loading`, `error`, or `disabled` for component state
22
+ - `data-status="success"`, `info`, `warning`, `danger`, or connection-specific status text for semantic status
23
+ - `aria-busy="true"` while a region is updating; keep a visible loading message
24
+ - `data-loading="true"` as the button's visual loading hook; native buttons also require `disabled`
21
25
  - `aria-invalid="true"` for form errors
22
26
  - `aria-current="page"` for current pagination page
23
27
  - `aria-selected="true"` for active tabs and selected datepicker days
24
28
  - `aria-disabled="true"` when disabled but not using native `disabled`
25
29
 
30
+ ARIA communicates state but does not block events. Prefer native `disabled` for form controls. For links and custom controls, suppress pointer and keyboard activation in application code when `aria-disabled="true"` is set. Add live-region attributes only to content that changes dynamically; static status labels do not need `aria-live`.
31
+
26
32
  ## Keyboard + ARIA Requirements
27
33
 
28
- When building framework wrappers, implement these keyboard patterns:
34
+ Use `@neuravision/construct/behaviors` where a controller is available (including Tabs, menu Dropdown,
35
+ Modal, and Tooltip), or implement an equivalent tested framework adapter. CSS alone does not provide
36
+ these keyboard patterns:
29
37
 
30
38
  - **Tabs**: Arrow keys move focus between tabs, Home/End jumps to first/last, Enter/Space activates. Use roving tabindex (active tab `tabindex="0"`, others `tabindex="-1"`).
31
- - **Dropdown (Action-List)**: Trigger uses `aria-expanded` + `aria-controls`. On open, move focus to first item, Esc closes and returns focus to trigger.
32
- - **Dropdown (Role=menu)**: Only use if implementing arrow-key navigation, Home/End, typeahead, and roving tabindex.
39
+ - **Dropdown (Action menu)**: Trigger uses `aria-expanded`, `aria-controls`, and `aria-haspopup="menu"`. The menu uses roving focus; Arrow keys, Home/End, and typeahead navigate, Esc closes and restores trigger focus, and Tab closes without cancelling normal focus movement. Use `createDropdownController` for this complete contract.
40
+ - **Generic disclosure**: When popup content contains ordinary links or controls in natural Tab order, use the Popover pattern rather than adding menu roles.
33
41
  - **Datepicker**: Arrow keys move by day, PageUp/PageDown switches months, Home/End jumps within week, Enter/Space selects, Esc closes.
34
42
  - **Modal**: Focus trap, initial focus inside dialog, Esc closes, focus returns to trigger.
35
43
  - **Tooltip**: Opens on hover/focus, closes on blur/Esc, uses `role="tooltip"` with `aria-describedby`.
@@ -41,31 +49,39 @@ When building framework wrappers, implement these keyboard patterns:
41
49
  Basic button with variants and sizes:
42
50
 
43
51
  ```html
44
- <button class="ct-button">Primary</button>
45
- <button class="ct-button ct-button--secondary">Secondary</button>
46
- <button class="ct-button ct-button--ghost">Ghost</button>
47
- <button class="ct-button ct-button--outline">Outline</button>
48
- <button class="ct-button ct-button--danger">Danger</button>
49
- <button class="ct-button ct-button--accent">Accent</button>
50
- <button class="ct-button ct-button--link">Link</button>
52
+ <button class="ct-button" type="button">Primary</button>
53
+ <button class="ct-button ct-button--secondary" type="button">Secondary</button>
54
+ <button class="ct-button ct-button--ghost" type="button">Ghost</button>
55
+ <button class="ct-button ct-button--outline" type="button">Outline</button>
56
+ <button class="ct-button ct-button--danger" type="button">Danger</button>
57
+ <button class="ct-button ct-button--accent" type="button">Accent</button>
58
+ <button class="ct-button ct-button--link" type="button">Link</button>
51
59
 
52
60
  <!-- Sizes -->
53
- <button class="ct-button ct-button--sm">Small</button>
54
- <button class="ct-button">Medium</button>
55
- <button class="ct-button ct-button--lg">Large</button>
61
+ <button class="ct-button ct-button--sm" type="button">Small</button>
62
+ <button class="ct-button" type="button">Medium</button>
63
+ <button class="ct-button ct-button--lg" type="button">Large</button>
56
64
 
57
65
  <!-- With icon -->
58
- <button class="ct-button">
66
+ <button class="ct-button" type="button">
59
67
  <span class="ct-button__icon" aria-hidden="true">+</span>
60
68
  Add item
61
69
  </button>
62
70
 
63
71
  <!-- Icon only -->
64
- <button class="ct-button ct-button--icon" aria-label="Settings">
72
+ <button class="ct-button ct-button--icon" type="button" aria-label="Settings">
65
73
  <span class="ct-button__icon" aria-hidden="true">⚙</span>
66
74
  </button>
75
+
76
+ <!-- Loading: stable label, decorative loader, native interaction lock -->
77
+ <button class="ct-button" type="button" aria-busy="true" data-loading="true" disabled>
78
+ <span class="ct-button__content">Save changes</span>
79
+ <span class="ct-button__loader" aria-hidden="true"></span>
80
+ </button>
67
81
  ```
68
82
 
83
+ The `.ct-button__content` remains in layout, so switching to loading does not change the button width. Its text also remains the accessible name. `aria-busy` exposes the state, `data-loading` selects the visual treatment, and native `disabled` prevents both pointer and keyboard activation. The spinner stops animating under `prefers-reduced-motion: reduce`.
84
+
69
85
  ### Field + Input
70
86
 
71
87
  Form field with label, input, hint, and error states:
@@ -73,15 +89,17 @@ Form field with label, input, hint, and error states:
73
89
  ```html
74
90
  <div class="ct-field">
75
91
  <label class="ct-field__label" for="email">Email</label>
76
- <input class="ct-input" id="email" type="email" placeholder="name@company.com" />
77
- <div class="ct-field__hint">We will not share this.</div>
92
+ <input class="ct-input" id="email" type="email" placeholder="name@company.com"
93
+ aria-describedby="email-hint" />
94
+ <div class="ct-field__hint" id="email-hint">We will not share this.</div>
78
95
  </div>
79
96
 
80
97
  <!-- Error state -->
81
98
  <div class="ct-field ct-field--error">
82
99
  <label class="ct-field__label" for="name">Name</label>
83
- <input class="ct-input" id="name" aria-invalid="true" />
84
- <div class="ct-field__error">Name is required.</div>
100
+ <input class="ct-input" id="name" type="text" aria-invalid="true"
101
+ aria-describedby="name-error" />
102
+ <div class="ct-field__error" id="name-error">Name is required.</div>
85
103
  </div>
86
104
 
87
105
  <!-- With icon -->
@@ -122,7 +140,7 @@ Multi-line text input:
122
140
 
123
141
  ### Checkbox
124
142
 
125
- Single checkbox or checkbox group:
143
+ Single checkbox or a labelled checkbox group:
126
144
 
127
145
  ```html
128
146
  <label class="ct-check">
@@ -130,10 +148,17 @@ Single checkbox or checkbox group:
130
148
  <span class="ct-check__label">Remember me</span>
131
149
  </label>
132
150
 
133
- <label class="ct-check">
134
- <input class="ct-check__input" type="checkbox" checked />
135
- <span class="ct-check__label">Send weekly reports</span>
136
- </label>
151
+ <fieldset>
152
+ <legend class="ct-field__label">Notifications</legend>
153
+ <label class="ct-check">
154
+ <input class="ct-check__input" type="checkbox" checked />
155
+ <span class="ct-check__label">Send weekly reports</span>
156
+ </label>
157
+ <label class="ct-check">
158
+ <input class="ct-check__input" type="checkbox" />
159
+ <span class="ct-check__label">Send security alerts</span>
160
+ </label>
161
+ </fieldset>
137
162
  ```
138
163
 
139
164
  ### Radio
@@ -141,15 +166,18 @@ Single checkbox or checkbox group:
141
166
  Radio button group:
142
167
 
143
168
  ```html
144
- <label class="ct-radio">
145
- <input class="ct-radio__input" type="radio" name="plan" checked />
146
- <span class="ct-radio__label">Standard</span>
147
- </label>
169
+ <fieldset>
170
+ <legend class="ct-field__label">Plan</legend>
171
+ <label class="ct-radio">
172
+ <input class="ct-radio__input" type="radio" name="plan" checked />
173
+ <span class="ct-radio__label">Standard</span>
174
+ </label>
148
175
 
149
- <label class="ct-radio">
150
- <input class="ct-radio__input" type="radio" name="plan" />
151
- <span class="ct-radio__label">Premium</span>
152
- </label>
176
+ <label class="ct-radio">
177
+ <input class="ct-radio__input" type="radio" name="plan" />
178
+ <span class="ct-radio__label">Premium</span>
179
+ </label>
180
+ </fieldset>
153
181
  ```
154
182
 
155
183
  ### Switch
@@ -171,7 +199,7 @@ Content card with header, body, and footer:
171
199
  <section class="ct-card">
172
200
  <div class="ct-card__header">
173
201
  <h3>Team</h3>
174
- <button class="ct-button ct-button--ghost">Edit</button>
202
+ <button class="ct-button ct-button--ghost" type="button">Edit</button>
175
203
  </div>
176
204
  <div class="ct-card__body">
177
205
  <p>Shared ownership and clear permissions.</p>
@@ -179,7 +207,7 @@ Content card with header, body, and footer:
179
207
  </div>
180
208
  <div class="ct-card__footer">
181
209
  <span class="ct-muted">12 members</span>
182
- <button class="ct-button ct-button--secondary">Open</button>
210
+ <button class="ct-button ct-button--secondary" type="button">Open</button>
183
211
  </div>
184
212
  </section>
185
213
  ```
@@ -217,7 +245,8 @@ Basic data table with variants:
217
245
  Variants:
218
246
  - `ct-table--striped` - Alternating row backgrounds
219
247
  - `ct-table--compact` - Reduced padding
220
- - `ct-table--hover` - Row hover effect
248
+
249
+ Rows receive a visual hover affordance automatically on hover-capable pointers; no hover modifier exists.
221
250
 
222
251
  ### Data Table
223
252
 
@@ -231,8 +260,8 @@ Complex data table with header, toolbar, filters, and actions:
231
260
  <span class="ct-data-table__meta">24 total</span>
232
261
  </div>
233
262
  <div class="ct-data-table__actions">
234
- <button class="ct-button ct-button--secondary ct-button--sm">Export</button>
235
- <button class="ct-button ct-button--sm">New project</button>
263
+ <button class="ct-button ct-button--secondary ct-button--sm" type="button">Export</button>
264
+ <button class="ct-button ct-button--sm" type="button">New project</button>
236
265
  </div>
237
266
  </div>
238
267
 
@@ -260,29 +289,89 @@ Complex data table with header, toolbar, filters, and actions:
260
289
  </div>
261
290
  ```
262
291
 
292
+ ### Metric / Stat
293
+
294
+ Use `ct-metric` for product metrics. It is the semantic successor to `ct-spec`; the existing `ct-spec` classes remain supported for compatibility. Values use tabular numerals, and every semantic status includes visible text.
295
+
296
+ ```html
297
+ <dl class="ct-metrics" aria-label="Service metrics">
298
+ <div class="ct-metric" data-status="success">
299
+ <dt class="ct-metric__label">Availability</dt>
300
+ <dd class="ct-metric__value">99.98<small>%</small>
301
+ <span class="ct-metric__meta">
302
+ <span>Last 30 days</span>
303
+ <span class="ct-metric__status">Healthy</span>
304
+ </span>
305
+ </dd>
306
+ </div>
307
+ </dl>
308
+ ```
309
+
310
+ Use `data-state="active"`, `loading`, `error`, or `disabled` for structural state and `data-status` for `success`, `info`, `warning`, `danger`, or `error` semantics. Pair `data-state="loading"` with `aria-busy="true"`; pair disabled content with `aria-disabled="true"`.
311
+
312
+ ### Segmented Meter
313
+
314
+ The segmented meter represents a part-to-whole distribution. Its visible legend is required: colour is decorative reinforcement, never the only way a segment is identified. Use the scalar progress bar instead when one `aria-valuenow` value describes the whole component.
315
+
316
+ ```html
317
+ <div class="ct-meter" role="group"
318
+ aria-labelledby="delivery-label" aria-describedby="delivery-legend">
319
+ <div class="ct-meter__header">
320
+ <span class="ct-meter__label" id="delivery-label">Delivery status</span>
321
+ <span class="ct-meter__summary">100 items</span>
322
+ </div>
323
+ <div class="ct-meter__track" aria-hidden="true">
324
+ <span class="ct-meter__segment" data-variant="success"
325
+ style="--ct-meter-segment-size: 62%;"></span>
326
+ <span class="ct-meter__segment" data-variant="warning"
327
+ style="--ct-meter-segment-size: 23%;"></span>
328
+ <span class="ct-meter__segment" data-variant="danger"
329
+ style="--ct-meter-segment-size: 15%;"></span>
330
+ </div>
331
+ <ul class="ct-meter__legend" id="delivery-legend">
332
+ <li class="ct-meter__legend-item">
333
+ <span class="ct-meter__marker" data-variant="success" aria-hidden="true"></span>
334
+ <span>Completed</span><span class="ct-meter__legend-value">62%</span>
335
+ </li>
336
+ <li class="ct-meter__legend-item">
337
+ <span class="ct-meter__marker" data-variant="warning" aria-hidden="true"></span>
338
+ <span>In review</span><span class="ct-meter__legend-value">23%</span>
339
+ </li>
340
+ <li class="ct-meter__legend-item">
341
+ <span class="ct-meter__marker" data-variant="danger" aria-hidden="true"></span>
342
+ <span>Blocked</span><span class="ct-meter__legend-value">15%</span>
343
+ </li>
344
+ </ul>
345
+ </div>
346
+ ```
347
+
263
348
  ### Modal
264
349
 
265
350
  Dialog with backdrop:
266
351
 
267
352
  ```html
268
- <div class="ct-modal" data-state="open" role="dialog" aria-modal="true" aria-labelledby="modal-title">
269
- <div class="ct-modal__dialog">
353
+ <div class="ct-modal" data-state="closed" hidden>
354
+ <div class="ct-modal__dialog" role="dialog" aria-modal="true" aria-labelledby="modal-title">
270
355
  <div class="ct-modal__header">
271
356
  <h2 id="modal-title">Invite team</h2>
272
- <button class="ct-button ct-button--ghost" aria-label="Close">×</button>
357
+ <button class="ct-button ct-button--ghost" type="button"
358
+ aria-label="Close" data-ct-dismiss>×</button>
273
359
  </div>
274
360
  <div class="ct-modal__body">
275
361
  <p>Send an invite to a new team member.</p>
276
362
  <!-- form content -->
277
363
  </div>
278
364
  <div class="ct-modal__footer">
279
- <button class="ct-button ct-button--secondary">Cancel</button>
280
- <button class="ct-button">Send</button>
365
+ <button class="ct-button ct-button--secondary" type="button" data-ct-dismiss>Cancel</button>
366
+ <button class="ct-button" type="button">Send</button>
281
367
  </div>
282
368
  </div>
283
369
  </div>
284
370
  ```
285
371
 
372
+ Attach `createModalController({ container, dialog, trigger })` to synchronize state, trap focus,
373
+ handle Escape/backdrop dismissal, and restore focus. See [Headless behaviors](../docs/behaviors.md).
374
+
286
375
  ### Toast
287
376
 
288
377
  Notification with variants:
@@ -292,12 +381,12 @@ Notification with variants:
292
381
  <div class="ct-toast" data-variant="success" data-state="open">
293
382
  <div class="ct-toast__title">Saved</div>
294
383
  <div class="ct-toast__description">Your changes were saved.</div>
295
- <button class="ct-button ct-button--ghost">Undo</button>
384
+ <button class="ct-button ct-button--ghost" type="button">Undo</button>
296
385
  </div>
297
386
  </div>
298
387
  ```
299
388
 
300
- Variants: `success`, `error`, `info`, `warning`
389
+ Variants: `success`, `danger`, `info`, `warning`
301
390
 
302
391
  ### Alert / Banner
303
392
 
@@ -310,7 +399,7 @@ Inline feedback message with variants:
310
399
  <div class="ct-alert__title">Action required</div>
311
400
  <div class="ct-alert__description">Please review the pending changes.</div>
312
401
  <div class="ct-alert__actions">
313
- <button class="ct-button ct-button--secondary ct-button--sm">Review</button>
402
+ <button class="ct-button ct-button--secondary ct-button--sm" type="button">Review</button>
314
403
  </div>
315
404
  </div>
316
405
  </div>
@@ -330,6 +419,21 @@ Small status indicators with optional icon or dot:
330
419
  </span>
331
420
  ```
332
421
 
422
+ ### Live / Connection Status
423
+
424
+ Status text is mandatory; the indicator is decorative. Static statuses need no live-region role. For a connection label that changes in place, use a polite atomic status region.
425
+
426
+ ```html
427
+ <span class="ct-status" data-status="connecting" data-state="loading"
428
+ role="status" aria-live="polite" aria-atomic="true" aria-busy="true">
429
+ <span class="ct-status__indicator" aria-hidden="true"></span>
430
+ <span class="ct-status__label">Connecting</span>
431
+ <span class="ct-status__detail">Retrying</span>
432
+ </span>
433
+ ```
434
+
435
+ Connection values are `online`, `connecting`, `degraded`, `offline`, and `error`. Generic semantic aliases `success`, `info`, `warning`, `neutral`, and `danger` are also supported. Update the existing label in place so screen readers announce the change once.
436
+
333
437
  ### Chip / Tag
334
438
 
335
439
  Compact tags for filters or metadata:
@@ -348,7 +452,9 @@ Compact tags for filters or metadata:
348
452
 
349
453
  ### File Upload / Drag & Drop
350
454
 
351
- Drag area + file list pattern. Use `data-state="dragover"` and `aria-invalid="true"` when needed.
455
+ Drag area + file list pattern. Use `data-state="dragover"` while dragging. For validation, put
456
+ `aria-invalid="true"` on both the styled dropzone and the native file input, then connect visible
457
+ error text to the input with `aria-describedby`.
352
458
 
353
459
  ```html
354
460
  <div class="ct-file-upload">
@@ -368,7 +474,18 @@ Drag area + file list pattern. Use `data-state="dragover"` and `aria-invalid="tr
368
474
  <span class="ct-badge ct-badge--success">Uploaded</span>
369
475
  </li>
370
476
  </ul>
371
- <div class="ct-file-upload__error">File is too large.</div>
477
+ </div>
478
+
479
+ <!-- Invalid state -->
480
+ <div class="ct-file-upload">
481
+ <label class="ct-file-upload__dropzone" for="files-invalid" aria-invalid="true">
482
+ <input class="ct-file-upload__input" id="files-invalid" type="file"
483
+ aria-invalid="true" aria-describedby="files-error" />
484
+ <div class="ct-file-upload__title">Choose another file</div>
485
+ <div class="ct-file-upload__hint">PDF, DOCX up to 10MB</div>
486
+ <span class="ct-button ct-button--secondary ct-button--sm">Browse files</span>
487
+ </label>
488
+ <div class="ct-file-upload__error" id="files-error">File is too large.</div>
372
489
  </div>
373
490
  ```
374
491
 
@@ -377,7 +494,9 @@ Drag area + file list pattern. Use `data-state="dragover"` and `aria-invalid="tr
377
494
  Spinner for inline loading and overlay for blocking states:
378
495
 
379
496
  ```html
380
- <div class="ct-spinner ct-spinner--sm" role="status" aria-live="polite"></div>
497
+ <div class="ct-spinner ct-spinner--sm" role="status" aria-live="polite">
498
+ <span class="ct-sr-only">Loading…</span>
499
+ </div>
381
500
 
382
501
  <div class="ct-card" style="position: relative; min-height: 180px;">
383
502
  <div class="ct-loading-overlay" data-state="active" aria-busy="true">
@@ -394,8 +513,9 @@ Spinner for inline loading and overlay for blocking states:
394
513
  Use the modal base with a confirmation layout:
395
514
 
396
515
  ```html
397
- <div class="ct-modal ct-modal--confirmation" data-state="open" role="dialog" aria-modal="true" aria-labelledby="confirm-title" aria-describedby="confirm-desc">
398
- <div class="ct-modal__dialog">
516
+ <div class="ct-modal ct-modal--confirmation" data-state="open">
517
+ <div class="ct-modal__dialog" role="dialog" aria-modal="true"
518
+ aria-labelledby="confirm-title" aria-describedby="confirm-desc">
399
519
  <div class="ct-modal__body">
400
520
  <div class="ct-confirmation" data-variant="danger">
401
521
  <div class="ct-confirmation__icon" aria-hidden="true">!</div>
@@ -406,8 +526,8 @@ Use the modal base with a confirmation layout:
406
526
  </div>
407
527
  </div>
408
528
  <div class="ct-modal__footer">
409
- <button class="ct-button ct-button--secondary">Cancel</button>
410
- <button class="ct-button ct-button--danger">Delete</button>
529
+ <button class="ct-button ct-button--secondary" type="button" data-ct-dismiss>Cancel</button>
530
+ <button class="ct-button ct-button--danger" type="button">Delete</button>
411
531
  </div>
412
532
  </div>
413
533
  </div>
@@ -432,31 +552,41 @@ Tab navigation with panels:
432
552
  ```html
433
553
  <div class="ct-tabs">
434
554
  <div class="ct-tabs__list" role="tablist">
435
- <button class="ct-tabs__trigger" role="tab" aria-selected="true" aria-controls="panel-1" id="tab-1" tabindex="0">Overview</button>
436
- <button class="ct-tabs__trigger" role="tab" aria-selected="false" aria-controls="panel-2" id="tab-2" tabindex="-1">Settings</button>
555
+ <button class="ct-tabs__trigger" type="button" role="tab" aria-selected="true" aria-controls="panel-1" id="tab-1" tabindex="0">Overview</button>
556
+ <button class="ct-tabs__trigger" type="button" role="tab" aria-selected="false" aria-controls="panel-2" id="tab-2" tabindex="-1">Settings</button>
437
557
  </div>
438
558
  <div class="ct-tabs__panel" role="tabpanel" id="panel-1" aria-labelledby="tab-1">
439
559
  <p>Panel content</p>
440
560
  </div>
561
+ <div class="ct-tabs__panel" role="tabpanel" id="panel-2" aria-labelledby="tab-2" hidden>
562
+ <p>Settings content</p>
563
+ </div>
441
564
  </div>
442
565
  ```
443
566
 
567
+ Attach `createTabsController({ tablist })` unless the consuming framework already owns the equivalent roving-focus and selection state machine.
568
+
444
569
  ### Dropdown
445
570
 
446
571
  Action menu:
447
572
 
448
573
  ```html
449
- <div class="ct-dropdown" data-state="open">
450
- <button class="ct-button ct-button--secondary ct-dropdown__trigger" aria-expanded="true" aria-controls="menu">Actions</button>
451
- <div class="ct-dropdown__menu" id="menu">
452
- <button class="ct-dropdown__item" type="button">Edit</button>
453
- <button class="ct-dropdown__item" type="button">Duplicate</button>
574
+ <div class="ct-dropdown" data-state="closed">
575
+ <button class="ct-button ct-button--secondary ct-dropdown__trigger" type="button"
576
+ id="actions-trigger" aria-expanded="false" aria-haspopup="menu"
577
+ aria-controls="actions-menu">Actions</button>
578
+ <div class="ct-dropdown__menu" id="actions-menu" role="menu"
579
+ aria-labelledby="actions-trigger" hidden>
580
+ <button class="ct-dropdown__item" type="button" role="menuitem" tabindex="-1">Edit</button>
581
+ <button class="ct-dropdown__item" type="button" role="menuitem" tabindex="-1">Duplicate</button>
454
582
  <div class="ct-dropdown__separator" role="separator"></div>
455
- <button class="ct-dropdown__item" type="button">Delete</button>
583
+ <button class="ct-dropdown__item" type="button" role="menuitem" tabindex="-1">Delete</button>
456
584
  </div>
457
585
  </div>
458
586
  ```
459
587
 
588
+ Attach `createDropdownController({ root, trigger, menu })` to own `hidden`, `data-state`, ARIA, keyboard navigation, selection, outside interaction, and focus return. The caller still gives the menu an accessible name with `aria-label` or `aria-labelledby`.
589
+
460
590
  ### Pagination
461
591
 
462
592
  Page navigation:
@@ -480,14 +610,14 @@ Path navigation:
480
610
  <ol class="ct-breadcrumbs__list">
481
611
  <li class="ct-breadcrumbs__item">
482
612
  <a class="ct-breadcrumbs__link" href="/">Home</a>
483
- <span class="ct-breadcrumbs__separator">/</span>
613
+ <span class="ct-breadcrumbs__separator" aria-hidden="true">/</span>
484
614
  </li>
485
615
  <li class="ct-breadcrumbs__item">
486
616
  <a class="ct-breadcrumbs__link" href="/projects">Projects</a>
487
- <span class="ct-breadcrumbs__separator">/</span>
617
+ <span class="ct-breadcrumbs__separator" aria-hidden="true">/</span>
488
618
  </li>
489
619
  <li class="ct-breadcrumbs__item">
490
- <span class="ct-breadcrumbs__current">Alpha</span>
620
+ <span class="ct-breadcrumbs__current" aria-current="page">Alpha</span>
491
621
  </li>
492
622
  </ol>
493
623
  </nav>
@@ -495,25 +625,46 @@ Path navigation:
495
625
 
496
626
  ### Datepicker
497
627
 
498
- Calendar popup for date selection:
628
+ Calendar popup for date selection. The application or framework adapter owns the calendar's
629
+ roving-tabindex keyboard behavior, month changes, selection, and open/close state described in the
630
+ accessibility guidelines.
499
631
 
500
632
  ```html
501
633
  <div class="ct-field">
502
634
  <label class="ct-field__label" for="date">Date</label>
503
635
  <div class="ct-datepicker" data-state="open">
504
- <input class="ct-input" id="date" type="text" placeholder="Select date" />
505
- <div class="ct-datepicker__popover" role="dialog" aria-label="Choose date">
636
+ <input class="ct-input" id="date" type="text" placeholder="Select date"
637
+ role="combobox" aria-haspopup="dialog" aria-expanded="true"
638
+ aria-controls="date-popover" />
639
+ <div class="ct-datepicker__popover" id="date-popover"
640
+ role="dialog" aria-modal="true" aria-label="Choose date">
506
641
  <div class="ct-datepicker__header">
507
- <button class="ct-button ct-button--ghost ct-button--icon" aria-label="Previous month">‹</button>
508
- <div class="ct-datepicker__title">March 2026</div>
509
- <button class="ct-button ct-button--ghost ct-button--icon" aria-label="Next month">›</button>
642
+ <button class="ct-button ct-button--ghost ct-button--icon" type="button"
643
+ aria-label="Previous month">‹</button>
644
+ <div class="ct-datepicker__title" aria-live="polite">March 2026</div>
645
+ <button class="ct-button ct-button--ghost ct-button--icon" type="button"
646
+ aria-label="Next month">›</button>
510
647
  </div>
511
- <div class="ct-datepicker__grid">
512
- <div class="ct-datepicker__weekday">Mo</div>
513
- <!-- ... weekdays ... -->
514
- <button class="ct-datepicker__day">1</button>
515
- <button class="ct-datepicker__day" aria-selected="true">15</button>
516
- <!-- ... days ... -->
648
+ <div class="ct-datepicker__grid" role="grid" aria-label="March 2026">
649
+ <div class="ct-datepicker__row" role="row">
650
+ <abbr class="ct-datepicker__weekday" role="columnheader" title="Monday">Mo</abbr>
651
+ <abbr class="ct-datepicker__weekday" role="columnheader" title="Tuesday">Tu</abbr>
652
+ <abbr class="ct-datepicker__weekday" role="columnheader" title="Wednesday">We</abbr>
653
+ <abbr class="ct-datepicker__weekday" role="columnheader" title="Thursday">Th</abbr>
654
+ <abbr class="ct-datepicker__weekday" role="columnheader" title="Friday">Fr</abbr>
655
+ <abbr class="ct-datepicker__weekday" role="columnheader" title="Saturday">Sa</abbr>
656
+ <abbr class="ct-datepicker__weekday" role="columnheader" title="Sunday">Su</abbr>
657
+ </div>
658
+ <div class="ct-datepicker__row" role="row">
659
+ <button class="ct-datepicker__day" type="button" role="gridcell" tabindex="-1" aria-label="9 March 2026">9</button>
660
+ <button class="ct-datepicker__day" type="button" role="gridcell" tabindex="-1" aria-label="10 March 2026">10</button>
661
+ <button class="ct-datepicker__day" type="button" role="gridcell" tabindex="-1" aria-label="11 March 2026">11</button>
662
+ <button class="ct-datepicker__day" type="button" role="gridcell" tabindex="0" aria-selected="true" aria-label="12 March 2026">12</button>
663
+ <button class="ct-datepicker__day" type="button" role="gridcell" tabindex="-1" aria-label="13 March 2026">13</button>
664
+ <button class="ct-datepicker__day" type="button" role="gridcell" tabindex="-1" aria-label="14 March 2026">14</button>
665
+ <button class="ct-datepicker__day" type="button" role="gridcell" tabindex="-1" aria-label="15 March 2026">15</button>
666
+ </div>
667
+ <!-- Render the remaining weeks with the same row/gridcell contract. -->
517
668
  </div>
518
669
  </div>
519
670
  </div>
@@ -525,12 +676,57 @@ Calendar popup for date selection:
525
676
  Hint on hover/focus:
526
677
 
527
678
  ```html
528
- <span class="ct-tooltip" data-state="open" data-side="top">
529
- <button class="ct-button" aria-describedby="tip-1">Hover me</button>
530
- <span class="ct-tooltip__content" role="tooltip" id="tip-1">Short hint</span>
679
+ <span class="ct-tooltip" data-state="closed" data-side="top">
680
+ <button class="ct-button" type="button" aria-describedby="tip-1">Hover me</button>
681
+ <span class="ct-tooltip__content" role="tooltip" id="tip-1" hidden>Short hint</span>
531
682
  </span>
532
683
  ```
533
684
 
685
+ Attach `createTooltipController({ root, trigger, tooltip })` for hover-capable pointers, keyboard focus, touch dismissal, Escape, delays, and cleanup. The tooltip supplements the trigger's accessible name; it must not contain interactive content.
686
+
687
+ ### Navbar
688
+
689
+ Use `ct-navbar--compact` for dense product headers. Mark expendable actions with `data-priority="secondary"`; they hide below the 600px small breakpoint while the menu toggle and primary action remain available. If no mobile toggle exists, primary navigation remains reachable as a horizontal scroll row instead of disappearing.
690
+
691
+ ```html
692
+ <header class="ct-navbar ct-navbar--compact">
693
+ <a class="ct-navbar__brand" href="/">
694
+ <span class="ct-navbar__title">Workspace</span>
695
+ </a>
696
+ <button class="ct-navbar__toggle" type="button"
697
+ aria-label="Open navigation" aria-expanded="false" aria-controls="mobile-nav">
698
+ <span class="ct-navbar__toggle-icon" aria-hidden="true"><span></span><span></span><span></span></span>
699
+ </button>
700
+ <div class="ct-navbar__spacer"></div>
701
+ <div class="ct-navbar__actions">
702
+ <button class="ct-button ct-button--ghost ct-button--icon"
703
+ type="button" aria-label="Search" data-priority="secondary">…</button>
704
+ <button class="ct-button ct-button--ghost ct-button--icon"
705
+ type="button" aria-label="Profile">…</button>
706
+ </div>
707
+ <nav class="ct-navbar__mobile-menu" id="mobile-nav" data-state="closed"
708
+ aria-label="Mobile navigation">…</nav>
709
+ </header>
710
+ ```
711
+
712
+ The CSS controls presentation only. Application code must keep `aria-expanded`, `data-state`, focus movement, Escape handling, and focus return in sync.
713
+
714
+ ### App Shell
715
+
716
+ App Shell V2 is the recommended layout for new applications. The original `.ct-app-shell` remains a supported compatibility layout and is not being rewritten, so existing DOM and selectors keep working. Both variants now share these public override hooks:
717
+
718
+ - `--ct-shell-sidebar-width`
719
+ - `--ct-shell-sidebar-rail-width`
720
+ - `--ct-shell-panel-width`
721
+ - `--ct-shell-motion-duration`
722
+ - `--ct-shell-motion-easing`
723
+
724
+ The historical V1 (`--ct-sidebar-*`) and V2 (`--ct-v2-*`) variables remain compatibility aliases. Prefer the shared hooks in new integrations.
725
+
726
+ ## Touch Target Policy
727
+
728
+ On coarse pointers Construct exposes at least a 44×44 CSS-pixel interaction area for the covered compact controls: default and small buttons, checkbox and radio inputs, default and small switches, sliders, pagination links, and interactive chips. Automated browser contracts verify those targets. This exceeds the WCAG 2.2 AA size threshold in SC 2.5.8 and meets Construct's enhanced target policy; SC 2.5.5 itself is AAA. Visual sizes remain compact on fine pointers. Products remain responsible for custom controls and applicable spacing exceptions.
729
+
534
730
  ## Layout Utilities
535
731
 
536
732
  ### Stack
@@ -549,9 +745,9 @@ Vertical spacing between children:
549
745
  Horizontal spacing with wrapping:
550
746
 
551
747
  ```html
552
- <div class="ct-cluster" style="--ct-cluster-space: var(--space-3);">
553
- <button>Button 1</button>
554
- <button>Button 2</button>
748
+ <div class="ct-cluster" style="--ct-cluster-gap: var(--space-3);">
749
+ <button type="button">Button 1</button>
750
+ <button type="button">Button 2</button>
555
751
  </div>
556
752
  ```
557
753