@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,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,28 @@ 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
27
+ - `aria-current="true"` for the current table row; `aria-selected="true"`/`data-state="selected"` for selected rows in grid contexts
23
28
  - `aria-selected="true"` for active tabs and selected datepicker days
29
+ - `aria-sort="ascending|descending"` on the actively sorted column header
24
30
  - `aria-disabled="true"` when disabled but not using native `disabled`
25
31
 
32
+ 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`.
33
+
26
34
  ## Keyboard + ARIA Requirements
27
35
 
28
- When building framework wrappers, implement these keyboard patterns:
36
+ Use `@neuravision/construct/behaviors` where a controller is available (including Tabs, menu Dropdown,
37
+ Modal, and Tooltip), or implement an equivalent tested framework adapter. CSS alone does not provide
38
+ these keyboard patterns:
29
39
 
30
40
  - **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.
41
+ - **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.
42
+ - **Generic disclosure**: When popup content contains ordinary links or controls in natural Tab order, use the Popover pattern rather than adding menu roles.
33
43
  - **Datepicker**: Arrow keys move by day, PageUp/PageDown switches months, Home/End jumps within week, Enter/Space selects, Esc closes.
34
44
  - **Modal**: Focus trap, initial focus inside dialog, Esc closes, focus returns to trigger.
35
45
  - **Tooltip**: Opens on hover/focus, closes on blur/Esc, uses `role="tooltip"` with `aria-describedby`.
@@ -41,31 +51,39 @@ When building framework wrappers, implement these keyboard patterns:
41
51
  Basic button with variants and sizes:
42
52
 
43
53
  ```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>
54
+ <button class="ct-button" type="button">Primary</button>
55
+ <button class="ct-button ct-button--secondary" type="button">Secondary</button>
56
+ <button class="ct-button ct-button--ghost" type="button">Ghost</button>
57
+ <button class="ct-button ct-button--outline" type="button">Outline</button>
58
+ <button class="ct-button ct-button--danger" type="button">Danger</button>
59
+ <button class="ct-button ct-button--accent" type="button">Accent</button>
60
+ <button class="ct-button ct-button--link" type="button">Link</button>
51
61
 
52
62
  <!-- 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>
63
+ <button class="ct-button ct-button--sm" type="button">Small</button>
64
+ <button class="ct-button" type="button">Medium</button>
65
+ <button class="ct-button ct-button--lg" type="button">Large</button>
56
66
 
57
67
  <!-- With icon -->
58
- <button class="ct-button">
68
+ <button class="ct-button" type="button">
59
69
  <span class="ct-button__icon" aria-hidden="true">+</span>
60
70
  Add item
61
71
  </button>
62
72
 
63
73
  <!-- Icon only -->
64
- <button class="ct-button ct-button--icon" aria-label="Settings">
74
+ <button class="ct-button ct-button--icon" type="button" aria-label="Settings">
65
75
  <span class="ct-button__icon" aria-hidden="true">⚙</span>
66
76
  </button>
77
+
78
+ <!-- Loading: stable label, decorative loader, native interaction lock -->
79
+ <button class="ct-button" type="button" aria-busy="true" data-loading="true" disabled>
80
+ <span class="ct-button__content">Save changes</span>
81
+ <span class="ct-button__loader" aria-hidden="true"></span>
82
+ </button>
67
83
  ```
68
84
 
85
+ 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`.
86
+
69
87
  ### Field + Input
70
88
 
71
89
  Form field with label, input, hint, and error states:
@@ -73,15 +91,17 @@ Form field with label, input, hint, and error states:
73
91
  ```html
74
92
  <div class="ct-field">
75
93
  <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>
94
+ <input class="ct-input" id="email" type="email" placeholder="name@company.com"
95
+ aria-describedby="email-hint" />
96
+ <div class="ct-field__hint" id="email-hint">We will not share this.</div>
78
97
  </div>
79
98
 
80
99
  <!-- Error state -->
81
100
  <div class="ct-field ct-field--error">
82
101
  <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>
102
+ <input class="ct-input" id="name" type="text" aria-invalid="true"
103
+ aria-describedby="name-error" />
104
+ <div class="ct-field__error" id="name-error">Name is required.</div>
85
105
  </div>
86
106
 
87
107
  <!-- With icon -->
@@ -122,7 +142,7 @@ Multi-line text input:
122
142
 
123
143
  ### Checkbox
124
144
 
125
- Single checkbox or checkbox group:
145
+ Single checkbox or a labelled checkbox group:
126
146
 
127
147
  ```html
128
148
  <label class="ct-check">
@@ -130,10 +150,17 @@ Single checkbox or checkbox group:
130
150
  <span class="ct-check__label">Remember me</span>
131
151
  </label>
132
152
 
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>
153
+ <fieldset>
154
+ <legend class="ct-field__label">Notifications</legend>
155
+ <label class="ct-check">
156
+ <input class="ct-check__input" type="checkbox" checked />
157
+ <span class="ct-check__label">Send weekly reports</span>
158
+ </label>
159
+ <label class="ct-check">
160
+ <input class="ct-check__input" type="checkbox" />
161
+ <span class="ct-check__label">Send security alerts</span>
162
+ </label>
163
+ </fieldset>
137
164
  ```
138
165
 
139
166
  ### Radio
@@ -141,15 +168,18 @@ Single checkbox or checkbox group:
141
168
  Radio button group:
142
169
 
143
170
  ```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>
171
+ <fieldset>
172
+ <legend class="ct-field__label">Plan</legend>
173
+ <label class="ct-radio">
174
+ <input class="ct-radio__input" type="radio" name="plan" checked />
175
+ <span class="ct-radio__label">Standard</span>
176
+ </label>
148
177
 
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>
178
+ <label class="ct-radio">
179
+ <input class="ct-radio__input" type="radio" name="plan" />
180
+ <span class="ct-radio__label">Premium</span>
181
+ </label>
182
+ </fieldset>
153
183
  ```
154
184
 
155
185
  ### Switch
@@ -171,7 +201,7 @@ Content card with header, body, and footer:
171
201
  <section class="ct-card">
172
202
  <div class="ct-card__header">
173
203
  <h3>Team</h3>
174
- <button class="ct-button ct-button--ghost">Edit</button>
204
+ <button class="ct-button ct-button--ghost" type="button">Edit</button>
175
205
  </div>
176
206
  <div class="ct-card__body">
177
207
  <p>Shared ownership and clear permissions.</p>
@@ -179,11 +209,42 @@ Content card with header, body, and footer:
179
209
  </div>
180
210
  <div class="ct-card__footer">
181
211
  <span class="ct-muted">12 members</span>
182
- <button class="ct-button ct-button--secondary">Open</button>
212
+ <button class="ct-button ct-button--secondary" type="button">Open</button>
183
213
  </div>
184
214
  </section>
185
215
  ```
186
216
 
217
+ ### Datum Primitives
218
+
219
+ The reference-line grammar from `components/datum.css` — Construct's orange signature as reusable
220
+ structural vocabulary. All dimensions are component tokens (`--component-datum-*`).
221
+
222
+ ```html
223
+ <div class="ct-eyebrow-set">
224
+ <p class="ct-eyebrow ct-eyebrow--numbered">Foundations</p>
225
+ </div>
226
+
227
+ <div class="ct-datum-scale" role="presentation"></div>
228
+
229
+ <div class="ct-datum-scale ct-datum-scale--progress" role="progressbar"
230
+ aria-label="Upload" aria-valuenow="60" aria-valuemin="0" aria-valuemax="100"
231
+ style="--ct-datum-scale-progress: 60%;"></div>
232
+
233
+ <figure class="ct-datum-frame">…</figure>
234
+ ```
235
+
236
+ - `ct-eyebrow` - Mono kicker with a leading datum tick; `--numbered` adds drawing-set sheet numbers
237
+ (scope with `ct-eyebrow-set`)
238
+ - `ct-datum-scale` - Measured ruler line; decorative, mark it `role="presentation"`
239
+ - `ct-datum-scale--progress` - The origin segment reports progress via `--ct-datum-scale-progress`;
240
+ pair with `role="progressbar"` and `aria-value*`
241
+ - `ct-datum-frame` - Corner registration mark for figures, code samples, and media
242
+ - `ct-card--datum`, active leading edges, the tab indicator, sorted table columns, and
243
+ `ct-chart__datum-line` speak the same grammar
244
+
245
+ Orange is never text; edges use `brand-accent`, meaning-bearing fills use `brand-accent-strong`.
246
+ One datum per surface — see `docs/guidelines.md`.
247
+
187
248
  ### Table
188
249
 
189
250
  Basic data table with variants:
@@ -217,7 +278,15 @@ Basic data table with variants:
217
278
  Variants:
218
279
  - `ct-table--striped` - Alternating row backgrounds
219
280
  - `ct-table--compact` - Reduced padding
220
- - `ct-table--hover` - Row hover effect
281
+
282
+ Rows receive a visual hover affordance automatically on hover-capable pointers; no hover modifier exists.
283
+
284
+ States (the datum grammar):
285
+ - `tr[aria-current="true"]`, `tr[aria-selected="true"]`, or `tr[data-state="selected"]` marks the
286
+ current/selected row with a muted background and the orange leading edge. Use `aria-current` on
287
+ native tables; `aria-selected` is only valid on rows inside `role="grid"`/`role="treegrid"`.
288
+ - `th[aria-sort="ascending|descending"]` underlines the actively sorted column header with the datum;
289
+ sort direction stays shape-encoded via the `ct-table__sort-indicator` arrows.
221
290
 
222
291
  ### Data Table
223
292
 
@@ -231,8 +300,8 @@ Complex data table with header, toolbar, filters, and actions:
231
300
  <span class="ct-data-table__meta">24 total</span>
232
301
  </div>
233
302
  <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>
303
+ <button class="ct-button ct-button--secondary ct-button--sm" type="button">Export</button>
304
+ <button class="ct-button ct-button--sm" type="button">New project</button>
236
305
  </div>
237
306
  </div>
238
307
 
@@ -260,29 +329,89 @@ Complex data table with header, toolbar, filters, and actions:
260
329
  </div>
261
330
  ```
262
331
 
332
+ ### Metric / Stat
333
+
334
+ 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.
335
+
336
+ ```html
337
+ <dl class="ct-metrics" aria-label="Service metrics">
338
+ <div class="ct-metric" data-status="success">
339
+ <dt class="ct-metric__label">Availability</dt>
340
+ <dd class="ct-metric__value">99.98<small>%</small>
341
+ <span class="ct-metric__meta">
342
+ <span>Last 30 days</span>
343
+ <span class="ct-metric__status">Healthy</span>
344
+ </span>
345
+ </dd>
346
+ </div>
347
+ </dl>
348
+ ```
349
+
350
+ 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"`.
351
+
352
+ ### Segmented Meter
353
+
354
+ 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.
355
+
356
+ ```html
357
+ <div class="ct-meter" role="group"
358
+ aria-labelledby="delivery-label" aria-describedby="delivery-legend">
359
+ <div class="ct-meter__header">
360
+ <span class="ct-meter__label" id="delivery-label">Delivery status</span>
361
+ <span class="ct-meter__summary">100 items</span>
362
+ </div>
363
+ <div class="ct-meter__track" aria-hidden="true">
364
+ <span class="ct-meter__segment" data-variant="success"
365
+ style="--ct-meter-segment-size: 62%;"></span>
366
+ <span class="ct-meter__segment" data-variant="warning"
367
+ style="--ct-meter-segment-size: 23%;"></span>
368
+ <span class="ct-meter__segment" data-variant="danger"
369
+ style="--ct-meter-segment-size: 15%;"></span>
370
+ </div>
371
+ <ul class="ct-meter__legend" id="delivery-legend">
372
+ <li class="ct-meter__legend-item">
373
+ <span class="ct-meter__marker" data-variant="success" aria-hidden="true"></span>
374
+ <span>Completed</span><span class="ct-meter__legend-value">62%</span>
375
+ </li>
376
+ <li class="ct-meter__legend-item">
377
+ <span class="ct-meter__marker" data-variant="warning" aria-hidden="true"></span>
378
+ <span>In review</span><span class="ct-meter__legend-value">23%</span>
379
+ </li>
380
+ <li class="ct-meter__legend-item">
381
+ <span class="ct-meter__marker" data-variant="danger" aria-hidden="true"></span>
382
+ <span>Blocked</span><span class="ct-meter__legend-value">15%</span>
383
+ </li>
384
+ </ul>
385
+ </div>
386
+ ```
387
+
263
388
  ### Modal
264
389
 
265
390
  Dialog with backdrop:
266
391
 
267
392
  ```html
268
- <div class="ct-modal" data-state="open" role="dialog" aria-modal="true" aria-labelledby="modal-title">
269
- <div class="ct-modal__dialog">
393
+ <div class="ct-modal" data-state="closed" hidden>
394
+ <div class="ct-modal__dialog" role="dialog" aria-modal="true" aria-labelledby="modal-title">
270
395
  <div class="ct-modal__header">
271
396
  <h2 id="modal-title">Invite team</h2>
272
- <button class="ct-button ct-button--ghost" aria-label="Close">×</button>
397
+ <button class="ct-button ct-button--ghost" type="button"
398
+ aria-label="Close" data-ct-dismiss>×</button>
273
399
  </div>
274
400
  <div class="ct-modal__body">
275
401
  <p>Send an invite to a new team member.</p>
276
402
  <!-- form content -->
277
403
  </div>
278
404
  <div class="ct-modal__footer">
279
- <button class="ct-button ct-button--secondary">Cancel</button>
280
- <button class="ct-button">Send</button>
405
+ <button class="ct-button ct-button--secondary" type="button" data-ct-dismiss>Cancel</button>
406
+ <button class="ct-button" type="button">Send</button>
281
407
  </div>
282
408
  </div>
283
409
  </div>
284
410
  ```
285
411
 
412
+ Attach `createModalController({ container, dialog, trigger })` to synchronize state, trap focus,
413
+ handle Escape/backdrop dismissal, and restore focus. See [Headless behaviors](../docs/behaviors.md).
414
+
286
415
  ### Toast
287
416
 
288
417
  Notification with variants:
@@ -292,12 +421,12 @@ Notification with variants:
292
421
  <div class="ct-toast" data-variant="success" data-state="open">
293
422
  <div class="ct-toast__title">Saved</div>
294
423
  <div class="ct-toast__description">Your changes were saved.</div>
295
- <button class="ct-button ct-button--ghost">Undo</button>
424
+ <button class="ct-button ct-button--ghost" type="button">Undo</button>
296
425
  </div>
297
426
  </div>
298
427
  ```
299
428
 
300
- Variants: `success`, `error`, `info`, `warning`
429
+ Variants: `success`, `danger`, `info`, `warning`
301
430
 
302
431
  ### Alert / Banner
303
432
 
@@ -310,7 +439,7 @@ Inline feedback message with variants:
310
439
  <div class="ct-alert__title">Action required</div>
311
440
  <div class="ct-alert__description">Please review the pending changes.</div>
312
441
  <div class="ct-alert__actions">
313
- <button class="ct-button ct-button--secondary ct-button--sm">Review</button>
442
+ <button class="ct-button ct-button--secondary ct-button--sm" type="button">Review</button>
314
443
  </div>
315
444
  </div>
316
445
  </div>
@@ -330,6 +459,21 @@ Small status indicators with optional icon or dot:
330
459
  </span>
331
460
  ```
332
461
 
462
+ ### Live / Connection Status
463
+
464
+ 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.
465
+
466
+ ```html
467
+ <span class="ct-status" data-status="connecting" data-state="loading"
468
+ role="status" aria-live="polite" aria-atomic="true" aria-busy="true">
469
+ <span class="ct-status__indicator" aria-hidden="true"></span>
470
+ <span class="ct-status__label">Connecting</span>
471
+ <span class="ct-status__detail">Retrying</span>
472
+ </span>
473
+ ```
474
+
475
+ 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.
476
+
333
477
  ### Chip / Tag
334
478
 
335
479
  Compact tags for filters or metadata:
@@ -348,7 +492,9 @@ Compact tags for filters or metadata:
348
492
 
349
493
  ### File Upload / Drag & Drop
350
494
 
351
- Drag area + file list pattern. Use `data-state="dragover"` and `aria-invalid="true"` when needed.
495
+ Drag area + file list pattern. Use `data-state="dragover"` while dragging. For validation, put
496
+ `aria-invalid="true"` on both the styled dropzone and the native file input, then connect visible
497
+ error text to the input with `aria-describedby`.
352
498
 
353
499
  ```html
354
500
  <div class="ct-file-upload">
@@ -368,7 +514,18 @@ Drag area + file list pattern. Use `data-state="dragover"` and `aria-invalid="tr
368
514
  <span class="ct-badge ct-badge--success">Uploaded</span>
369
515
  </li>
370
516
  </ul>
371
- <div class="ct-file-upload__error">File is too large.</div>
517
+ </div>
518
+
519
+ <!-- Invalid state -->
520
+ <div class="ct-file-upload">
521
+ <label class="ct-file-upload__dropzone" for="files-invalid" aria-invalid="true">
522
+ <input class="ct-file-upload__input" id="files-invalid" type="file"
523
+ aria-invalid="true" aria-describedby="files-error" />
524
+ <div class="ct-file-upload__title">Choose another file</div>
525
+ <div class="ct-file-upload__hint">PDF, DOCX up to 10MB</div>
526
+ <span class="ct-button ct-button--secondary ct-button--sm">Browse files</span>
527
+ </label>
528
+ <div class="ct-file-upload__error" id="files-error">File is too large.</div>
372
529
  </div>
373
530
  ```
374
531
 
@@ -377,7 +534,9 @@ Drag area + file list pattern. Use `data-state="dragover"` and `aria-invalid="tr
377
534
  Spinner for inline loading and overlay for blocking states:
378
535
 
379
536
  ```html
380
- <div class="ct-spinner ct-spinner--sm" role="status" aria-live="polite"></div>
537
+ <div class="ct-spinner ct-spinner--sm" role="status" aria-live="polite">
538
+ <span class="ct-sr-only">Loading…</span>
539
+ </div>
381
540
 
382
541
  <div class="ct-card" style="position: relative; min-height: 180px;">
383
542
  <div class="ct-loading-overlay" data-state="active" aria-busy="true">
@@ -394,8 +553,9 @@ Spinner for inline loading and overlay for blocking states:
394
553
  Use the modal base with a confirmation layout:
395
554
 
396
555
  ```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">
556
+ <div class="ct-modal ct-modal--confirmation" data-state="open">
557
+ <div class="ct-modal__dialog" role="dialog" aria-modal="true"
558
+ aria-labelledby="confirm-title" aria-describedby="confirm-desc">
399
559
  <div class="ct-modal__body">
400
560
  <div class="ct-confirmation" data-variant="danger">
401
561
  <div class="ct-confirmation__icon" aria-hidden="true">!</div>
@@ -406,8 +566,8 @@ Use the modal base with a confirmation layout:
406
566
  </div>
407
567
  </div>
408
568
  <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>
569
+ <button class="ct-button ct-button--secondary" type="button" data-ct-dismiss>Cancel</button>
570
+ <button class="ct-button ct-button--danger" type="button">Delete</button>
411
571
  </div>
412
572
  </div>
413
573
  </div>
@@ -432,31 +592,41 @@ Tab navigation with panels:
432
592
  ```html
433
593
  <div class="ct-tabs">
434
594
  <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>
595
+ <button class="ct-tabs__trigger" type="button" role="tab" aria-selected="true" aria-controls="panel-1" id="tab-1" tabindex="0">Overview</button>
596
+ <button class="ct-tabs__trigger" type="button" role="tab" aria-selected="false" aria-controls="panel-2" id="tab-2" tabindex="-1">Settings</button>
437
597
  </div>
438
598
  <div class="ct-tabs__panel" role="tabpanel" id="panel-1" aria-labelledby="tab-1">
439
599
  <p>Panel content</p>
440
600
  </div>
601
+ <div class="ct-tabs__panel" role="tabpanel" id="panel-2" aria-labelledby="tab-2" hidden>
602
+ <p>Settings content</p>
603
+ </div>
441
604
  </div>
442
605
  ```
443
606
 
607
+ Attach `createTabsController({ tablist })` unless the consuming framework already owns the equivalent roving-focus and selection state machine.
608
+
444
609
  ### Dropdown
445
610
 
446
611
  Action menu:
447
612
 
448
613
  ```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>
614
+ <div class="ct-dropdown" data-state="closed">
615
+ <button class="ct-button ct-button--secondary ct-dropdown__trigger" type="button"
616
+ id="actions-trigger" aria-expanded="false" aria-haspopup="menu"
617
+ aria-controls="actions-menu">Actions</button>
618
+ <div class="ct-dropdown__menu" id="actions-menu" role="menu"
619
+ aria-labelledby="actions-trigger" hidden>
620
+ <button class="ct-dropdown__item" type="button" role="menuitem" tabindex="-1">Edit</button>
621
+ <button class="ct-dropdown__item" type="button" role="menuitem" tabindex="-1">Duplicate</button>
454
622
  <div class="ct-dropdown__separator" role="separator"></div>
455
- <button class="ct-dropdown__item" type="button">Delete</button>
623
+ <button class="ct-dropdown__item" type="button" role="menuitem" tabindex="-1">Delete</button>
456
624
  </div>
457
625
  </div>
458
626
  ```
459
627
 
628
+ 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`.
629
+
460
630
  ### Pagination
461
631
 
462
632
  Page navigation:
@@ -480,14 +650,14 @@ Path navigation:
480
650
  <ol class="ct-breadcrumbs__list">
481
651
  <li class="ct-breadcrumbs__item">
482
652
  <a class="ct-breadcrumbs__link" href="/">Home</a>
483
- <span class="ct-breadcrumbs__separator">/</span>
653
+ <span class="ct-breadcrumbs__separator" aria-hidden="true">/</span>
484
654
  </li>
485
655
  <li class="ct-breadcrumbs__item">
486
656
  <a class="ct-breadcrumbs__link" href="/projects">Projects</a>
487
- <span class="ct-breadcrumbs__separator">/</span>
657
+ <span class="ct-breadcrumbs__separator" aria-hidden="true">/</span>
488
658
  </li>
489
659
  <li class="ct-breadcrumbs__item">
490
- <span class="ct-breadcrumbs__current">Alpha</span>
660
+ <span class="ct-breadcrumbs__current" aria-current="page">Alpha</span>
491
661
  </li>
492
662
  </ol>
493
663
  </nav>
@@ -495,25 +665,46 @@ Path navigation:
495
665
 
496
666
  ### Datepicker
497
667
 
498
- Calendar popup for date selection:
668
+ Calendar popup for date selection. The application or framework adapter owns the calendar's
669
+ roving-tabindex keyboard behavior, month changes, selection, and open/close state described in the
670
+ accessibility guidelines.
499
671
 
500
672
  ```html
501
673
  <div class="ct-field">
502
674
  <label class="ct-field__label" for="date">Date</label>
503
675
  <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">
676
+ <input class="ct-input" id="date" type="text" placeholder="Select date"
677
+ role="combobox" aria-haspopup="dialog" aria-expanded="true"
678
+ aria-controls="date-popover" />
679
+ <div class="ct-datepicker__popover" id="date-popover"
680
+ role="dialog" aria-modal="true" aria-label="Choose date">
506
681
  <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>
682
+ <button class="ct-button ct-button--ghost ct-button--icon" type="button"
683
+ aria-label="Previous month">‹</button>
684
+ <div class="ct-datepicker__title" aria-live="polite">March 2026</div>
685
+ <button class="ct-button ct-button--ghost ct-button--icon" type="button"
686
+ aria-label="Next month">›</button>
510
687
  </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 ... -->
688
+ <div class="ct-datepicker__grid" role="grid" aria-label="March 2026">
689
+ <div class="ct-datepicker__row" role="row">
690
+ <abbr class="ct-datepicker__weekday" role="columnheader" title="Monday">Mo</abbr>
691
+ <abbr class="ct-datepicker__weekday" role="columnheader" title="Tuesday">Tu</abbr>
692
+ <abbr class="ct-datepicker__weekday" role="columnheader" title="Wednesday">We</abbr>
693
+ <abbr class="ct-datepicker__weekday" role="columnheader" title="Thursday">Th</abbr>
694
+ <abbr class="ct-datepicker__weekday" role="columnheader" title="Friday">Fr</abbr>
695
+ <abbr class="ct-datepicker__weekday" role="columnheader" title="Saturday">Sa</abbr>
696
+ <abbr class="ct-datepicker__weekday" role="columnheader" title="Sunday">Su</abbr>
697
+ </div>
698
+ <div class="ct-datepicker__row" role="row">
699
+ <button class="ct-datepicker__day" type="button" role="gridcell" tabindex="-1" aria-label="9 March 2026">9</button>
700
+ <button class="ct-datepicker__day" type="button" role="gridcell" tabindex="-1" aria-label="10 March 2026">10</button>
701
+ <button class="ct-datepicker__day" type="button" role="gridcell" tabindex="-1" aria-label="11 March 2026">11</button>
702
+ <button class="ct-datepicker__day" type="button" role="gridcell" tabindex="0" aria-selected="true" aria-label="12 March 2026">12</button>
703
+ <button class="ct-datepicker__day" type="button" role="gridcell" tabindex="-1" aria-label="13 March 2026">13</button>
704
+ <button class="ct-datepicker__day" type="button" role="gridcell" tabindex="-1" aria-label="14 March 2026">14</button>
705
+ <button class="ct-datepicker__day" type="button" role="gridcell" tabindex="-1" aria-label="15 March 2026">15</button>
706
+ </div>
707
+ <!-- Render the remaining weeks with the same row/gridcell contract. -->
517
708
  </div>
518
709
  </div>
519
710
  </div>
@@ -525,12 +716,57 @@ Calendar popup for date selection:
525
716
  Hint on hover/focus:
526
717
 
527
718
  ```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>
719
+ <span class="ct-tooltip" data-state="closed" data-side="top">
720
+ <button class="ct-button" type="button" aria-describedby="tip-1">Hover me</button>
721
+ <span class="ct-tooltip__content" role="tooltip" id="tip-1" hidden>Short hint</span>
531
722
  </span>
532
723
  ```
533
724
 
725
+ 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.
726
+
727
+ ### Navbar
728
+
729
+ 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.
730
+
731
+ ```html
732
+ <header class="ct-navbar ct-navbar--compact">
733
+ <a class="ct-navbar__brand" href="/">
734
+ <span class="ct-navbar__title">Workspace</span>
735
+ </a>
736
+ <button class="ct-navbar__toggle" type="button"
737
+ aria-label="Open navigation" aria-expanded="false" aria-controls="mobile-nav">
738
+ <span class="ct-navbar__toggle-icon" aria-hidden="true"><span></span><span></span><span></span></span>
739
+ </button>
740
+ <div class="ct-navbar__spacer"></div>
741
+ <div class="ct-navbar__actions">
742
+ <button class="ct-button ct-button--ghost ct-button--icon"
743
+ type="button" aria-label="Search" data-priority="secondary">…</button>
744
+ <button class="ct-button ct-button--ghost ct-button--icon"
745
+ type="button" aria-label="Profile">…</button>
746
+ </div>
747
+ <nav class="ct-navbar__mobile-menu" id="mobile-nav" data-state="closed"
748
+ aria-label="Mobile navigation">…</nav>
749
+ </header>
750
+ ```
751
+
752
+ The CSS controls presentation only. Application code must keep `aria-expanded`, `data-state`, focus movement, Escape handling, and focus return in sync.
753
+
754
+ ### App Shell
755
+
756
+ 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:
757
+
758
+ - `--ct-shell-sidebar-width`
759
+ - `--ct-shell-sidebar-rail-width`
760
+ - `--ct-shell-panel-width`
761
+ - `--ct-shell-motion-duration`
762
+ - `--ct-shell-motion-easing`
763
+
764
+ The historical V1 (`--ct-sidebar-*`) and V2 (`--ct-v2-*`) variables remain compatibility aliases. Prefer the shared hooks in new integrations.
765
+
766
+ ## Touch Target Policy
767
+
768
+ 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.
769
+
534
770
  ## Layout Utilities
535
771
 
536
772
  ### Stack
@@ -549,9 +785,9 @@ Vertical spacing between children:
549
785
  Horizontal spacing with wrapping:
550
786
 
551
787
  ```html
552
- <div class="ct-cluster" style="--ct-cluster-space: var(--space-3);">
553
- <button>Button 1</button>
554
- <button>Button 2</button>
788
+ <div class="ct-cluster" style="--ct-cluster-gap: var(--space-3);">
789
+ <button type="button">Button 1</button>
790
+ <button type="button">Button 2</button>
555
791
  </div>
556
792
  ```
557
793