@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,107 @@
1
+ /* ── Live / Connection Status ──────────────────────────────────── */
2
+
3
+ .ct-status {
4
+ --ct-status-gap: var(--component-status-gap);
5
+ --ct-status-font-size: var(--component-status-font-size);
6
+ --ct-status-icon-size: var(--component-status-icon-size);
7
+ --ct-status-color: var(--color-text-secondary);
8
+ --ct-status-indicator-color: var(--color-border-strong);
9
+
10
+ display: inline-flex;
11
+ align-items: center;
12
+ gap: var(--ct-status-gap);
13
+ color: var(--ct-status-color);
14
+ font-size: var(--ct-status-font-size);
15
+ font-weight: var(--font-weight-medium);
16
+ line-height: var(--line-height-sm);
17
+ }
18
+
19
+ .ct-status__indicator,
20
+ .ct-status__icon {
21
+ width: var(--ct-status-icon-size);
22
+ height: var(--ct-status-icon-size);
23
+ flex: none;
24
+ }
25
+
26
+ .ct-status__indicator {
27
+ border: var(--border-thin) solid currentColor;
28
+ border-radius: var(--radius-round);
29
+ background: var(--ct-status-indicator-color);
30
+ color: var(--ct-status-indicator-color);
31
+ }
32
+
33
+ .ct-status__icon {
34
+ display: inline-flex;
35
+ align-items: center;
36
+ justify-content: center;
37
+ color: var(--ct-status-indicator-color);
38
+ }
39
+
40
+ .ct-status__label {
41
+ min-width: 0;
42
+ }
43
+
44
+ .ct-status__detail {
45
+ color: var(--color-text-muted);
46
+ font-weight: var(--font-weight-regular);
47
+ }
48
+
49
+ /* Labels use surface-safe neutral text because status can appear on any
50
+ surface. Semantic colour is reserved for the non-text indicator, while
51
+ the required visible wording carries meaning without colour. */
52
+
53
+ .ct-status:is([data-status='online'], [data-status='success']) {
54
+ --ct-status-color: var(--color-text-primary);
55
+ --ct-status-indicator-color: var(--color-state-success);
56
+ }
57
+
58
+ .ct-status:is([data-status='connecting'], [data-status='info'], [data-state='loading']) {
59
+ --ct-status-color: var(--color-text-primary);
60
+ --ct-status-indicator-color: var(--color-state-info);
61
+ }
62
+
63
+ .ct-status:is([data-status='degraded'], [data-status='warning']) {
64
+ --ct-status-color: var(--color-text-primary);
65
+ --ct-status-indicator-color: var(--color-state-warning);
66
+ }
67
+
68
+ .ct-status:is([data-status='offline'], [data-status='neutral']) {
69
+ --ct-status-color: var(--color-text-secondary);
70
+ --ct-status-indicator-color: var(--color-border-strong);
71
+ }
72
+
73
+ .ct-status:is([data-status='error'], [data-status='danger'], [data-state='error']) {
74
+ --ct-status-color: var(--color-text-primary);
75
+ --ct-status-indicator-color: var(--color-state-danger);
76
+ }
77
+
78
+ .ct-status:is([aria-busy='true'], [data-status='connecting'], [data-state='loading']) {
79
+ cursor: wait;
80
+ }
81
+
82
+ .ct-status:is([aria-busy='true'], [data-status='connecting'], [data-state='loading']) .ct-status__indicator {
83
+ background: transparent;
84
+ border: var(--border-medium) solid currentColor;
85
+ border-inline-end-color: transparent;
86
+ animation: ct-spin var(--ct-spinner-duration) linear infinite;
87
+ }
88
+
89
+ .ct-status:is([aria-disabled='true'], [data-state='disabled']) {
90
+ opacity: var(--opacity-disabled);
91
+ }
92
+
93
+ @media (prefers-reduced-motion: reduce) {
94
+ .ct-status__indicator {
95
+ animation: none;
96
+ }
97
+ }
98
+
99
+ @media (forced-colors: active) {
100
+ .ct-status__indicator {
101
+ border: var(--border-medium) solid currentColor;
102
+ }
103
+
104
+ .ct-status:is([aria-busy='true'], [data-status='connecting'], [data-state='loading']) .ct-status__indicator {
105
+ border-inline-end-style: dotted;
106
+ }
107
+ }
@@ -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 {
@@ -54,6 +54,9 @@
54
54
  .ct-switch__input:disabled {
55
55
  background: var(--color-border-subtle);
56
56
  cursor: not-allowed;
57
+ /* disabled is encoded via the muted track color; opting out of the global
58
+ :disabled opacity avoids double dimming (same pattern as the slider) */
59
+ opacity: 1;
57
60
  }
58
61
 
59
62
  .ct-switch__input:focus-visible {
@@ -61,13 +64,21 @@
61
64
  outline-offset: 2px;
62
65
  }
63
66
 
64
- /* Invisible touch-target expansion (WCAG 2.5.8: 44×44px minimum) */
67
+ /* WCAG 2.2 SC 2.5.8: the small track is only 20px tall, so every pointer
68
+ context gets at least a 24px-tall hit area. */
69
+ .ct-switch__input::before {
70
+ content: '';
71
+ position: absolute;
72
+ inset-block: calc((var(--ct-switch-height) - 24px) / 2);
73
+ inset-inline: 0;
74
+ }
75
+
76
+ /* Construct 44×44 target policy: meets the size in WCAG 2.2 SC 2.5.5
77
+ (AAA) and exceeds the AA minimum in SC 2.5.8. */
65
78
  @media (pointer: coarse) {
66
79
  .ct-switch__input::before {
67
- content: '';
68
- position: absolute;
69
80
  inset-block: calc((var(--ct-switch-height) - 44px) / 2);
70
- inset-inline: 0;
81
+ inset-inline: calc((var(--ct-switch-width) - 44px) / 2);
71
82
  }
72
83
  }
73
84
 
@@ -80,3 +91,53 @@
80
91
  --ct-switch-width: 52px;
81
92
  --ct-switch-height: 28px;
82
93
  }
94
+
95
+ @media (prefers-reduced-motion: reduce) {
96
+ .ct-switch__input,
97
+ .ct-switch__input::after {
98
+ transition: none;
99
+ }
100
+ }
101
+
102
+ @media (forced-colors: active) {
103
+ /* Track and thumb are shadow/background-drawn; redraw both with
104
+ system-palette borders so on/off state survives without shadows. */
105
+ .ct-switch__input {
106
+ border: 1px solid ButtonText;
107
+ background: Canvas;
108
+ }
109
+
110
+ .ct-switch__input::after {
111
+ border: 1px solid ButtonText;
112
+ background: Canvas;
113
+ box-shadow: none;
114
+ }
115
+
116
+ /* The checked fill must survive forced backgrounds; the extra hover
117
+ selector matches the specificity of the base hover rule. */
118
+ .ct-switch__input:checked,
119
+ .ct-switch__input:checked:not(:disabled):hover {
120
+ background: SelectedItem;
121
+ border-color: ButtonText;
122
+ forced-color-adjust: none;
123
+ }
124
+
125
+ .ct-switch__input:checked::after {
126
+ background: SelectedItemText;
127
+ border-color: SelectedItemText;
128
+ }
129
+
130
+ .ct-switch__input:disabled,
131
+ .ct-switch__input:disabled::after {
132
+ border-color: GrayText;
133
+ }
134
+
135
+ .ct-switch__input:checked:disabled {
136
+ background: GrayText;
137
+ }
138
+
139
+ .ct-switch__input:checked:disabled::after {
140
+ background: Canvas;
141
+ border-color: GrayText;
142
+ }
143
+ }
@@ -97,14 +97,18 @@
97
97
  mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 5'%3E%3Cpath d='M4 5L0 0h8z'/%3E%3C/svg%3E");
98
98
  }
99
99
 
100
- th[aria-sort='ascending'] .ct-table__sort-indicator::before {
100
+ th[aria-sort='ascending'] .ct-table__sort-indicator::before,
101
+ th[aria-sort='descending'] .ct-table__sort-indicator::after {
101
102
  background-color: var(--color-text-secondary);
102
103
  opacity: 1;
103
104
  }
104
105
 
105
- th[aria-sort='descending'] .ct-table__sort-indicator::after {
106
- background-color: var(--color-text-secondary);
107
- opacity: 1;
106
+ /* Direction is shape-encoded: the unsorted state keeps the dimmed
107
+ both-ways glyph, while a sorted column shows only its own arrow so the
108
+ state never relies on colour or opacity alone. */
109
+ th[aria-sort='ascending'] .ct-table__sort-indicator::after,
110
+ th[aria-sort='descending'] .ct-table__sort-indicator::before {
111
+ display: none;
108
112
  }
109
113
 
110
114
  .ct-table__cell--numeric .ct-table__sort {
@@ -137,3 +141,48 @@ th[aria-sort='descending'] .ct-table__sort-indicator::after {
137
141
  font-size: var(--font-size-xs);
138
142
  color: var(--color-text-muted);
139
143
  }
144
+
145
+ /* ── Selected / current rows ──
146
+ The datum: an orange leading edge marks the selected or current row,
147
+ consistent with list, sidebar, and card active states. The edge sits
148
+ on the row's first cell — box-shadow on <tr> is unreliable with
149
+ collapsed borders. Row text stays neutral; only the edge is orange. */
150
+ .ct-table tbody tr > :first-child {
151
+ transition: box-shadow var(--duration-fast) var(--easing-standard);
152
+ }
153
+
154
+ .ct-table tbody tr:is([aria-selected='true'], [aria-current='true'], [data-state='selected']) {
155
+ background: var(--color-bg-muted);
156
+ }
157
+
158
+ .ct-table tbody tr:is([aria-selected='true'], [aria-current='true'], [data-state='selected']) > :first-child {
159
+ box-shadow: inset var(--component-datum-edge-width) 0 0 0 var(--color-brand-accent);
160
+ }
161
+
162
+ /* ── Sorted column ──
163
+ The datum underlines the actively sorted column header, like the tab
164
+ indicator. Direction stays shape-encoded via the arrow glyphs. */
165
+ .ct-table thead th:is([aria-sort='ascending'], [aria-sort='descending']) {
166
+ box-shadow: inset 0 calc(-1 * var(--component-datum-edge-width)) 0 0 var(--color-brand-accent);
167
+ }
168
+
169
+ @media (forced-colors: active) {
170
+ /* Mask-drawn arrows paint via background-color; the sorted selectors
171
+ match the specificity of the full-strength rules above. */
172
+ .ct-table__sort-indicator::before,
173
+ .ct-table__sort-indicator::after,
174
+ th[aria-sort='ascending'] .ct-table__sort-indicator::before,
175
+ th[aria-sort='descending'] .ct-table__sort-indicator::after {
176
+ background-color: CanvasText;
177
+ }
178
+
179
+ /* The datum edges are inset box-shadows, which forced colors drops —
180
+ re-encode them as borders. */
181
+ .ct-table tbody tr:is([aria-selected='true'], [aria-current='true'], [data-state='selected']) > :first-child {
182
+ border-inline-start: var(--component-datum-edge-width) solid SelectedItem;
183
+ }
184
+
185
+ .ct-table thead th:is([aria-sort='ascending'], [aria-sort='descending']) {
186
+ border-block-end: var(--component-datum-edge-width) solid SelectedItem;
187
+ }
188
+ }
@@ -30,19 +30,39 @@
30
30
  font-size: var(--ct-tabs-trigger-font-size);
31
31
  line-height: var(--ct-tabs-trigger-line-height);
32
32
  font-weight: var(--font-weight-medium);
33
+ text-decoration: none;
33
34
  cursor: pointer;
34
35
  transition: color var(--duration-fast) var(--easing-standard),
35
36
  background var(--duration-fast) var(--easing-standard);
36
37
  }
37
38
 
39
+ /* Triggers may be rendered as <a> for router-driven tabs. Neutralize the
40
+ global link styling (underline + link colors) so anchor triggers read
41
+ identically to buttons — the active-tab signal is the indicator, never
42
+ an underline. Deliberately excludes :hover so the shared trigger hover
43
+ rule above keeps winning. */
44
+ a.ct-tabs__trigger,
45
+ a.ct-tabs__trigger:active {
46
+ color: var(--color-text-secondary);
47
+ text-decoration: none;
48
+ }
49
+
38
50
  .ct-tabs__trigger::after {
39
51
  content: '';
40
52
  position: absolute;
41
53
  inset-inline: 0;
42
54
  inset-block-end: 0;
43
55
  height: var(--ct-tabs-indicator-size);
44
- background: transparent;
45
- transition: background var(--duration-fast) var(--easing-standard);
56
+ background: var(--ct-tabs-indicator-color);
57
+ /* The datum draws itself: the indicator grows from its origin on
58
+ activation instead of popping in. Collapsed to zero at rest. */
59
+ transform: scaleX(0);
60
+ transform-origin: 0 50%;
61
+ transition: transform var(--duration-fast) var(--easing-standard);
62
+ }
63
+
64
+ .ct-tabs__trigger:is(:dir(rtl))::after {
65
+ transform-origin: 100% 50%;
46
66
  }
47
67
 
48
68
  @media (hover: hover) {
@@ -65,7 +85,7 @@
65
85
 
66
86
  .ct-tabs__trigger[aria-selected='true']::after,
67
87
  .ct-tabs__trigger[data-state='active']::after {
68
- background: var(--ct-tabs-indicator-color);
88
+ transform: scale(1);
69
89
  }
70
90
 
71
91
  .ct-tabs__trigger:disabled,
@@ -114,6 +134,13 @@
114
134
  inset-block: 0;
115
135
  width: var(--ct-tabs-indicator-size);
116
136
  height: auto;
137
+ /* Vertical tabs draw the indicator from the block start. */
138
+ transform: scaleY(0);
139
+ transform-origin: 50% 0;
140
+ }
141
+
142
+ .ct-tabs--vertical .ct-tabs__trigger:is([aria-selected='true'], [data-state='active'])::after {
143
+ transform: scale(1);
117
144
  }
118
145
 
119
146
  /* Pill variant */
@@ -139,9 +166,34 @@
139
166
  box-shadow: var(--shadow-sm);
140
167
  }
141
168
 
169
+ /* Construct 44×44 target policy (WCAG 2.2 SC 2.5.5 is AAA) */
170
+
171
+ @media (pointer: coarse) {
172
+ .ct-tabs__trigger {
173
+ min-block-size: 44px;
174
+ }
175
+ }
176
+
142
177
  @media (prefers-reduced-motion: reduce) {
143
178
  .ct-tabs__trigger,
144
179
  .ct-tabs__trigger::after {
145
180
  transition: none;
146
181
  }
147
182
  }
183
+
184
+ @media (forced-colors: active) {
185
+ /* The active indicator is background-drawn and must survive forced
186
+ backgrounds. */
187
+ .ct-tabs__trigger[aria-selected='true']::after,
188
+ .ct-tabs__trigger[data-state='active']::after {
189
+ background: SelectedItem;
190
+ forced-color-adjust: none;
191
+ }
192
+
193
+ .ct-tabs--pill .ct-tabs__trigger[aria-selected='true'],
194
+ .ct-tabs--pill .ct-tabs__trigger[data-state='active'] {
195
+ background: SelectedItem;
196
+ color: SelectedItemText;
197
+ forced-color-adjust: none;
198
+ }
199
+ }
@@ -23,12 +23,21 @@
23
23
  box-shadow: var(--shadow-md);
24
24
  color: var(--color-text-primary);
25
25
  transition: opacity var(--duration-fast) var(--easing-standard),
26
- transform var(--duration-fast) var(--easing-standard);
26
+ transform var(--duration-fast) var(--easing-standard),
27
+ visibility 0s linear 0s;
27
28
  }
28
29
 
30
+ /* Delayed visibility (drawer.css pattern) removes the faded-out toast
31
+ from the accessibility tree and tab order once the fade completes;
32
+ reopening restores it instantly via the base transition. */
29
33
  .ct-toast[data-state='closed'] {
30
34
  opacity: 0;
31
35
  transform: translateY(8px);
36
+ visibility: hidden;
37
+ pointer-events: none;
38
+ transition: opacity var(--duration-fast) var(--easing-standard),
39
+ transform var(--duration-fast) var(--easing-standard),
40
+ visibility 0s linear var(--duration-fast);
32
41
  }
33
42
 
34
43
  .ct-toast__title {
@@ -24,11 +24,7 @@
24
24
  overflow-x: auto;
25
25
  overflow-y: hidden;
26
26
  -webkit-overflow-scrolling: touch;
27
- scrollbar-width: none;
28
- }
29
-
30
- .ct-toggle-group::-webkit-scrollbar {
31
- display: none;
27
+ scrollbar-width: thin;
32
28
  }
33
29
 
34
30
  /* ── Item ── */
@@ -194,3 +190,18 @@
194
190
  transition: none;
195
191
  }
196
192
  }
193
+
194
+ /* ── Forced colors ── */
195
+
196
+ @media (forced-colors: active) {
197
+ /* The pressed fill must survive forced backgrounds; the outlined
198
+ selectors match the specificity of that variant's override. */
199
+ .ct-toggle-group__item[aria-pressed='true'],
200
+ .ct-toggle-group__item[aria-checked='true'],
201
+ .ct-toggle-group--outlined .ct-toggle-group__item[aria-pressed='true'],
202
+ .ct-toggle-group--outlined .ct-toggle-group__item[aria-checked='true'] {
203
+ background: SelectedItem;
204
+ color: SelectedItemText;
205
+ forced-color-adjust: none;
206
+ }
207
+ }
@@ -15,7 +15,8 @@
15
15
  display: flex;
16
16
  align-items: center;
17
17
  gap: var(--ct-toolbar-gap);
18
- height: var(--ct-toolbar-height);
18
+ /* min-height, not height: the bar must grow with text scaling. */
19
+ min-height: var(--ct-toolbar-height);
19
20
  padding: 0 var(--ct-toolbar-padding-x);
20
21
  background: var(--color-bg-elevated);
21
22
  border-bottom: var(--border-thin) solid var(--color-border-subtle);
@@ -153,8 +154,15 @@
153
154
  --ct-toolbar-actions-gap: var(--space-2);
154
155
  }
155
156
 
157
+ /* WCAG 2.2 SC 2.5.8: dense padding alone cannot guarantee 24px links. */
158
+ .ct-toolbar--dense .ct-toolbar__nav-link {
159
+ min-height: 24px;
160
+ }
161
+
156
162
  /* ── Sticky ── */
157
163
 
164
+ /* Consumers: set --ct-scroll-offset-top on :root (see foundations.css) so
165
+ scrolled-to content is not obscured behind the bar (WCAG 2.4.11). */
158
166
  .ct-toolbar--sticky {
159
167
  position: sticky;
160
168
  inset-block-start: 0;
@@ -213,6 +221,14 @@
213
221
  }
214
222
  }
215
223
 
224
+ /* ── Construct 44×44 target policy (WCAG 2.2 SC 2.5.5 is AAA) ── */
225
+
226
+ @media (pointer: coarse) {
227
+ .ct-toolbar__nav-link {
228
+ min-height: 44px;
229
+ }
230
+ }
231
+
216
232
  /* ── Reduced Motion ── */
217
233
 
218
234
  @media (prefers-reduced-motion: reduce) {
@@ -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;
@@ -118,10 +118,25 @@
118
118
  border-radius: var(--ct-tree-toggle-radius);
119
119
  color: var(--ct-tree-toggle-color);
120
120
  cursor: pointer;
121
+ position: relative;
121
122
  transition: color var(--duration-fast) var(--easing-standard),
122
123
  background var(--duration-fast) var(--easing-standard);
123
124
  }
124
125
 
126
+ /* WCAG 2.2 SC 2.5.8: the visual chevron stays 1rem; the pointer target
127
+ does not. The hit area overlaps the row, mirroring checkbox.css. */
128
+ .ct-tree__toggle::before {
129
+ content: '';
130
+ position: absolute;
131
+ inset: calc((var(--ct-tree-toggle-size) - 24px) / 2);
132
+ }
133
+
134
+ @media (pointer: coarse) {
135
+ .ct-tree__toggle::before {
136
+ inset: calc((var(--ct-tree-toggle-size) - 44px) / 2);
137
+ }
138
+ }
139
+
125
140
  @media (hover: hover) {
126
141
  .ct-tree__toggle:hover {
127
142
  color: var(--ct-tree-toggle-color-hover);
@@ -194,6 +209,13 @@
194
209
  --ct-tree-actions-opacity: 1;
195
210
  }
196
211
 
212
+ /* Touch surfaces have no hover to reveal row actions — keep them visible. */
213
+ @media (hover: none) {
214
+ .ct-tree {
215
+ --ct-tree-actions-opacity: 1;
216
+ }
217
+ }
218
+
197
219
  /* ── Selection ── */
198
220
 
199
221
  .ct-tree__node[aria-selected='true'] > .ct-tree__row,
@@ -277,6 +299,14 @@
277
299
  background: var(--color-bg-elevated);
278
300
  }
279
301
 
302
+ /* ── Construct 44×44 target policy (WCAG 2.2 SC 2.5.5 is AAA) ── */
303
+
304
+ @media (pointer: coarse) {
305
+ .ct-tree__row {
306
+ min-height: 44px;
307
+ }
308
+ }
309
+
280
310
  /* ── Reduced motion ── */
281
311
 
282
312
  @media (prefers-reduced-motion: reduce) {
@@ -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.