@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
@@ -0,0 +1,122 @@
1
+ # Headless Behaviors
2
+
3
+ Construct's optional behavior package enhances existing DOM with accessible state machines. It has no framework or runtime dependency and does not render markup or styles.
4
+
5
+ ```js
6
+ import {
7
+ createModalController,
8
+ createTabsController,
9
+ } from '@neuravision/construct/behaviors';
10
+ ```
11
+
12
+ Every factory validates its DOM contract, returns the existing controller when initialized twice for the same root/trigger, and exposes an idempotent `destroy()` method. Call `destroy()` before the owning view unmounts.
13
+
14
+ ## Controllers
15
+
16
+ | Factory | Behavior contract |
17
+ | --- | --- |
18
+ | `createModalController` | Dialog semantics, initial focus, focus trap, Escape/backdrop dismissal, nested overlay ordering, scroll lock, focus return, optional `inertBackground` |
19
+ | `createDrawerController` | Modal drawer behavior with the same focus/dismissal invariants and toggle trigger support |
20
+ | `createTabsController` | Roving tabindex, horizontal/vertical arrows, Home/End, automatic or manual activation |
21
+ | `createToggleGroupController` | Single/multiple pressed state (`aria-pressed`, or `aria-checked` radio semantics for single groups authored with `role="radiogroup"`/`role="radio"`), roving tabindex, orientation-aware arrows, controlled programmatic value |
22
+ | `createDropdownController` | ARIA menu button, first/last focus, arrows, Home/End, typeahead (open and closed), outside/Escape dismissal, menu item selection |
23
+ | `createSelectMenuController` | Button/listbox selection, active option, typeahead, PageUp/PageDown, Tab commits the active option, keyboard opening and dismissal, value synchronization |
24
+ | `createComboboxController` | Editable combobox, virtual listbox focus, optional local filtering, active descendant, Alt+Arrow open/close, IME-safe key handling, announcements, selection |
25
+ | `createTooltipController` | Focus and hover timing, touch toggle, Escape/outside dismissal, described-by wiring |
26
+ | `createPopoverController` | Non-modal popup disclosure, optional focus entry, hover/focus modes, outside/Escape dismissal, focus return |
27
+
28
+ The package includes TypeScript declarations for options, callbacks, controller methods, and event-detail types.
29
+
30
+ ### What the caller still owns (accessible names and live regions)
31
+
32
+ Controllers wire state attributes, but accessible names and announcement targets come from your markup. Missing ones are reported as console warnings in the browser:
33
+
34
+ | Controller | You must provide |
35
+ | --- | --- |
36
+ | Modal / Drawer | `aria-labelledby` or `aria-label` on the dialog element |
37
+ | Dropdown | `aria-label` or `aria-labelledby` on the `role="menu"` element |
38
+ | Toggle group | `aria-label` or `aria-labelledby` on the group/radiogroup/toolbar root |
39
+ | Select menu | A visible label or `aria-labelledby` for the trigger, and labels on options |
40
+ | Combobox | A `<label>` for the input, an `aria-label` on the listbox, and a visually hidden live-region element passed as `options.status` — without it filter results are never announced |
41
+ | Tooltip | A keyboard-focusable trigger (natively focusable or `tabindex="0"`) and text content in the tooltip element |
42
+ | Popover | An accessible name when using `role="dialog"` content |
43
+
44
+ ### Documented deviations from APG
45
+
46
+ - Select menu and dropdown arrow navigation wraps at the ends (APG select-only combobox specifies no wrap); wrapping matches Construct's menu behavior and is intentional.
47
+ - Typing on a closed select menu commits the matching value immediately, mirroring native `<select>` on Windows, instead of opening the listbox first.
48
+
49
+ ## Modal example
50
+
51
+ ```html
52
+ <button id="open-settings" class="ct-button" type="button">Settings</button>
53
+
54
+ <div id="settings-modal" class="ct-modal" data-state="closed" hidden>
55
+ <section class="ct-modal__dialog" aria-labelledby="settings-title">
56
+ <header class="ct-modal__header">
57
+ <h2 id="settings-title">Settings</h2>
58
+ <button class="ct-button ct-button--ghost" type="button" data-ct-dismiss>Close</button>
59
+ </header>
60
+ <div class="ct-modal__body">
61
+ <label class="ct-field">
62
+ <span class="ct-field__label">Display name</span>
63
+ <input class="ct-input" type="text" autofocus />
64
+ </label>
65
+ </div>
66
+ </section>
67
+ </div>
68
+ ```
69
+
70
+ ```js
71
+ import { createModalController } from '@neuravision/construct/behaviors';
72
+
73
+ const modal = document.querySelector('#settings-modal');
74
+ const controller = createModalController({
75
+ container: modal,
76
+ dialog: modal.querySelector('.ct-modal__dialog'),
77
+ trigger: document.querySelector('#open-settings'),
78
+ });
79
+
80
+ // When the owning view is removed:
81
+ controller.destroy();
82
+ ```
83
+
84
+ Buttons with `data-ct-dismiss` are discovered automatically. The caller still owns the accessible name through `aria-labelledby` or `aria-label` and owns the action performed after confirmation.
85
+
86
+ Pass `inertBackground: true` to additionally set `inert` on the container's DOM siblings while the dialog is open — a belt-and-suspenders layer on top of `aria-modal` for legacy screen-reader virtual cursors. Content portaled into the DOM after opening (for example popup layers) is unaffected.
87
+
88
+ ## Tabs example
89
+
90
+ ```js
91
+ import { createTabsController } from '@neuravision/construct/behaviors';
92
+
93
+ const tablist = document.querySelector('[role="tablist"]');
94
+ const tabs = createTabsController({
95
+ tablist,
96
+ activation: 'automatic',
97
+ onSelect({ index }) {
98
+ analytics.track('settings_tab_selected', { index });
99
+ },
100
+ });
101
+ ```
102
+
103
+ The controller can discover tabs from `[role="tab"]` or `[data-ct-tab]`. Panels can be linked with `aria-controls` or passed explicitly. Disabled tabs use native `disabled` or `aria-disabled="true"` and are skipped by keyboard movement.
104
+
105
+ ## State and events
106
+
107
+ Controllers synchronize the attributes consumed by Construct CSS, including `data-state`, `aria-expanded`, `aria-selected`, `aria-pressed`, `aria-activedescendant`, `tabindex`, and `hidden` as applicable.
108
+
109
+ State changes dispatch cancellable `ct:beforeopen`, `ct:beforeclose`, `ct:beforeselect`, or `ct:beforevaluechange` events where relevant. Preventing the event prevents the transition. Successful transitions dispatch the corresponding `ct:open`, `ct:close`, `ct:select`, or `ct:valuechange` event and invoke the typed callback option.
110
+
111
+ Programmatic methods accept a `reason` string. Use stable product-specific reasons when analytics or application policy needs to distinguish pointer, keyboard, routing, and remote-data transitions.
112
+
113
+ ## Lifecycle rules
114
+
115
+ - Initialize after all managed elements exist.
116
+ - Keep one controller per mounted widget; repeated factory calls for the same owner return it.
117
+ - Do not update controller-owned ARIA attributes from a second state machine.
118
+ - Call `destroy()` during framework cleanup. It removes listeners, overlay registrations, timers, scroll locks, and owned temporary attributes.
119
+ - Recreate the controller when the managed item collection changes. `ComboboxController.refresh()` is the exception for filter/result refresh within its fixed managed collection.
120
+ - Prefer native `<details>`, `<select>`, and `<dialog>` where their platform behavior fully meets the product requirement.
121
+
122
+ See the [React example](../examples/react/README.md) for framework lifecycle integration and [Component usage](../components/README.md) for the corresponding markup and CSS classes.
package/docs/fonts.md ADDED
@@ -0,0 +1,70 @@
1
+ # Fonts
2
+
3
+ Construct foundations are network-free. They declare font-family tokens with resilient system fallbacks, but they do not download font files.
4
+
5
+ ## System-font fallback
6
+
7
+ No extra step is required. Import the normal foundation stylesheet and the browser uses a locally installed Lato or JetBrains Mono face when available, then the generated system fallback stack.
8
+
9
+ ```css
10
+ @import '@neuravision/construct/foundations.css';
11
+ ```
12
+
13
+ This is the default for strict Content Security Policy, privacy-sensitive, offline, and embedded applications.
14
+
15
+ ## Optional hosted preset
16
+
17
+ Projects that explicitly accept Google-hosted font requests can import the opt-in preset before foundations:
18
+
19
+ ```css
20
+ @import '@neuravision/construct/fonts.css';
21
+ @import '@neuravision/construct/foundations.css';
22
+ ```
23
+
24
+ The preset requests Lato 400/700/900 and JetBrains Mono 400/500/600/700. Do not use it when the product's privacy or CSP policy forbids third-party font requests.
25
+
26
+ ## Self-hosting
27
+
28
+ Host the licensed font files in the product and define the faces in application CSS. The URLs below are examples; Construct intentionally does not publish third-party font binaries.
29
+
30
+ ```css
31
+ @font-face {
32
+ font-family: 'Lato';
33
+ src: url('/fonts/lato-regular.woff2') format('woff2');
34
+ font-style: normal;
35
+ font-weight: 400;
36
+ font-display: swap;
37
+ }
38
+
39
+ @font-face {
40
+ font-family: 'Lato';
41
+ src: url('/fonts/lato-bold.woff2') format('woff2');
42
+ font-style: normal;
43
+ font-weight: 700;
44
+ font-display: swap;
45
+ }
46
+
47
+ @font-face {
48
+ font-family: 'Lato';
49
+ src: url('/fonts/lato-black.woff2') format('woff2');
50
+ font-style: normal;
51
+ font-weight: 900;
52
+ font-display: swap;
53
+ }
54
+ ```
55
+
56
+ Add the JetBrains Mono faces only if the application renders code or tabular data with the mono token. Preload only the files needed above the fold; indiscriminate preloading delays other critical resources.
57
+
58
+ ## Product typefaces
59
+
60
+ A product can keep Construct's sizing and hierarchy while replacing the families. Override both primitive family variables after the Construct imports; the semantic aliases continue to follow them.
61
+
62
+ ```css
63
+ :root {
64
+ --font-family-brand: 'Company Sans', 'Segoe UI', sans-serif;
65
+ --font-family-text: 'Company Sans', 'Segoe UI', sans-serif;
66
+ --font-family-mono: 'Company Mono', 'SFMono-Regular', Menlo, monospace;
67
+ }
68
+ ```
69
+
70
+ Verify every available weight, text reflow at 200% zoom, fallback metrics during loading, and contrast in all supported themes before release.
@@ -0,0 +1,85 @@
1
+ # Construct Governance and Definition of Done
2
+
3
+ Construct is a reusable product contract, not a collection of project-specific styles. Changes must keep the token, CSS, behavior, accessibility, documentation, and package surfaces aligned.
4
+
5
+ ## Ownership boundaries
6
+
7
+ | Layer | Owns | Must not own |
8
+ | --- | --- | --- |
9
+ | Primitive tokens | Raw scales and values | Product meaning or component selectors |
10
+ | Semantic tokens | Theme-aware intent | Component anatomy |
11
+ | Component tokens | Stable component decisions | DOM event handling |
12
+ | Component CSS | Appearance and attribute-driven states | Focus management or keyboard state machines |
13
+ | Headless behaviors | ARIA synchronization, keyboard interaction, focus, open/close state | Product data or visual styling |
14
+ | Framework adapters | Rendering, lifecycle integration, application data | Private forks of Construct behavior |
15
+
16
+ Product-specific rules belong in the consuming application. A pattern that is useful across products should first be expressed as a general Construct contract and covered by the quality gates below.
17
+
18
+ ## Definition of Done
19
+
20
+ A change is complete only when every applicable item is satisfied.
21
+
22
+ ### Tokens and themes
23
+
24
+ - Source JSON is changed instead of generated `tokens/tokens.*` files.
25
+ - Primitive, semantic, and component-token responsibilities remain distinct.
26
+ - References resolve without cycles or missing targets.
27
+ - Built-in theme structures stay compatible with the light-theme contract.
28
+ - Custom-theme input is schema-valid, only overrides known tokens, and passes its declared contrast checks.
29
+ - Generated CSS, JSON, JavaScript, and TypeScript outputs are rebuilt together before checks and packaging. They are intentionally ignored in this repository and produced in clean CI/prepack runs; source JSON and schemas are the reviewed files.
30
+ - Token additions, removals, or value changes follow the documented versioning policy.
31
+
32
+ ### Components and CSS
33
+
34
+ - The public anatomy, modifiers, sizes, and supported ARIA/data attributes are documented.
35
+ - Default, hover, focus-visible, active, selected/open, disabled, invalid, loading, and empty states are handled where they apply.
36
+ - Covered compact controls follow Construct's 44×44 CSS-pixel coarse-pointer policy; custom controls are tested separately.
37
+ - Motion has a `prefers-reduced-motion` path and essential meaning does not depend on animation.
38
+ - Essential controls and states remain perceivable with `forced-colors: active`.
39
+ - CSS uses Construct tokens, logical properties where direction matters, and no unresolved custom properties.
40
+ - The component works through both the full bundle and its standalone package import.
41
+ - A Storybook story covers representative states and the accessibility addon reports no serious violation.
42
+
43
+ ### Interactive behavior
44
+
45
+ - Native HTML is preferred where it already provides the required semantics.
46
+ - ARIA state and visible state share one source of truth.
47
+ - Keyboard behavior follows the relevant WAI-ARIA Authoring Practices pattern.
48
+ - Focus entry, containment where required, Escape behavior, outside interaction, and focus restoration are explicit.
49
+ - Disabled items are not activated, and composite widgets maintain a valid focus model.
50
+ - Initialization is safe, teardown removes every listener and temporary attribute, and repeated initialization does not leak state.
51
+ - Behavior modules have no framework dependency and expose a documented controller contract.
52
+ - Unit tests cover the keyboard and lifecycle invariants; Storybook verifies the integrated markup and styles.
53
+
54
+ ### Documentation and release
55
+
56
+ - README, component usage, token guidance, and Storybook prose do not overstate what CSS alone provides.
57
+ - A framework example is updated when a public behavior or markup contract changes.
58
+ - The changelog explains consumer-visible additions, migrations, and accessibility implications.
59
+ - `package.json`, its lockfile, examples, issue templates, and visible version labels agree on the release version.
60
+ - Package exports and the packed file list are verified before release.
61
+
62
+ ## Required quality gates
63
+
64
+ Run these from the Construct package root:
65
+
66
+ ```bash
67
+ npm run audit
68
+ npm run build
69
+ npm run check
70
+ npm test
71
+ npm run storybook:build
72
+ npm pack --dry-run
73
+ ```
74
+
75
+ The audit gate includes development tooling and rejects high or critical dependency advisories. Run `npm run build` before the non-mutating `npm run check`, including in a clean checkout where ignored token outputs do not exist. The check verifies generated token and standalone outputs, token/theme validity, contrast declarations, CSS-variable closure, and package export targets. `npm test` verifies headless behavior, Storybook interactions/accessibility, and the real coarse-pointer browser contract.
76
+
77
+ Manual review is still required for visual hierarchy, zoom and reflow, screen-reader announcements, keyboard order, and product-context language. Automated checks support that review; they do not replace it.
78
+
79
+ ## Versioning
80
+
81
+ - **Major:** remove or rename a public token, selector, behavior export, or required markup/attribute contract.
82
+ - **Minor:** add a token, component, variant, optional behavior, theme capability, or backward-compatible state.
83
+ - **Patch:** fix an implementation without changing the documented consumer contract.
84
+
85
+ Deprecations must identify the replacement and removal release. Compatibility files may forward to a canonical implementation, but must not maintain a second divergent copy.
@@ -15,11 +15,12 @@ A consistent, accessible, professional design system for modern web applications
15
15
  - Maintain clear state logic (hover, focus, active, disabled, error)
16
16
  - Use consistent sizes (sm, md, lg) for form controls and buttons
17
17
  - Establish clear hierarchies (Primary vs. Secondary actions)
18
- - Test all components with keyboard navigation (Tab, Shift+Tab, Enter, Space)
18
+ - Test all interactive functionality with keyboard navigation (Tab, Shift+Tab, Enter, Space)
19
19
 
20
20
  ### Don't
21
21
 
22
22
  - Don't rely on color alone for status information without text or icons
23
+ - Don't rely on hue alone to distinguish chart series — use direct labels, ordered legends, and the always-present data-table fallback; several series pairs are near-identical in luminance for color-blind users
23
24
  - Don't create custom controls without ARIA roles and keyboard support
24
25
  - Don't use fixed pixel layouts for content that can grow
25
26
  - Don't use low-contrast text or disabled states that are unreadable
@@ -28,34 +29,47 @@ A consistent, accessible, professional design system for modern web applications
28
29
  ## Accessibility Rules (Minimum)
29
30
 
30
31
  - **Visible Focus**: All interactive elements must have visible focus indication
31
- - **Labels**: Every form input must have a label (explicit `<label>` or `aria-label`)
32
- - **Keyboard Navigation**: Full support for dropdowns, tabs, modals, tooltips, datepickers
32
+ - **Labels**: Every form input must have a label. Prefer a visible `<label>`; use `aria-label` only when a visible label is impossible, and never let it differ from visible text (2.5.3 Label in Name — note that `aria-label` overrides an associated `<label>` when both are present)
33
+ - **Headings**: Keep `h1`–`h6` in order without skipping levels, including inside cards, modals, and shells
34
+ - **Landmarks**: Wrap navigation lists in `<nav>` (also inside `<aside>` sidebars), label repeated landmarks (`aria-label`), and provide a skip link to `main` (see `skip-link.css`)
35
+ - **Truncation**: `nowrap` + ellipsis on essential text (titles, values, labels) needs a way to reach the full text — a tooltip, a wrapping variant, or full text elsewhere on the page
36
+ - **Status colors**: Semantic color (toast, banner, alert, badge) must be paired with an icon or explicit wording; the tinted border alone is decorative and intentionally not contrast-gated
37
+ - **Sticky chrome**: Products using `--sticky`/`--fixed` navbar, toolbar, or banner variants must set `--ct-scroll-offset-top`/`--ct-scroll-offset-bottom` on `:root` so focused elements are never scrolled underneath (2.4.11); the app shells set their own scroll padding automatically
38
+ - **Keyboard Navigation**: Use the tested headless controller for supported composite widgets; framework adapters must implement the documented pattern for others such as Datepicker and Tree
33
39
  - **ARIA**: Use ARIA attributes only where semantic HTML isn't sufficient
34
40
  - **Links**: Links must be distinguishable without color alone (e.g., underline)
35
- - **Contrast**: Text, icons, and focus rings must meet WCAG AA standards
41
+ - **Contrast**: Text, icons, component boundaries, and focus rings must meet applicable WCAG 2.2 AA requirements
36
42
  - **Themes**: Support light, dark, and high-contrast modes; respect user preferences
37
43
  - **Disabled States**: Must be recognizable, but content should remain readable
38
44
  - **Motion**: Respect `prefers-reduced-motion` for animations
39
- - **Live Regions**: Use `aria-live` for toasts and status messages
45
+ - **Live Regions**: Use a polite live region only when a dynamic update must be announced; do not make static status labels noisy
40
46
  - **Modal**: Requires `aria-modal`, `role="dialog"`, and focus trap
47
+ - **Targets**: Covered compact controls follow Construct's 44×44 CSS-pixel coarse-pointer policy; custom controls require their own checks
48
+ - **Forced colors**: Essential focus, boundary, selected, and status meaning must remain perceivable
41
49
 
42
50
  ## Keyboard Patterns for Composite Components
43
51
 
52
+ Construct provides typed controllers for Modal, Drawer, Tabs, Toggle Group, Dropdown, Select Menu,
53
+ Combobox, Tooltip, and Popover through `@neuravision/construct/behaviors`. Use those controllers or a
54
+ framework adapter with equivalent tested semantics; CSS state selectors alone do not implement behavior.
55
+
44
56
  ### Tabs
45
57
  - **Arrow keys**: Move focus between tabs
46
58
  - **Home/End**: Jump to first/last tab
47
59
  - **Enter/Space**: Activate tab
48
60
  - **Implementation**: Use roving tabindex (active tab `tabindex="0"`, others `tabindex="-1"`)
49
61
 
50
- ### Dropdown (Action-List)
62
+ ### Generic disclosure / action list (Popover)
63
+ - **Use for**: Ordinary links, buttons, form controls, or mixed content that should retain native semantics
51
64
  - **Trigger**: Uses `aria-expanded` and `aria-controls`
52
- - **Open**: Focus moves to first item
53
- - **Esc**: Closes and returns focus to trigger
54
- - **Items**: Normal buttons/links in tab order
65
+ - **Focus**: Content remains in its natural Tab order; optional focus entry and focus return depend on the product flow
66
+ - **Behavior**: Use `createPopoverController` or an equivalent adapter for Escape, outside dismissal, and state synchronization
67
+ - **Roles**: Do not add `menu` / `menuitem` roles to generic disclosure content
55
68
 
56
- ### Dropdown (Role=menu)
57
- - **Only use if implementing**: Arrow-key navigation, Home/End, typeahead, and roving tabindex
58
- - Most dropdowns should use Action-List pattern instead
69
+ ### Dropdown action menu (`role="menu"`)
70
+ - **Use for**: A menu button whose popup contains application commands
71
+ - **Behavior**: Use `createDropdownController` for first/last focus, Arrow keys, Home/End, typeahead, roving tabindex, Tab dismissal, Escape, and focus return
72
+ - **Naming**: Give the menu an accessible name with `aria-label` or `aria-labelledby`
59
73
 
60
74
  ### Datepicker
61
75
  - **Arrow keys**: Move by day in calendar grid
@@ -78,9 +92,10 @@ A consistent, accessible, professional design system for modern web applications
78
92
  - **Backdrop**: Click closes drawer
79
93
 
80
94
  ### Tooltip
81
- - **Open**: On hover and focus
95
+ - **Open**: On hover and keyboard focus (the trigger must be focusable)
82
96
  - **Close**: On blur and Esc
83
97
  - **Role**: `role="tooltip"` with `aria-describedby`
98
+ - **Implementation**: The CSS `:hover`/`:focus-within` styles alone do not satisfy WCAG 1.4.13 — Esc dismissal and hover persistence require `createTooltipController` or an equivalent adapter
84
99
 
85
100
  ### Tree (`ct-tree`)
86
101
 
@@ -93,7 +108,7 @@ Hierarchical disclosure following the [WAI-ARIA Tree View pattern](https://www.w
93
108
  - Expandable nodes carry `aria-expanded="true|false"`. Leaf nodes omit the attribute.
94
109
 
95
110
  **Indent**
96
- Set `--ct-level` inline on each `.ct-tree__row` (matches the node's `aria-level`). The bundled JS controller used in Storybook fills it in automatically; framework wrappers should generate it during render.
111
+ Set `--ct-level` inline on each `.ct-tree__row` (matches the node's `aria-level`). The Storybook reference implementation fills it in automatically; framework wrappers should generate it during render.
97
112
 
98
113
  **Keyboard**
99
114
  - `↑` / `↓`: focus previous / next visible row
@@ -103,6 +118,7 @@ Set `--ct-level` inline on each `.ct-tree__row` (matches the node's `aria-level`
103
118
  - `Enter`: activate (consumer-defined; emit `ct-tree:activate`)
104
119
  - `Space`: toggle selection (multi/single) or activate (no selection)
105
120
  - `*`: expand all siblings on the same level
121
+ - `F2`: move focus into the current row's action buttons; Arrow keys move between actions and `Escape` returns to the treeitem
106
122
  - Type-ahead (`A`–`Z`): focus next row whose label starts with the typed prefix; buffer resets after 500 ms
107
123
 
108
124
  **Roving tabindex**
@@ -112,24 +128,24 @@ Exactly one `<li role="treeitem">` carries `tabindex="0"`. All others carry `tab
112
128
  The `.ct-tree__toggle` is a non-focusable `<span aria-hidden="true">`. Expand/collapse is reachable via keyboard through `←`/`→` on the row and via mouse through clicking the chevron. We deliberately avoid making the toggle a `<button>`: a focusable button inside a focusable row violates `aria-hidden-focus` / `nested-interactive`, and the row already provides the keyboard affordance.
113
129
 
114
130
  **Row actions**
115
- Buttons in the `.ct-tree__actions` slot live inside the focusable treeitem. Give them `tabindex="-1"` so the tree exposes a single Tab stop, as required by the WAI-ARIA Tree View pattern. Reach them via mouse, or expose a row-level action hotkey from the consuming framework.
131
+ Buttons in the `.ct-tree__actions` slot live inside the focusable treeitem. Give them `tabindex="-1"` so the tree exposes a single Tab stop, as required by the WAI-ARIA Tree View pattern. Storybook's reference controller uses `F2` to focus the first enabled action, Arrow keys to traverse the action set, and `Escape` to restore focus to the treeitem. Consumer implementations that expose row actions must provide the same reachable keyboard path or an explicitly documented equivalent.
116
132
 
117
133
  **Selection**
118
134
  - `aria-selected="true|false"` on the `<li role="treeitem">` (only when selection is active).
119
- - For multi-selection, the container needs `aria-multiselectable="true"`. The Storybook controller (`attachTree`) sets and tears this down automatically when invoked with `selection: 'multi'`.
135
+ - For multi-selection, the container needs `aria-multiselectable="true"`. Storybook's `attachTree` reference implementation sets and tears this down when invoked with `selection: 'multi'`; it is not exported from `@neuravision/construct/behaviors`.
120
136
  - Construct only styles selection — the consumer decides whether to clear other rows (`single`) or keep them (`multi`).
121
137
 
122
138
  **Async children**
123
139
  Set `aria-busy="true"` on the `<li role="treeitem">` while its children are loading. The chevron switches to a spinner via the existing `ct-spin` keyframe and the toggle becomes non-interactive while busy.
124
140
 
125
141
  **Disabled nodes**
126
- Use `aria-disabled="true"` on the `<li>`. Do **not** use the HTML `disabled` attribute — `treeitem` is not a form control. The Storybook controller skips activation, selection and toggle for disabled nodes; arrow-key navigation still passes through them so screen readers can announce them.
142
+ Use `aria-disabled="true"` on the `<li>`. Do **not** use the HTML `disabled` attribute — `treeitem` is not a form control. Storybook's reference implementation skips activation, selection and toggle for disabled nodes; arrow-key navigation still passes through them so screen readers can announce them.
127
143
 
128
144
  **Orphan state**
129
145
  For sub-nodes whose parent reference is missing in the data set, render them on the root level with the `.ct-tree__node--orphan` modifier. They get a warning-tinted surface and a dashed border so the data inconsistency is visible without breaking the tree.
130
146
 
131
- **Custom events**
132
- The Storybook controller emits four bubbling `CustomEvent`s on the focused treeitem so consumers can wire up application logic without re-implementing the keyboard model:
147
+ **Reference events**
148
+ Storybook's non-public `attachTree` reference implementation emits four bubbling `CustomEvent`s on the focused treeitem. Consumer wrappers may adopt the same event shape, but these events and `attachTree` are examples rather than a published behavior API:
133
149
 
134
150
  | Event | Detail | Fires on |
135
151
  |---|---|---|
@@ -150,6 +166,31 @@ Use these attributes for state management:
150
166
  - **selected**: `aria-selected="true"`
151
167
  - **current**: `aria-current="page"`
152
168
  - **expanded**: `aria-expanded="true|false"`
169
+ - **loading**: `aria-busy="true"`; loading buttons also use `data-loading="true"` and native `disabled`
170
+
171
+ Never use `aria-disabled="true"` alone to block a native button: it announces state but does not suppress
172
+ activation. Use the native `disabled` attribute whenever the HTML element supports it.
173
+
174
+ ## The Datum
175
+
176
+ Construct's signature is one orange reference line — "the datum" — that marks what is focused, active,
177
+ current, or the reference value. The grammar lives in `components/datum.css` (eyebrow, scale, frame,
178
+ metrics), in active leading edges (list, sidebar, card, table rows), in the tab indicator and sorted
179
+ column, and in the chart reference line. Its dimensions are component tokens (`--component-datum-*`).
180
+
181
+ Rules that keep the signature strong and accessible:
182
+
183
+ - **One datum per surface.** Orange marks exactly one thing per view region — the current, active, or
184
+ reference element. Never let two orange edges compete; if everything is highlighted, nothing is.
185
+ - **Orange is never text.** Labels and values stay on `--color-text-*` neutrals; orange is carried by
186
+ non-text ticks, edges, rules, and lines.
187
+ - **Edges are `--color-brand-accent`; meaning-bearing fills are `--color-brand-accent-strong`**
188
+ (validated ≥3:1 against muted/canvas/surface tracks per theme). A decorative rule may use plain
189
+ accent; anything that encodes a value (progress, chart reference) uses strong.
190
+ - **The datum enters, it never pops.** State changes draw the edge in from its origin
191
+ (`--duration-fast` / `--easing-standard`); `prefers-reduced-motion` disables this globally.
192
+ - **Forced colors re-encode edges as borders.** Inset box-shadow edges are dropped by forced colors —
193
+ every new edge needs a `SelectedItem` border fallback.
153
194
 
154
195
  ## Breakpoints & Media Queries
155
196
 
@@ -181,20 +222,26 @@ CSS custom properties cannot be used inside `@media` queries. Use the raw pixel
181
222
  - Always use `max-width: <token - 1>px` to target viewports *below* a breakpoint
182
223
  - Always add a reference comment noting the breakpoint name and token value
183
224
  - Only use values derived from the token system — never arbitrary pixel values
184
- - Current components use two breakpoints: `< sm` (599px) and `< md` (899px)
225
+ - Current components use the `sm`, `md`, and `lg` contracts (599px, 899px, and 1199px maximums). App Shell V1 and V2 use `< md` for compact overlay navigation and `< lg` for rail/panel behavior.
185
226
 
186
227
  ## Fonts
187
228
 
188
- - **Default**: Fonts are loaded via Google Fonts in `foundations.css` (Manrope for display + body, JetBrains Mono for code/data)
189
- - **Self-hosting**: For CSP or privacy requirements, replace Google Fonts import with local `@font-face` rules
190
- - **Weights**: Ensure 400, 500, 600, 700, and 800 are available
229
+ - **Default**: `foundations.css` makes no network request and uses resilient local/system fallback stacks.
230
+ - **Hosted opt-in**: Import `fonts.css` before foundations only when Google-hosted requests satisfy product privacy and CSP policy.
231
+ - **Self-hosting**: Host licensed files in the application; Construct intentionally does not publish third-party binaries.
232
+ - **Lato weights**: Provide 400, 700, and 900. JetBrains Mono uses 400, 500, 600, and 700.
233
+
234
+ See [Fonts](fonts.md) for complete hosted, self-hosted, and product-family examples.
191
235
 
192
236
  ## Governance
193
237
 
194
- - **New components require**: Design tokens, state definitions, accessibility notes, Storybook story
195
- - **Before merge**: Visual QA + keyboard testing + contrast check
238
+ - **New components require**: owning token layer, state definitions, accessibility notes, Storybook coverage, and public import verification
239
+ - **Interactive changes require**: keyboard/focus lifecycle tests and complete controller cleanup
240
+ - **Before merge**: `npm run check`, `npm test`, Storybook build, visual/keyboard/screen-reader review
196
241
  - **Review process**: At least one accessibility review for new interactive components
197
242
 
243
+ The normative checklist is [Governance and Definition of Done](governance.md).
244
+
198
245
  ---
199
246
 
200
247
  **Construct** - Build accessible design constructs
@@ -0,0 +1,20 @@
1
+ # React integration example
2
+
3
+ This example keeps React responsible for rendering and lifecycle while Construct owns visual and interaction contracts.
4
+
5
+ 1. Install `@neuravision/construct` alongside the application's existing React dependencies.
6
+ 2. Import `construct.css` once from the application entry.
7
+ 3. Render `SettingsModal`.
8
+
9
+ The effect creates the controller after the DOM refs exist and returns `controller.destroy` through its cleanup. This is safe with React Strict Mode's development setup/cleanup cycle and prevents listeners, focus state, or scroll locks from surviving an unmount.
10
+
11
+ The controller owns open/closed state, dialog semantics, focus trapping, Escape/backdrop dismissal, and focus return. React still owns form values, validation, persistence, routing, and product feedback. Wire the Save action to application state; do not create a second open/close state machine around the same DOM.
12
+
13
+ For a full component bundle, replace the selective component imports with:
14
+
15
+ ```css
16
+ @import '@neuravision/construct/foundations.css';
17
+ @import '@neuravision/construct/components/components.css';
18
+ ```
19
+
20
+ See [Headless behaviors](../../docs/behaviors.md) for the complete controller API and lifecycle rules.
@@ -0,0 +1,66 @@
1
+ import { useEffect, useRef } from 'react';
2
+
3
+ import { createModalController } from '@neuravision/construct/behaviors';
4
+
5
+ export function SettingsModal() {
6
+ const triggerRef = useRef(null);
7
+ const containerRef = useRef(null);
8
+ const dialogRef = useRef(null);
9
+
10
+ useEffect(() => {
11
+ const controller = createModalController({
12
+ container: containerRef.current,
13
+ dialog: dialogRef.current,
14
+ trigger: triggerRef.current,
15
+ initialFocus: '#display-name',
16
+ });
17
+
18
+ return () => controller.destroy();
19
+ }, []);
20
+
21
+ return (
22
+ <>
23
+ <button ref={triggerRef} className="ct-button" type="button">
24
+ Edit settings
25
+ </button>
26
+
27
+ <div ref={containerRef} className="ct-modal" data-state="closed" hidden>
28
+ <section
29
+ ref={dialogRef}
30
+ className="ct-modal__dialog"
31
+ aria-labelledby="settings-title"
32
+ >
33
+ <header className="ct-modal__header">
34
+ <h2 id="settings-title">Settings</h2>
35
+ <button
36
+ className="ct-button ct-button--ghost ct-button--icon"
37
+ type="button"
38
+ aria-label="Close settings"
39
+ data-ct-dismiss
40
+ >
41
+ <span aria-hidden="true">×</span>
42
+ </button>
43
+ </header>
44
+
45
+ <div className="ct-modal__body">
46
+ <div className="ct-field">
47
+ <label className="ct-field__label" htmlFor="display-name">
48
+ Display name
49
+ </label>
50
+ <input id="display-name" className="ct-input" autoComplete="name" />
51
+ </div>
52
+ </div>
53
+
54
+ <footer className="ct-modal__footer">
55
+ <button className="ct-button ct-button--secondary" type="button" data-ct-dismiss>
56
+ Cancel
57
+ </button>
58
+ <button className="ct-button" type="button">
59
+ Save
60
+ </button>
61
+ </footer>
62
+ </section>
63
+ </div>
64
+ </>
65
+ );
66
+ }
@@ -0,0 +1,5 @@
1
+ @import '@neuravision/construct/foundations.css';
2
+ @import '@neuravision/construct/components/button.css';
3
+ @import '@neuravision/construct/components/field.css';
4
+ @import '@neuravision/construct/components/input.css';
5
+ @import '@neuravision/construct/components/modal.css';
package/fonts.css ADDED
@@ -0,0 +1,10 @@
1
+ /*
2
+ * Optional Construct web-font preset.
3
+ *
4
+ * Foundations never make a network request. Import this file before
5
+ * foundations.css only when Google-hosted fonts fit the product's privacy,
6
+ * CSP, and offline requirements. Self-hosted applications can omit it and
7
+ * provide @font-face rules for "Lato" and "JetBrains Mono" themselves, or
8
+ * override the generated --font-family-* custom properties.
9
+ */
10
+ @import url('https://fonts.googleapis.com/css2?family=Lato:wght@400;700;900&family=JetBrains+Mono:wght@400;500;600;700&display=swap');
package/foundations.css CHANGED
@@ -1,4 +1,3 @@
1
- @import url('https://fonts.googleapis.com/css2?family=Lato:wght@400;700;900&family=JetBrains+Mono:wght@400;500;600;700&display=swap');
2
1
  @import './tokens/tokens.css';
3
2
 
4
3
  :root {
@@ -19,6 +18,12 @@
19
18
  html {
20
19
  height: 100%;
21
20
  text-size-adjust: 100%;
21
+ /* WCAG 2.4.11 Focus Not Obscured contract: sticky/fixed chrome (navbar,
22
+ toolbar, banner) does not know the document, so products publish its
23
+ height by setting --ct-scroll-offset-top/-bottom on :root/html. Scroll
24
+ operations then keep focused or anchored content clear of the bars. */
25
+ scroll-padding-block-start: var(--ct-scroll-offset-top, 0px);
26
+ scroll-padding-block-end: var(--ct-scroll-offset-bottom, 0px);
22
27
  }
23
28
 
24
29
  body {
@@ -231,6 +236,14 @@ button {
231
236
  color: var(--color-text-muted);
232
237
  }
233
238
 
239
+ .ct-truncate {
240
+ display: block;
241
+ min-width: 0;
242
+ overflow: hidden;
243
+ text-overflow: ellipsis;
244
+ white-space: nowrap;
245
+ }
246
+
234
247
  .ct-text-center {
235
248
  text-align: center;
236
249
  }