@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.
- package/CHANGELOG.md +318 -0
- package/README.md +158 -149
- package/behaviors/combobox.js +486 -0
- package/behaviors/drawer.js +18 -0
- package/behaviors/dropdown.js +310 -0
- package/behaviors/index.d.ts +335 -0
- package/behaviors/index.js +9 -0
- package/behaviors/internal/dialog-controller.js +421 -0
- package/behaviors/internal/dom.js +408 -0
- package/behaviors/internal/overlay.js +241 -0
- package/behaviors/internal/popup-layer.js +190 -0
- package/behaviors/modal.js +17 -0
- package/behaviors/popover.js +278 -0
- package/behaviors/select-menu.js +344 -0
- package/behaviors/tabs.js +241 -0
- package/behaviors/toggle-group.js +266 -0
- package/behaviors/tooltip.js +257 -0
- package/components/README.md +319 -83
- package/components/_shared.css +22 -10
- package/components/_shell-shared.css +20 -4
- package/components/accordion.css +8 -0
- package/components/app-shell-v2.css +28 -14
- package/components/app-shell.css +30 -14
- package/components/badge.css +15 -0
- package/components/banner.css +32 -1
- package/components/button.css +103 -15
- package/components/card.css +67 -8
- package/components/chart.css +26 -0
- package/components/checkbox.css +35 -2
- package/components/chip.css +30 -2
- package/components/combobox.css +21 -0
- package/components/datepicker.css +58 -9
- package/components/datum.css +277 -0
- package/components/dropdown.css +23 -3
- package/components/field.css +6 -4
- package/components/file-upload.css +1 -1
- package/components/index.css +5 -0
- package/components/list.css +28 -2
- package/components/meter.css +182 -0
- package/components/navbar.css +102 -4
- package/components/pagination.css +18 -0
- package/components/progress-bar.css +22 -1
- package/components/select-menu.css +8 -0
- package/components/select.css +8 -0
- package/components/sidebar.css +15 -2
- package/components/skip-link.css +6 -2
- package/components/slider.css +7 -1
- package/components/status.css +107 -0
- package/components/switch.css +66 -5
- package/components/table.css +53 -4
- package/components/tabs.css +55 -3
- package/components/toast.css +10 -1
- package/components/toggle-group.css +16 -5
- package/components/toolbar.css +17 -1
- package/components/tooltip.css +4 -0
- package/components/tree.css +30 -0
- package/docs/architecture.md +79 -0
- package/docs/behaviors.md +122 -0
- package/docs/fonts.md +70 -0
- package/docs/governance.md +85 -0
- package/docs/guidelines.md +72 -25
- package/examples/react/README.md +20 -0
- package/examples/react/SettingsModal.jsx +66 -0
- package/examples/react/construct.css +5 -0
- package/fonts.css +10 -0
- package/foundations.css +14 -1
- package/package.json +76 -21
- package/schemas/components.schema.json +122 -0
- package/schemas/examples/theme.example.json +27 -0
- package/schemas/primitives.schema.json +135 -0
- package/schemas/semantic.schema.json +224 -0
- package/schemas/theme.schema.json +63 -0
- package/scripts/build-tokens.mjs +1499 -0
- package/scripts/construct-theme.mjs +43 -0
- package/scripts/validate-tokens.mjs +25 -0
- package/standalone/accordion.css +3 -0
- package/standalone/alert.css +3 -0
- package/standalone/app-shell-v2.css +4 -0
- package/standalone/app-shell.css +4 -0
- package/standalone/avatar.css +3 -0
- package/standalone/badge.css +3 -0
- package/standalone/banner.css +3 -0
- package/standalone/breadcrumbs.css +3 -0
- package/standalone/button.css +3 -0
- package/standalone/card.css +3 -0
- package/standalone/chart.css +3 -0
- package/standalone/checkbox.css +3 -0
- package/standalone/chip.css +3 -0
- package/standalone/combobox.css +3 -0
- package/standalone/core.css +4 -0
- package/standalone/data-table.css +3 -0
- package/standalone/datepicker.css +3 -0
- package/standalone/datum.css +3 -0
- package/standalone/divider.css +3 -0
- package/standalone/drawer.css +3 -0
- package/standalone/dropdown.css +3 -0
- package/standalone/empty-state.css +3 -0
- package/standalone/field.css +3 -0
- package/standalone/file-upload.css +3 -0
- package/standalone/icon.css +3 -0
- package/standalone/input.css +3 -0
- package/standalone/list.css +3 -0
- package/standalone/meter.css +3 -0
- package/standalone/modal.css +3 -0
- package/standalone/navbar.css +3 -0
- package/standalone/pagination.css +3 -0
- package/standalone/popover.css +3 -0
- package/standalone/progress-bar.css +3 -0
- package/standalone/select-menu.css +3 -0
- package/standalone/select.css +3 -0
- package/standalone/sidebar.css +3 -0
- package/standalone/skeleton.css +3 -0
- package/standalone/skip-link.css +3 -0
- package/standalone/slider.css +3 -0
- package/standalone/spinner.css +3 -0
- package/standalone/status.css +3 -0
- package/standalone/switch.css +3 -0
- package/standalone/table.css +3 -0
- package/standalone/tabs.css +3 -0
- package/standalone/textarea.css +3 -0
- package/standalone/toast.css +3 -0
- package/standalone/toggle-group.css +3 -0
- package/standalone/toolbar.css +3 -0
- package/standalone/tooltip.css +3 -0
- package/standalone/tree.css +3 -0
- package/themes/README.md +37 -0
- package/themes/walnut.theme.json +106 -0
- package/tokens/README.md +135 -123
- package/tokens/components.json +64 -0
- package/tokens/primitives.json +268 -0
- package/tokens/semantic.dark.json +94 -0
- package/tokens/semantic.high-contrast-dark.json +94 -0
- package/tokens/semantic.high-contrast.json +94 -0
- package/tokens/semantic.light.json +179 -0
- package/tokens/tokens.css +960 -58
- package/tokens/tokens.js +729 -120
- package/tokens/tokens.json +1158 -116
- 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
|
+
}
|
package/components/switch.css
CHANGED
|
@@ -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%,
|
|
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
|
-
/*
|
|
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:
|
|
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
|
+
}
|
package/components/table.css
CHANGED
|
@@ -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
|
-
|
|
106
|
-
|
|
107
|
-
|
|
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
|
+
}
|
package/components/tabs.css
CHANGED
|
@@ -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:
|
|
45
|
-
|
|
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
|
-
|
|
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
|
+
}
|
package/components/toast.css
CHANGED
|
@@ -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:
|
|
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
|
+
}
|
package/components/toolbar.css
CHANGED
|
@@ -15,7 +15,8 @@
|
|
|
15
15
|
display: flex;
|
|
16
16
|
align-items: center;
|
|
17
17
|
gap: var(--ct-toolbar-gap);
|
|
18
|
-
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) {
|
package/components/tooltip.css
CHANGED
|
@@ -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;
|
package/components/tree.css
CHANGED
|
@@ -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.
|