@neuravision/construct 2.0.3 → 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
@@ -43,7 +43,7 @@
43
43
 
44
44
  @media (hover: hover) {
45
45
  .ct-switch__input:not(:disabled):hover {
46
- background: color-mix(in srgb, var(--color-border-strong) 85%, black);
46
+ background: color-mix(in srgb, var(--color-border-strong) 85%, var(--color-text-primary));
47
47
  }
48
48
 
49
49
  .ct-switch__input:checked:not(:disabled):hover {
@@ -61,13 +61,14 @@
61
61
  outline-offset: 2px;
62
62
  }
63
63
 
64
- /* Invisible touch-target expansion (WCAG 2.5.8: 44×44px minimum) */
64
+ /* Construct 44×44 target policy: meets the size in WCAG 2.2 SC 2.5.5
65
+ (AAA) and exceeds the AA minimum in SC 2.5.8. */
65
66
  @media (pointer: coarse) {
66
67
  .ct-switch__input::before {
67
68
  content: '';
68
69
  position: absolute;
69
70
  inset-block: calc((var(--ct-switch-height) - 44px) / 2);
70
- inset-inline: 0;
71
+ inset-inline: calc((var(--ct-switch-width) - 44px) / 2);
71
72
  }
72
73
  }
73
74
 
@@ -41,16 +41,20 @@
41
41
  --_tt-open: 1;
42
42
  opacity: 1;
43
43
  visibility: visible;
44
+ pointer-events: auto;
44
45
  transition: opacity var(--duration-fast) var(--easing-standard),
45
46
  transform var(--duration-fast) var(--easing-standard),
46
47
  visibility 0s linear 0s;
47
48
  }
48
49
 
49
50
  @media (hover: hover) {
51
+ .ct-tooltip[data-state='open'] .ct-tooltip__content,
52
+ .ct-tooltip:focus-within .ct-tooltip__content,
50
53
  .ct-tooltip:hover .ct-tooltip__content {
51
54
  --_tt-open: 1;
52
55
  opacity: 1;
53
56
  visibility: visible;
57
+ pointer-events: auto;
54
58
  transition: opacity var(--duration-fast) var(--easing-standard),
55
59
  transform var(--duration-fast) var(--easing-standard),
56
60
  visibility 0s linear 0s;
@@ -0,0 +1,79 @@
1
+ # Construct Architecture
2
+
3
+ Construct separates reusable design decisions, visual component contracts, interaction behavior, and product integration. That separation lets a vanilla page and a framework application share the same public contract without coupling Construct to either runtime.
4
+
5
+ ```text
6
+ primitive tokens
7
+ │
8
+ ▼
9
+ semantic tokens ─── built-in/custom theme scopes
10
+ │
11
+ ▼
12
+ component tokens ── stable component decisions
13
+ │
14
+ ├──────────────► component CSS ──► DOM + ARIA/data state
15
+ │ ▲
16
+ └──────────────► JS/TS token exports │
17
+ │
18
+ optional headless behaviors ─────────────────┘
19
+ │
20
+ framework adapter / application data ───────┘
21
+ ```
22
+
23
+ ## Token layers
24
+
25
+ 1. **Primitives** are raw, context-free scales such as palette steps, spacing, type sizes, and motion durations.
26
+ 2. **Semantic tokens** express intent such as canvas, primary text, danger surface, or focus ring. Themes override this layer.
27
+ 3. **Component tokens** express stable component decisions such as button height or control border. They reference semantic or primitive tokens and are re-declared in theme scopes so nested container themes resolve correctly.
28
+
29
+ Source files live in `tokens/*.json`; `tokens/tokens.css`, `.json`, `.js`, and `.ts` are generated. Schemas in `schemas/` define the accepted source and product-theme contracts.
30
+
31
+ ## CSS layers and entry points
32
+
33
+ | Entry point | Purpose |
34
+ | --- | --- |
35
+ | `@neuravision/construct/tokens/tokens.css` | Tokens only |
36
+ | `@neuravision/construct/foundations.css` | Network-free reset, typography, and layout utilities |
37
+ | `@neuravision/construct/fonts.css` | Optional hosted-font request; import only with explicit privacy/CSP approval |
38
+ | `@neuravision/construct/components/components.css` | Complete component bundle |
39
+ | `@neuravision/construct/components/<name>.css` | Generated standalone component entry with tokens and shared component contracts |
40
+ | `@neuravision/construct/components/core.css` | Tokens and shared component contracts without a concrete component |
41
+
42
+ Files under `standalone/` are generated package adapters. Authored component CSS remains in `components/`; it is never copied into a second implementation. This keeps full-bundle and selective-import behavior on one source.
43
+
44
+ ## Behavior layer
45
+
46
+ Construct CSS styles ARIA and `data-*` state; it cannot implement a focus trap, roving tabindex, typeahead, Escape handling, or focus restoration. The optional `@neuravision/construct/behaviors` ESM entry provides dependency-free controllers for those state machines.
47
+
48
+ Controllers enhance caller-owned DOM. They synchronize semantics, emit cancellable `ct:before*` and post-change `ct:*` events where relevant, expose programmatic methods, and return a `destroy()` lifecycle. Rendering and product data stay in the application or framework adapter.
49
+
50
+ Importing the behavior entry has no side effect. A controller starts only when its factory is called, and destroying it restores the attributes it owned.
51
+
52
+ ## Product themes
53
+
54
+ A product theme is a validated override, not a copy of the generated stylesheet. It names a built-in base, supplies known semantic and/or component paths, and declares any product-specific contrast pairs.
55
+
56
+ ```bash
57
+ npx construct-theme \
58
+ --theme ./brand.theme.json \
59
+ --out-dir ./src/generated/construct
60
+ ```
61
+
62
+ The compiler validates schemas, unknown paths, references, complete resolved shapes, selector/value safety, duplicate selectors, typed CSS values, and contrast before writing the same CSS/JSON/JS/TS formats as the built-in build. Keep the theme input under product source control and treat its output as generated.
63
+
64
+ The consuming stylesheet must load the generated `tokens.css` after foundations and all full or standalone Construct component entries. Those entries include the built-in token sheet; putting the generated theme last gives its root and scoped declarations deterministic precedence. Product-specific overrides may follow it.
65
+
66
+ ## Framework boundary
67
+
68
+ Framework adapters own references, mount/unmount lifecycle, controlled application state, and rendering. They should initialize a controller after the relevant DOM exists and call `destroy()` during cleanup. They must not reproduce the controller's keyboard or focus logic in parallel.
69
+
70
+ Construct does not own routing, remote data, authorization, analytics, localization, or product-specific responsive decisions. Components expose explicit slots, attributes, tokens, and controller callbacks so those concerns can remain at the product layer.
71
+
72
+ ## Generated and compatibility files
73
+
74
+ - Edit token source JSON and run the build; never hand-edit `tokens/tokens.*`.
75
+ - Edit `components/index.css` and authored component files; regenerate `standalone/`.
76
+ - A compatibility entry may import a canonical implementation, but duplicated component implementations are not maintained.
77
+ - Public exports are checked against real files before packaging.
78
+
79
+ See [Governance and Definition of Done](governance.md) for change ownership, review requirements, and release gates.
@@ -0,0 +1,101 @@
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 |
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, roving tabindex, orientation-aware arrows, controlled programmatic value |
22
+ | `createDropdownController` | ARIA menu button, first/last focus, arrows, Home/End, typeahead, outside/Escape dismissal, menu item selection |
23
+ | `createSelectMenuController` | Button/listbox selection, active option, typeahead, keyboard opening and dismissal, value synchronization |
24
+ | `createComboboxController` | Editable combobox, virtual listbox focus, optional local filtering, active descendant, 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
+ ## Modal example
31
+
32
+ ```html
33
+ <button id="open-settings" class="ct-button" type="button">Settings</button>
34
+
35
+ <div id="settings-modal" class="ct-modal" data-state="closed" hidden>
36
+ <section class="ct-modal__dialog" aria-labelledby="settings-title">
37
+ <header class="ct-modal__header">
38
+ <h2 id="settings-title">Settings</h2>
39
+ <button class="ct-button ct-button--ghost" type="button" data-ct-dismiss>Close</button>
40
+ </header>
41
+ <div class="ct-modal__body">
42
+ <label class="ct-field">
43
+ <span class="ct-field__label">Display name</span>
44
+ <input class="ct-input" type="text" autofocus />
45
+ </label>
46
+ </div>
47
+ </section>
48
+ </div>
49
+ ```
50
+
51
+ ```js
52
+ import { createModalController } from '@neuravision/construct/behaviors';
53
+
54
+ const modal = document.querySelector('#settings-modal');
55
+ const controller = createModalController({
56
+ container: modal,
57
+ dialog: modal.querySelector('.ct-modal__dialog'),
58
+ trigger: document.querySelector('#open-settings'),
59
+ });
60
+
61
+ // When the owning view is removed:
62
+ controller.destroy();
63
+ ```
64
+
65
+ 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.
66
+
67
+ ## Tabs example
68
+
69
+ ```js
70
+ import { createTabsController } from '@neuravision/construct/behaviors';
71
+
72
+ const tablist = document.querySelector('[role="tablist"]');
73
+ const tabs = createTabsController({
74
+ tablist,
75
+ activation: 'automatic',
76
+ onSelect({ index }) {
77
+ analytics.track('settings_tab_selected', { index });
78
+ },
79
+ });
80
+ ```
81
+
82
+ 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.
83
+
84
+ ## State and events
85
+
86
+ Controllers synchronize the attributes consumed by Construct CSS, including `data-state`, `aria-expanded`, `aria-selected`, `aria-pressed`, `aria-activedescendant`, `tabindex`, and `hidden` as applicable.
87
+
88
+ 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.
89
+
90
+ 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.
91
+
92
+ ## Lifecycle rules
93
+
94
+ - Initialize after all managed elements exist.
95
+ - Keep one controller per mounted widget; repeated factory calls for the same owner return it.
96
+ - Do not update controller-owned ARIA attributes from a second state machine.
97
+ - Call `destroy()` during framework cleanup. It removes listeners, overlay registrations, timers, scroll locks, and owned temporary attributes.
98
+ - Recreate the controller when the managed item collection changes. `ComboboxController.refresh()` is the exception for filter/result refresh within its fixed managed collection.
99
+ - Prefer native `<details>`, `<select>`, and `<dialog>` where their platform behavior fully meets the product requirement.
100
+
101
+ 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,7 +15,7 @@ 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
 
@@ -29,33 +29,41 @@ A consistent, accessible, professional design system for modern web applications
29
29
 
30
30
  - **Visible Focus**: All interactive elements must have visible focus indication
31
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
+ - **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
33
  - **ARIA**: Use ARIA attributes only where semantic HTML isn't sufficient
34
34
  - **Links**: Links must be distinguishable without color alone (e.g., underline)
35
- - **Contrast**: Text, icons, and focus rings must meet WCAG AA standards
35
+ - **Contrast**: Text, icons, component boundaries, and focus rings must meet applicable WCAG 2.2 AA requirements
36
36
  - **Themes**: Support light, dark, and high-contrast modes; respect user preferences
37
37
  - **Disabled States**: Must be recognizable, but content should remain readable
38
38
  - **Motion**: Respect `prefers-reduced-motion` for animations
39
- - **Live Regions**: Use `aria-live` for toasts and status messages
39
+ - **Live Regions**: Use a polite live region only when a dynamic update must be announced; do not make static status labels noisy
40
40
  - **Modal**: Requires `aria-modal`, `role="dialog"`, and focus trap
41
+ - **Targets**: Covered compact controls follow Construct's 44×44 CSS-pixel coarse-pointer policy; custom controls require their own checks
42
+ - **Forced colors**: Essential focus, boundary, selected, and status meaning must remain perceivable
41
43
 
42
44
  ## Keyboard Patterns for Composite Components
43
45
 
46
+ Construct provides typed controllers for Modal, Drawer, Tabs, Toggle Group, Dropdown, Select Menu,
47
+ Combobox, Tooltip, and Popover through `@neuravision/construct/behaviors`. Use those controllers or a
48
+ framework adapter with equivalent tested semantics; CSS state selectors alone do not implement behavior.
49
+
44
50
  ### Tabs
45
51
  - **Arrow keys**: Move focus between tabs
46
52
  - **Home/End**: Jump to first/last tab
47
53
  - **Enter/Space**: Activate tab
48
54
  - **Implementation**: Use roving tabindex (active tab `tabindex="0"`, others `tabindex="-1"`)
49
55
 
50
- ### Dropdown (Action-List)
56
+ ### Generic disclosure / action list (Popover)
57
+ - **Use for**: Ordinary links, buttons, form controls, or mixed content that should retain native semantics
51
58
  - **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
59
+ - **Focus**: Content remains in its natural Tab order; optional focus entry and focus return depend on the product flow
60
+ - **Behavior**: Use `createPopoverController` or an equivalent adapter for Escape, outside dismissal, and state synchronization
61
+ - **Roles**: Do not add `menu` / `menuitem` roles to generic disclosure content
55
62
 
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
63
+ ### Dropdown action menu (`role="menu"`)
64
+ - **Use for**: A menu button whose popup contains application commands
65
+ - **Behavior**: Use `createDropdownController` for first/last focus, Arrow keys, Home/End, typeahead, roving tabindex, Tab dismissal, Escape, and focus return
66
+ - **Naming**: Give the menu an accessible name with `aria-label` or `aria-labelledby`
59
67
 
60
68
  ### Datepicker
61
69
  - **Arrow keys**: Move by day in calendar grid
@@ -93,7 +101,7 @@ Hierarchical disclosure following the [WAI-ARIA Tree View pattern](https://www.w
93
101
  - Expandable nodes carry `aria-expanded="true|false"`. Leaf nodes omit the attribute.
94
102
 
95
103
  **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.
104
+ 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
105
 
98
106
  **Keyboard**
99
107
  - `↑` / `↓`: focus previous / next visible row
@@ -103,6 +111,7 @@ Set `--ct-level` inline on each `.ct-tree__row` (matches the node's `aria-level`
103
111
  - `Enter`: activate (consumer-defined; emit `ct-tree:activate`)
104
112
  - `Space`: toggle selection (multi/single) or activate (no selection)
105
113
  - `*`: expand all siblings on the same level
114
+ - `F2`: move focus into the current row's action buttons; Arrow keys move between actions and `Escape` returns to the treeitem
106
115
  - Type-ahead (`A`–`Z`): focus next row whose label starts with the typed prefix; buffer resets after 500 ms
107
116
 
108
117
  **Roving tabindex**
@@ -112,24 +121,24 @@ Exactly one `<li role="treeitem">` carries `tabindex="0"`. All others carry `tab
112
121
  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
122
 
114
123
  **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.
124
+ 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
125
 
117
126
  **Selection**
118
127
  - `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'`.
128
+ - 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
129
  - Construct only styles selection — the consumer decides whether to clear other rows (`single`) or keep them (`multi`).
121
130
 
122
131
  **Async children**
123
132
  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
133
 
125
134
  **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.
135
+ 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
136
 
128
137
  **Orphan state**
129
138
  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
139
 
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:
140
+ **Reference events**
141
+ 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
142
 
134
143
  | Event | Detail | Fires on |
135
144
  |---|---|---|
@@ -150,6 +159,10 @@ Use these attributes for state management:
150
159
  - **selected**: `aria-selected="true"`
151
160
  - **current**: `aria-current="page"`
152
161
  - **expanded**: `aria-expanded="true|false"`
162
+ - **loading**: `aria-busy="true"`; loading buttons also use `data-loading="true"` and native `disabled`
163
+
164
+ Never use `aria-disabled="true"` alone to block a native button: it announces state but does not suppress
165
+ activation. Use the native `disabled` attribute whenever the HTML element supports it.
153
166
 
154
167
  ## Breakpoints & Media Queries
155
168
 
@@ -181,20 +194,26 @@ CSS custom properties cannot be used inside `@media` queries. Use the raw pixel
181
194
  - Always use `max-width: <token - 1>px` to target viewports *below* a breakpoint
182
195
  - Always add a reference comment noting the breakpoint name and token value
183
196
  - Only use values derived from the token system — never arbitrary pixel values
184
- - Current components use two breakpoints: `< sm` (599px) and `< md` (899px)
197
+ - 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
198
 
186
199
  ## Fonts
187
200
 
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
201
+ - **Default**: `foundations.css` makes no network request and uses resilient local/system fallback stacks.
202
+ - **Hosted opt-in**: Import `fonts.css` before foundations only when Google-hosted requests satisfy product privacy and CSP policy.
203
+ - **Self-hosting**: Host licensed files in the application; Construct intentionally does not publish third-party binaries.
204
+ - **Lato weights**: Provide 400, 700, and 900. JetBrains Mono uses 400, 500, 600, and 700.
205
+
206
+ See [Fonts](fonts.md) for complete hosted, self-hosted, and product-family examples.
191
207
 
192
208
  ## Governance
193
209
 
194
- - **New components require**: Design tokens, state definitions, accessibility notes, Storybook story
195
- - **Before merge**: Visual QA + keyboard testing + contrast check
210
+ - **New components require**: owning token layer, state definitions, accessibility notes, Storybook coverage, and public import verification
211
+ - **Interactive changes require**: keyboard/focus lifecycle tests and complete controller cleanup
212
+ - **Before merge**: `npm run check`, `npm test`, Storybook build, visual/keyboard/screen-reader review
196
213
  - **Review process**: At least one accessibility review for new interactive components
197
214
 
215
+ The normative checklist is [Governance and Definition of Done](governance.md).
216
+
198
217
  ---
199
218
 
200
219
  **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');