@neuravision/construct 2.1.1 → 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 +46 -0
- package/README.md +13 -0
- package/behaviors/combobox.js +43 -4
- package/behaviors/dropdown.js +16 -2
- package/behaviors/index.d.ts +2 -0
- package/behaviors/internal/dialog-controller.js +79 -9
- package/behaviors/internal/dom.js +38 -7
- package/behaviors/internal/overlay.js +4 -4
- package/behaviors/internal/popup-layer.js +2 -0
- package/behaviors/popover.js +21 -12
- package/behaviors/select-menu.js +20 -2
- package/behaviors/tabs.js +13 -2
- package/behaviors/toggle-group.js +37 -5
- package/behaviors/tooltip.js +26 -2
- package/components/README.md +40 -0
- package/components/_shared.css +11 -0
- package/components/accordion.css +8 -0
- package/components/app-shell-v2.css +7 -0
- package/components/app-shell.css +7 -0
- package/components/banner.css +32 -1
- package/components/card.css +23 -9
- package/components/chart.css +26 -0
- package/components/checkbox.css +32 -0
- package/components/chip.css +29 -1
- package/components/combobox.css +21 -0
- package/components/datepicker.css +55 -8
- package/components/datum.css +84 -11
- package/components/dropdown.css +23 -3
- package/components/field.css +6 -4
- package/components/file-upload.css +1 -1
- package/components/list.css +28 -2
- package/components/navbar.css +14 -1
- 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 +5 -0
- package/components/switch.css +62 -2
- 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/tree.css +30 -0
- package/docs/behaviors.md +26 -5
- package/docs/guidelines.md +30 -2
- package/foundations.css +6 -0
- package/package.json +7 -3
- package/schemas/components.schema.json +16 -1
- package/schemas/semantic.schema.json +2 -1
- package/schemas/theme.schema.json +1 -1
- package/scripts/build-tokens.mjs +87 -5
- package/themes/README.md +37 -0
- package/themes/walnut.theme.json +106 -0
- package/tokens/README.md +10 -2
- package/tokens/components.json +10 -0
- package/tokens/semantic.dark.json +4 -3
- package/tokens/semantic.high-contrast-dark.json +94 -0
- package/tokens/semantic.high-contrast.json +3 -2
- package/tokens/semantic.light.json +2 -1
- package/tokens/tokens.css +431 -33
- package/tokens/tokens.js +432 -133
- package/tokens/tokens.json +432 -133
- package/tokens/tokens.ts +432 -133
|
@@ -10,11 +10,12 @@ import {
|
|
|
10
10
|
createControllerLifecycle,
|
|
11
11
|
createDisposables,
|
|
12
12
|
dispatch,
|
|
13
|
-
|
|
13
|
+
focusIntoView,
|
|
14
14
|
getItemValue,
|
|
15
15
|
isDisabled,
|
|
16
16
|
nextEnabledIndex,
|
|
17
17
|
orientationDelta,
|
|
18
|
+
warnAccessibility,
|
|
18
19
|
} from './internal/dom.js';
|
|
19
20
|
|
|
20
21
|
const controllers = new WeakMap();
|
|
@@ -91,12 +92,35 @@ export function createToggleGroupController(options) {
|
|
|
91
92
|
if (focusIndex < 0) throw new TypeError('Toggle group controller requires at least one enabled item.');
|
|
92
93
|
if (!allowEmpty && selected.size === 0) selected.add(values[focusIndex]);
|
|
93
94
|
|
|
94
|
-
|
|
95
|
+
/* Two supported state models:
|
|
96
|
+
- button model (default): aria-pressed toggle buttons in a group/toolbar
|
|
97
|
+
- radio model: role="radiogroup"/role="radio" markup with aria-checked,
|
|
98
|
+
selection following focus per the APG radio-group pattern.
|
|
99
|
+
Radio markup used to be silently rewritten to buttons while its stale
|
|
100
|
+
aria-checked kept announcing the original selection — now it is honored. */
|
|
101
|
+
const radioSemantics =
|
|
102
|
+
type === 'single' &&
|
|
103
|
+
(root.getAttribute('role') === 'radiogroup' ||
|
|
104
|
+
items.some((item) => item.getAttribute('role') === 'radio'));
|
|
105
|
+
if (type === 'multiple' && items.some((item) => item.getAttribute('role') === 'radio')) {
|
|
106
|
+
warnAccessibility(
|
|
107
|
+
'Toggle group: role="radio" items require type "single"; falling back to aria-pressed buttons.',
|
|
108
|
+
);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const rootRole = root.getAttribute('role') ?? (radioSemantics ? 'radiogroup' : 'group');
|
|
95
112
|
attributes.set(root, 'role', rootRole);
|
|
96
113
|
attributes.set(root, 'data-orientation', orientation);
|
|
97
114
|
if (rootRole === 'toolbar') attributes.set(root, 'aria-orientation', orientation);
|
|
115
|
+
if (!root.hasAttribute('aria-label') && !root.hasAttribute('aria-labelledby')) {
|
|
116
|
+
warnAccessibility(
|
|
117
|
+
`Toggle group: the ${rootRole} element has neither aria-labelledby nor aria-label. ` +
|
|
118
|
+
'Grouping roles must expose an accessible name (WCAG 4.1.2).',
|
|
119
|
+
);
|
|
120
|
+
}
|
|
98
121
|
items.forEach((item) => {
|
|
99
|
-
if (
|
|
122
|
+
if (radioSemantics) attributes.set(item, 'role', 'radio');
|
|
123
|
+
else if (!isNativeButton(item)) attributes.set(item, 'role', 'button');
|
|
100
124
|
});
|
|
101
125
|
|
|
102
126
|
function currentValue() {
|
|
@@ -105,9 +129,15 @@ export function createToggleGroupController(options) {
|
|
|
105
129
|
}
|
|
106
130
|
|
|
107
131
|
function sync() {
|
|
132
|
+
/* An item disabled after init would otherwise strand the roving
|
|
133
|
+
tabindex and drop the whole group from the tab order. */
|
|
134
|
+
if (isDisabled(items[focusIndex])) {
|
|
135
|
+
const fallback = boundaryEnabledIndex(items);
|
|
136
|
+
if (fallback >= 0) focusIndex = fallback;
|
|
137
|
+
}
|
|
108
138
|
items.forEach((item, index) => {
|
|
109
139
|
const pressed = selected.has(values[index]);
|
|
110
|
-
attributes.set(item, 'aria-pressed', String(pressed));
|
|
140
|
+
attributes.set(item, radioSemantics ? 'aria-checked' : 'aria-pressed', String(pressed));
|
|
111
141
|
attributes.set(item, 'data-state', pressed ? 'on' : 'off');
|
|
112
142
|
attributes.set(item, 'tabindex', index === focusIndex && !isDisabled(item) ? '0' : '-1');
|
|
113
143
|
});
|
|
@@ -185,7 +215,9 @@ export function createToggleGroupController(options) {
|
|
|
185
215
|
event.preventDefault();
|
|
186
216
|
focusIndex = nextIndex;
|
|
187
217
|
sync();
|
|
188
|
-
|
|
218
|
+
focusIntoView(items[nextIndex]);
|
|
219
|
+
/* APG radio group: selection follows focus. */
|
|
220
|
+
if (radioSemantics && !selected.has(values[nextIndex])) toggle(nextIndex, 'keyboard');
|
|
189
221
|
return;
|
|
190
222
|
}
|
|
191
223
|
|
package/behaviors/tooltip.js
CHANGED
|
@@ -7,6 +7,8 @@ import {
|
|
|
7
7
|
dispatch,
|
|
8
8
|
ensureId,
|
|
9
9
|
eventTargetsOutside,
|
|
10
|
+
isComposingEvent,
|
|
11
|
+
warnAccessibility,
|
|
10
12
|
} from './internal/dom.js';
|
|
11
13
|
import { consumeTopOverlayEvent, pushOverlay } from './internal/overlay.js';
|
|
12
14
|
|
|
@@ -52,6 +54,14 @@ export function createTooltipController(options) {
|
|
|
52
54
|
let focused = false;
|
|
53
55
|
let touchOpened = false;
|
|
54
56
|
|
|
57
|
+
if (trigger.tabIndex < 0) {
|
|
58
|
+
warnAccessibility(
|
|
59
|
+
'Tooltip: the trigger is not keyboard-focusable, so keyboard and screen-reader ' +
|
|
60
|
+
'users can never open this tooltip (WCAG 2.1.1). Use a natively focusable ' +
|
|
61
|
+
'element or add tabindex="0".',
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
|
|
55
65
|
const tooltipId = ensureId(tooltip, 'ct-tooltip', attributes);
|
|
56
66
|
const describedBy = new Set((trigger.getAttribute('aria-describedby') ?? '').split(/\s+/).filter(Boolean));
|
|
57
67
|
describedBy.add(tooltipId);
|
|
@@ -165,7 +175,16 @@ export function createTooltipController(options) {
|
|
|
165
175
|
});
|
|
166
176
|
disposables.listen(trigger, 'focus', () => {
|
|
167
177
|
focused = true;
|
|
168
|
-
|
|
178
|
+
/* Open instantly for keyboard focus; pointer-driven focus (mouse click
|
|
179
|
+
on the trigger) is already covered by the hover path and would only
|
|
180
|
+
add noise. Engines without :focus-visible fall back to opening. */
|
|
181
|
+
let keyboardFocus = true;
|
|
182
|
+
try {
|
|
183
|
+
keyboardFocus = trigger.matches(':focus-visible');
|
|
184
|
+
} catch {
|
|
185
|
+
keyboardFocus = true;
|
|
186
|
+
}
|
|
187
|
+
if (keyboardFocus) scheduleOpen('focus', 0);
|
|
169
188
|
});
|
|
170
189
|
disposables.listen(trigger, 'blur', () => {
|
|
171
190
|
focused = false;
|
|
@@ -182,7 +201,12 @@ export function createTooltipController(options) {
|
|
|
182
201
|
}
|
|
183
202
|
});
|
|
184
203
|
disposables.listen(document, 'keydown', (event) => {
|
|
185
|
-
if (
|
|
204
|
+
if (
|
|
205
|
+
open &&
|
|
206
|
+
event.key === 'Escape' &&
|
|
207
|
+
!isComposingEvent(event) &&
|
|
208
|
+
consumeTopOverlayEvent(event, document, token)
|
|
209
|
+
) {
|
|
186
210
|
event.preventDefault();
|
|
187
211
|
event.stopPropagation();
|
|
188
212
|
closeTooltip('escape');
|
package/components/README.md
CHANGED
|
@@ -24,7 +24,9 @@ Example:
|
|
|
24
24
|
- `data-loading="true"` as the button's visual loading hook; native buttons also require `disabled`
|
|
25
25
|
- `aria-invalid="true"` for form errors
|
|
26
26
|
- `aria-current="page"` for current pagination page
|
|
27
|
+
- `aria-current="true"` for the current table row; `aria-selected="true"`/`data-state="selected"` for selected rows in grid contexts
|
|
27
28
|
- `aria-selected="true"` for active tabs and selected datepicker days
|
|
29
|
+
- `aria-sort="ascending|descending"` on the actively sorted column header
|
|
28
30
|
- `aria-disabled="true"` when disabled but not using native `disabled`
|
|
29
31
|
|
|
30
32
|
ARIA communicates state but does not block events. Prefer native `disabled` for form controls. For links and custom controls, suppress pointer and keyboard activation in application code when `aria-disabled="true"` is set. Add live-region attributes only to content that changes dynamically; static status labels do not need `aria-live`.
|
|
@@ -212,6 +214,37 @@ Content card with header, body, and footer:
|
|
|
212
214
|
</section>
|
|
213
215
|
```
|
|
214
216
|
|
|
217
|
+
### Datum Primitives
|
|
218
|
+
|
|
219
|
+
The reference-line grammar from `components/datum.css` — Construct's orange signature as reusable
|
|
220
|
+
structural vocabulary. All dimensions are component tokens (`--component-datum-*`).
|
|
221
|
+
|
|
222
|
+
```html
|
|
223
|
+
<div class="ct-eyebrow-set">
|
|
224
|
+
<p class="ct-eyebrow ct-eyebrow--numbered">Foundations</p>
|
|
225
|
+
</div>
|
|
226
|
+
|
|
227
|
+
<div class="ct-datum-scale" role="presentation"></div>
|
|
228
|
+
|
|
229
|
+
<div class="ct-datum-scale ct-datum-scale--progress" role="progressbar"
|
|
230
|
+
aria-label="Upload" aria-valuenow="60" aria-valuemin="0" aria-valuemax="100"
|
|
231
|
+
style="--ct-datum-scale-progress: 60%;"></div>
|
|
232
|
+
|
|
233
|
+
<figure class="ct-datum-frame">…</figure>
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
- `ct-eyebrow` - Mono kicker with a leading datum tick; `--numbered` adds drawing-set sheet numbers
|
|
237
|
+
(scope with `ct-eyebrow-set`)
|
|
238
|
+
- `ct-datum-scale` - Measured ruler line; decorative, mark it `role="presentation"`
|
|
239
|
+
- `ct-datum-scale--progress` - The origin segment reports progress via `--ct-datum-scale-progress`;
|
|
240
|
+
pair with `role="progressbar"` and `aria-value*`
|
|
241
|
+
- `ct-datum-frame` - Corner registration mark for figures, code samples, and media
|
|
242
|
+
- `ct-card--datum`, active leading edges, the tab indicator, sorted table columns, and
|
|
243
|
+
`ct-chart__datum-line` speak the same grammar
|
|
244
|
+
|
|
245
|
+
Orange is never text; edges use `brand-accent`, meaning-bearing fills use `brand-accent-strong`.
|
|
246
|
+
One datum per surface — see `docs/guidelines.md`.
|
|
247
|
+
|
|
215
248
|
### Table
|
|
216
249
|
|
|
217
250
|
Basic data table with variants:
|
|
@@ -248,6 +281,13 @@ Variants:
|
|
|
248
281
|
|
|
249
282
|
Rows receive a visual hover affordance automatically on hover-capable pointers; no hover modifier exists.
|
|
250
283
|
|
|
284
|
+
States (the datum grammar):
|
|
285
|
+
- `tr[aria-current="true"]`, `tr[aria-selected="true"]`, or `tr[data-state="selected"]` marks the
|
|
286
|
+
current/selected row with a muted background and the orange leading edge. Use `aria-current` on
|
|
287
|
+
native tables; `aria-selected` is only valid on rows inside `role="grid"`/`role="treegrid"`.
|
|
288
|
+
- `th[aria-sort="ascending|descending"]` underlines the actively sorted column header with the datum;
|
|
289
|
+
sort direction stays shape-encoded via the `ct-table__sort-indicator` arrows.
|
|
290
|
+
|
|
251
291
|
### Data Table
|
|
252
292
|
|
|
253
293
|
Complex data table with header, toolbar, filters, and actions:
|
package/components/_shared.css
CHANGED
|
@@ -93,6 +93,9 @@
|
|
|
93
93
|
background: var(--ct-control-bg-disabled);
|
|
94
94
|
border-color: var(--color-border-subtle);
|
|
95
95
|
color: var(--color-text-muted);
|
|
96
|
+
/* Disabled is fully colour-encoded above; cancel foundations' global
|
|
97
|
+
:disabled opacity so controls are not double-dimmed. */
|
|
98
|
+
opacity: 1;
|
|
96
99
|
}
|
|
97
100
|
|
|
98
101
|
:where(.ct-input, .ct-select, .ct-textarea, .ct-control)[aria-invalid='true'],
|
|
@@ -135,3 +138,11 @@
|
|
|
135
138
|
--ct-control-font-size: var(--font-size-lg);
|
|
136
139
|
--ct-control-line-height: var(--line-height-lg);
|
|
137
140
|
}
|
|
141
|
+
|
|
142
|
+
/* Per-file kill switch: standalone/core.css imports this file without
|
|
143
|
+
foundations.css and its global reduced-motion rule. */
|
|
144
|
+
@media (prefers-reduced-motion: reduce) {
|
|
145
|
+
:where(.ct-input, .ct-select, .ct-textarea, .ct-control) {
|
|
146
|
+
transition: none;
|
|
147
|
+
}
|
|
148
|
+
}
|
package/components/accordion.css
CHANGED
|
@@ -116,6 +116,14 @@
|
|
|
116
116
|
padding: 0 var(--space-4) var(--space-4);
|
|
117
117
|
}
|
|
118
118
|
|
|
119
|
+
/* ── Construct 44×44 target policy (WCAG 2.2 SC 2.5.5 is AAA) ── */
|
|
120
|
+
|
|
121
|
+
@media (pointer: coarse) {
|
|
122
|
+
.ct-accordion__trigger {
|
|
123
|
+
min-block-size: 44px;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
119
127
|
@media (prefers-reduced-motion: reduce) {
|
|
120
128
|
.ct-accordion__icon,
|
|
121
129
|
.ct-accordion__item::details-content {
|
|
@@ -170,6 +170,13 @@
|
|
|
170
170
|
background: var(--ct-v2-surface-bg);
|
|
171
171
|
}
|
|
172
172
|
|
|
173
|
+
/* The shell owns its scroll container, so it can satisfy WCAG 2.4.11
|
|
174
|
+
itself: scrolled-to content clears the sticky toolbar. Override
|
|
175
|
+
--ct-shell-scroll-offset when the toolbar height is customized. */
|
|
176
|
+
.ct-app-shell-v2__main:has(.ct-app-shell-v2__toolbar--sticky) {
|
|
177
|
+
scroll-padding-block-start: var(--ct-shell-scroll-offset, 64px);
|
|
178
|
+
}
|
|
179
|
+
|
|
173
180
|
|
|
174
181
|
/* ── Sidebar States ── */
|
|
175
182
|
|
package/components/app-shell.css
CHANGED
|
@@ -230,6 +230,13 @@
|
|
|
230
230
|
background: var(--color-bg-canvas);
|
|
231
231
|
}
|
|
232
232
|
|
|
233
|
+
/* The shell owns its scroll container, so it can satisfy WCAG 2.4.11
|
|
234
|
+
itself: scrolled-to content clears the sticky page header. Override
|
|
235
|
+
--ct-shell-scroll-offset when the header height is customized. */
|
|
236
|
+
.ct-app-shell__main:has(.ct-app-shell__page-header--sticky) {
|
|
237
|
+
scroll-padding-block-start: var(--ct-shell-scroll-offset, 64px);
|
|
238
|
+
}
|
|
239
|
+
|
|
233
240
|
|
|
234
241
|
|
|
235
242
|
/* ── Backdrop (Mobile Sidebar Overlay) ── */
|
package/components/banner.css
CHANGED
|
@@ -26,7 +26,8 @@
|
|
|
26
26
|
font-size: var(--font-size-sm);
|
|
27
27
|
line-height: var(--line-height-sm);
|
|
28
28
|
transition: opacity var(--duration-normal) var(--easing-standard),
|
|
29
|
-
transform var(--duration-normal) var(--easing-standard)
|
|
29
|
+
transform var(--duration-normal) var(--easing-standard),
|
|
30
|
+
visibility 0s linear 0s;
|
|
30
31
|
}
|
|
31
32
|
|
|
32
33
|
/* ---- Sub-elements ---- */
|
|
@@ -159,6 +160,8 @@
|
|
|
159
160
|
|
|
160
161
|
/* ---- Positions ---- */
|
|
161
162
|
|
|
163
|
+
/* Consumers: set --ct-scroll-offset-top on :root (see foundations.css) so
|
|
164
|
+
scrolled-to content is not obscured behind the banner (WCAG 2.4.11). */
|
|
162
165
|
.ct-banner--fixed-top {
|
|
163
166
|
position: fixed;
|
|
164
167
|
inset-block-start: 0;
|
|
@@ -167,6 +170,8 @@
|
|
|
167
170
|
border-radius: 0;
|
|
168
171
|
}
|
|
169
172
|
|
|
173
|
+
/* Consumers: set --ct-scroll-offset-bottom on :root (see foundations.css) so
|
|
174
|
+
scrolled-to content is not obscured behind the banner (WCAG 2.4.11). */
|
|
170
175
|
.ct-banner--fixed-bottom {
|
|
171
176
|
position: fixed;
|
|
172
177
|
inset-block-end: 0;
|
|
@@ -175,6 +180,8 @@
|
|
|
175
180
|
border-radius: 0;
|
|
176
181
|
}
|
|
177
182
|
|
|
183
|
+
/* Consumers: set --ct-scroll-offset-top on :root (see foundations.css) so
|
|
184
|
+
scrolled-to content is not obscured behind the banner (WCAG 2.4.11). */
|
|
178
185
|
.ct-banner--sticky {
|
|
179
186
|
position: sticky;
|
|
180
187
|
inset-block-start: 0;
|
|
@@ -211,9 +218,16 @@
|
|
|
211
218
|
|
|
212
219
|
/* ---- State ---- */
|
|
213
220
|
|
|
221
|
+
/* Delayed visibility (drawer.css pattern) removes the faded-out banner
|
|
222
|
+
from the accessibility tree and tab order once the fade completes;
|
|
223
|
+
reopening restores it instantly via the base transition. */
|
|
214
224
|
.ct-banner[data-state='closed'] {
|
|
215
225
|
opacity: 0;
|
|
226
|
+
visibility: hidden;
|
|
216
227
|
pointer-events: none;
|
|
228
|
+
transition: opacity var(--duration-normal) var(--easing-standard),
|
|
229
|
+
transform var(--duration-normal) var(--easing-standard),
|
|
230
|
+
visibility 0s linear var(--duration-normal);
|
|
217
231
|
}
|
|
218
232
|
|
|
219
233
|
.ct-banner--fixed-top[data-state='closed'],
|
|
@@ -237,6 +251,23 @@
|
|
|
237
251
|
}
|
|
238
252
|
}
|
|
239
253
|
|
|
254
|
+
/* ---- Construct 44×44 target policy (WCAG 2.2 SC 2.5.5 is AAA) ---- */
|
|
255
|
+
|
|
256
|
+
@media (pointer: coarse) {
|
|
257
|
+
/* The default close is --control-height-sm, which _shared.css remaps to
|
|
258
|
+
44px; only the fixed 24px compact close needs the checkbox.css-style
|
|
259
|
+
hit area. */
|
|
260
|
+
.ct-banner--compact .ct-banner__close {
|
|
261
|
+
position: relative;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
.ct-banner--compact .ct-banner__close::before {
|
|
265
|
+
content: '';
|
|
266
|
+
position: absolute;
|
|
267
|
+
inset: calc((var(--icon-lg) - 44px) / 2);
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
240
271
|
/* ---- Reduced Motion ---- */
|
|
241
272
|
|
|
242
273
|
@media (prefers-reduced-motion: reduce) {
|
package/components/card.css
CHANGED
|
@@ -11,6 +11,8 @@
|
|
|
11
11
|
border-radius: var(--ct-card-radius);
|
|
12
12
|
padding: var(--ct-card-padding);
|
|
13
13
|
box-shadow: var(--ct-card-shadow);
|
|
14
|
+
/* The datum edge slides in on activation instead of popping. */
|
|
15
|
+
transition: box-shadow var(--duration-fast) var(--easing-standard);
|
|
14
16
|
}
|
|
15
17
|
|
|
16
18
|
.ct-card--sm {
|
|
@@ -104,18 +106,30 @@
|
|
|
104
106
|
.ct-card--datum::before {
|
|
105
107
|
content: '';
|
|
106
108
|
position: absolute;
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
width:
|
|
110
|
-
height:
|
|
111
|
-
border-
|
|
112
|
-
border-
|
|
113
|
-
border-
|
|
109
|
+
inset-block-start: calc(-1 * var(--border-thin));
|
|
110
|
+
inset-inline-start: calc(-1 * var(--border-thin));
|
|
111
|
+
width: var(--component-datum-mark-size);
|
|
112
|
+
height: var(--component-datum-mark-size);
|
|
113
|
+
border-block-start: var(--border-medium) solid var(--color-brand-accent);
|
|
114
|
+
border-inline-start: var(--border-medium) solid var(--color-brand-accent);
|
|
115
|
+
border-start-start-radius: var(--radius-lg);
|
|
116
|
+
pointer-events: none;
|
|
114
117
|
}
|
|
115
118
|
|
|
116
119
|
.ct-card--active,
|
|
117
120
|
.ct-card[data-state='active'] {
|
|
118
121
|
/* The datum: an orange leading edge marks the active/current card,
|
|
119
|
-
consistent with .ct-list__item and .ct-sidebar active states.
|
|
120
|
-
|
|
122
|
+
consistent with .ct-list__item and .ct-sidebar active states. The
|
|
123
|
+
ambient shadow stays first so the list interpolates against the
|
|
124
|
+
base card shadow and the edge can animate in. */
|
|
125
|
+
box-shadow: var(--ct-card-shadow), inset var(--component-datum-edge-width) 0 0 0 var(--color-brand-accent);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
@media (forced-colors: active) {
|
|
129
|
+
/* The datum edge is an inset box-shadow, which forced colors drops —
|
|
130
|
+
re-encode it as a border. */
|
|
131
|
+
.ct-card--active,
|
|
132
|
+
.ct-card[data-state='active'] {
|
|
133
|
+
border-inline-start: var(--component-datum-edge-width) solid SelectedItem;
|
|
134
|
+
}
|
|
121
135
|
}
|
package/components/chart.css
CHANGED
|
@@ -86,6 +86,27 @@
|
|
|
86
86
|
fill: var(--color-text-secondary);
|
|
87
87
|
}
|
|
88
88
|
|
|
89
|
+
/* ── Datum reference line ──
|
|
90
|
+
Construct's signature applied to data: an orange reference line marks
|
|
91
|
+
a threshold, target, or "today". The dash pattern separates the
|
|
92
|
+
reference from data series without relying on hue, and the value it
|
|
93
|
+
encodes must also appear as text — in the label and in the chart's
|
|
94
|
+
data-table fallback — so the line itself stays supplementary. */
|
|
95
|
+
.ct-chart__datum-line {
|
|
96
|
+
stroke: var(--color-brand-accent-strong);
|
|
97
|
+
stroke-width: var(--ct-chart-line-width);
|
|
98
|
+
stroke-dasharray: 6 4;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
.ct-chart__datum-label {
|
|
102
|
+
fill: var(--color-text-secondary);
|
|
103
|
+
font-family: var(--font-family-mono);
|
|
104
|
+
font-size: var(--font-size-xs);
|
|
105
|
+
font-weight: var(--font-weight-semibold);
|
|
106
|
+
letter-spacing: 0.08em;
|
|
107
|
+
text-transform: uppercase;
|
|
108
|
+
}
|
|
109
|
+
|
|
89
110
|
/* ── Line & area ── */
|
|
90
111
|
.ct-chart__line {
|
|
91
112
|
fill: none;
|
|
@@ -366,4 +387,9 @@
|
|
|
366
387
|
.ct-chart__axis-line {
|
|
367
388
|
stroke: GrayText;
|
|
368
389
|
}
|
|
390
|
+
|
|
391
|
+
/* The datum reference line stays distinct from the dimmed chrome. */
|
|
392
|
+
.ct-chart__datum-line {
|
|
393
|
+
stroke: CanvasText;
|
|
394
|
+
}
|
|
369
395
|
}
|
package/components/checkbox.css
CHANGED
|
@@ -98,6 +98,9 @@
|
|
|
98
98
|
background: var(--color-bg-muted);
|
|
99
99
|
border-color: var(--color-border-subtle);
|
|
100
100
|
cursor: not-allowed;
|
|
101
|
+
/* Disabled is fully colour-encoded above; cancel foundations' global
|
|
102
|
+
:disabled opacity so the control is not double-dimmed. */
|
|
103
|
+
opacity: 1;
|
|
101
104
|
}
|
|
102
105
|
|
|
103
106
|
.ct-check__input:focus-visible,
|
|
@@ -119,3 +122,32 @@
|
|
|
119
122
|
border-inline-end: 0;
|
|
120
123
|
transform: scale(1);
|
|
121
124
|
}
|
|
125
|
+
|
|
126
|
+
@media (prefers-reduced-motion: reduce) {
|
|
127
|
+
.ct-check__input,
|
|
128
|
+
.ct-radio__input,
|
|
129
|
+
.ct-check__input::before,
|
|
130
|
+
.ct-radio__input::before {
|
|
131
|
+
transition: none;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
@media (forced-colors: active) {
|
|
136
|
+
.ct-check__input:checked,
|
|
137
|
+
.ct-check__input:indeterminate,
|
|
138
|
+
.ct-radio__input:checked {
|
|
139
|
+
border-color: ButtonText;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/* The tick and indeterminate dash are border-drawn and survive on their
|
|
143
|
+
own; the radio dot is background-drawn and would be forced to Canvas. */
|
|
144
|
+
.ct-radio__input:checked::before {
|
|
145
|
+
background: SelectedItem;
|
|
146
|
+
forced-color-adjust: none;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
.ct-check__input:disabled,
|
|
150
|
+
.ct-radio__input:disabled {
|
|
151
|
+
border-color: GrayText;
|
|
152
|
+
}
|
|
153
|
+
}
|
package/components/chip.css
CHANGED
|
@@ -35,7 +35,9 @@
|
|
|
35
35
|
--ct-chip-padding-x: var(--space-2);
|
|
36
36
|
--ct-chip-font-size: var(--font-size-xs);
|
|
37
37
|
--ct-chip-line-height: var(--line-height-xs);
|
|
38
|
-
|
|
38
|
+
/* WCAG 2.2 SC 2.5.8: the remove button stays at least 24px in every
|
|
39
|
+
pointer context, so the small chip cannot shrink it below that. */
|
|
40
|
+
--ct-chip-remove-size: var(--icon-lg);
|
|
39
41
|
--ct-chip-gap: var(--space-1);
|
|
40
42
|
}
|
|
41
43
|
|
|
@@ -346,6 +348,17 @@
|
|
|
346
348
|
.ct-chip {
|
|
347
349
|
min-height: 44px;
|
|
348
350
|
}
|
|
351
|
+
|
|
352
|
+
/* Hit area may extend past the chip edge, mirroring checkbox.css. */
|
|
353
|
+
.ct-chip__remove {
|
|
354
|
+
position: relative;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
.ct-chip__remove::before {
|
|
358
|
+
content: '';
|
|
359
|
+
position: absolute;
|
|
360
|
+
inset: calc((var(--ct-chip-remove-size) - 44px) / 2);
|
|
361
|
+
}
|
|
349
362
|
}
|
|
350
363
|
|
|
351
364
|
/* ── Reduced motion ──────────────────────────────────────────── */
|
|
@@ -361,3 +374,18 @@
|
|
|
361
374
|
transform: none;
|
|
362
375
|
}
|
|
363
376
|
}
|
|
377
|
+
|
|
378
|
+
/* ── Forced colors ───────────────────────────────────────────── */
|
|
379
|
+
|
|
380
|
+
@media (forced-colors: active) {
|
|
381
|
+
/* Mask-drawn glyphs and the status dot paint via background-color,
|
|
382
|
+
which forced colors resets — repaint them with system ink. The
|
|
383
|
+
selected checkmark is border-drawn and survives on its own. */
|
|
384
|
+
.ct-chip__icon--info,
|
|
385
|
+
.ct-chip__icon--success,
|
|
386
|
+
.ct-chip__icon--warning,
|
|
387
|
+
.ct-chip__icon--danger,
|
|
388
|
+
.ct-chip__dot {
|
|
389
|
+
background-color: CanvasText;
|
|
390
|
+
}
|
|
391
|
+
}
|
package/components/combobox.css
CHANGED
|
@@ -374,6 +374,27 @@
|
|
|
374
374
|
border-color: transparent;
|
|
375
375
|
}
|
|
376
376
|
|
|
377
|
+
/* Replacement indicator for the removed input outline (mirrors the --multi pattern) */
|
|
378
|
+
.ct-combobox--inline:focus-within {
|
|
379
|
+
outline: 2px solid var(--color-focus-ring);
|
|
380
|
+
outline-offset: 2px;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
/* ── Construct 44×44 target policy (WCAG 2.2 SC 2.5.5 is AAA) ── */
|
|
384
|
+
|
|
385
|
+
@media (pointer: coarse) {
|
|
386
|
+
.ct-combobox__option {
|
|
387
|
+
min-block-size: 44px;
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
/* Hit area extends past the visual 24px trigger, mirroring checkbox.css. */
|
|
391
|
+
.ct-combobox__trigger::before {
|
|
392
|
+
content: '';
|
|
393
|
+
position: absolute;
|
|
394
|
+
inset: calc((var(--icon-lg) - 44px) / 2);
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
|
|
377
398
|
/* ── Reduced motion ──────────────────────────────────────────── */
|
|
378
399
|
|
|
379
400
|
@media (prefers-reduced-motion: reduce) {
|
|
@@ -79,10 +79,22 @@
|
|
|
79
79
|
justify-items: center;
|
|
80
80
|
}
|
|
81
81
|
|
|
82
|
+
/* Rows keep grid placement on the shared 7-column grid. display: contents
|
|
83
|
+
once dropped element semantics in older engines; current Chromium/Firefox/
|
|
84
|
+
Safari expose role="row" on display: contents elements correctly. */
|
|
82
85
|
.ct-datepicker__row {
|
|
83
86
|
display: contents;
|
|
84
87
|
}
|
|
85
88
|
|
|
89
|
+
/* Optional gridcell wrapper (APG date-picker dialog markup): the cell is the
|
|
90
|
+
grid item and carries role="gridcell" + aria-selected; the day button
|
|
91
|
+
inside keeps its native button role. */
|
|
92
|
+
.ct-datepicker__cell {
|
|
93
|
+
display: flex;
|
|
94
|
+
width: 100%;
|
|
95
|
+
justify-content: center;
|
|
96
|
+
}
|
|
97
|
+
|
|
86
98
|
.ct-datepicker__weekday {
|
|
87
99
|
font-size: var(--font-size-xs);
|
|
88
100
|
color: var(--color-text-muted);
|
|
@@ -115,9 +127,10 @@
|
|
|
115
127
|
outline-offset: 2px;
|
|
116
128
|
}
|
|
117
129
|
|
|
118
|
-
.ct-datepicker__day[aria-selected='true']
|
|
130
|
+
.ct-datepicker__day[aria-selected='true'],
|
|
131
|
+
.ct-datepicker__cell[aria-selected='true'] .ct-datepicker__day {
|
|
119
132
|
background: var(--color-brand-primary);
|
|
120
|
-
color: var(--color-text-
|
|
133
|
+
color: var(--color-text-on-primary);
|
|
121
134
|
}
|
|
122
135
|
|
|
123
136
|
.ct-datepicker__day[data-outside='true'] {
|
|
@@ -158,13 +171,13 @@
|
|
|
158
171
|
|
|
159
172
|
.ct-datepicker__day[data-range-start] {
|
|
160
173
|
background: var(--color-brand-primary);
|
|
161
|
-
color: var(--color-text-
|
|
174
|
+
color: var(--color-text-on-primary);
|
|
162
175
|
border-radius: var(--radius-sm) 0 0 var(--radius-sm);
|
|
163
176
|
}
|
|
164
177
|
|
|
165
178
|
.ct-datepicker__day[data-range-end] {
|
|
166
179
|
background: var(--color-brand-primary);
|
|
167
|
-
color: var(--color-text-
|
|
180
|
+
color: var(--color-text-on-primary);
|
|
168
181
|
border-radius: 0 var(--radius-sm) var(--radius-sm) 0;
|
|
169
182
|
}
|
|
170
183
|
|
|
@@ -172,6 +185,38 @@
|
|
|
172
185
|
border-radius: var(--radius-sm);
|
|
173
186
|
}
|
|
174
187
|
|
|
188
|
+
/* ── Coarse pointers ────────────────────────────────────────────── */
|
|
189
|
+
|
|
190
|
+
@media (pointer: coarse) {
|
|
191
|
+
/* Day cells may compress inside the min(90vw, …) popover; tap size
|
|
192
|
+
wins over the shared column width (44×44 policy). */
|
|
193
|
+
.ct-datepicker__day {
|
|
194
|
+
min-width: var(--control-height-calendar-day);
|
|
195
|
+
min-height: var(--control-height-calendar-day);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/* ── Forced colors ──────────────────────────────────────────────── */
|
|
200
|
+
|
|
201
|
+
@media (forced-colors: active) {
|
|
202
|
+
/* Selection fills are background-only and would be stripped to Canvas —
|
|
203
|
+
re-encode with system selection colors (data-today's border and the
|
|
204
|
+
highlight outline survive on their own). */
|
|
205
|
+
.ct-datepicker__day[aria-selected='true'],
|
|
206
|
+
.ct-datepicker__cell[aria-selected='true'] .ct-datepicker__day,
|
|
207
|
+
.ct-datepicker__cell[aria-selected='true'] .ct-datepicker__month,
|
|
208
|
+
.ct-datepicker__cell[aria-selected='true'] .ct-datepicker__year,
|
|
209
|
+
.ct-datepicker__month[aria-selected='true'],
|
|
210
|
+
.ct-datepicker__year[aria-selected='true'],
|
|
211
|
+
.ct-datepicker__day[data-in-range],
|
|
212
|
+
.ct-datepicker__day[data-range-start],
|
|
213
|
+
.ct-datepicker__day[data-range-end] {
|
|
214
|
+
background: SelectedItem;
|
|
215
|
+
color: SelectedItemText;
|
|
216
|
+
forced-color-adjust: none;
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
175
220
|
/* ── Month grid (3×4) ───────────────────────────────────────────── */
|
|
176
221
|
|
|
177
222
|
.ct-datepicker__month-grid {
|
|
@@ -205,9 +250,10 @@
|
|
|
205
250
|
outline-offset: 2px;
|
|
206
251
|
}
|
|
207
252
|
|
|
208
|
-
.ct-datepicker__month[aria-selected='true']
|
|
253
|
+
.ct-datepicker__month[aria-selected='true'],
|
|
254
|
+
.ct-datepicker__cell[aria-selected='true'] .ct-datepicker__month {
|
|
209
255
|
background: var(--color-brand-primary);
|
|
210
|
-
color: var(--color-text-
|
|
256
|
+
color: var(--color-text-on-primary);
|
|
211
257
|
}
|
|
212
258
|
|
|
213
259
|
.ct-datepicker__month[data-highlighted] {
|
|
@@ -254,9 +300,10 @@
|
|
|
254
300
|
outline-offset: 2px;
|
|
255
301
|
}
|
|
256
302
|
|
|
257
|
-
.ct-datepicker__year[aria-selected='true']
|
|
303
|
+
.ct-datepicker__year[aria-selected='true'],
|
|
304
|
+
.ct-datepicker__cell[aria-selected='true'] .ct-datepicker__year {
|
|
258
305
|
background: var(--color-brand-primary);
|
|
259
|
-
color: var(--color-text-
|
|
306
|
+
color: var(--color-text-on-primary);
|
|
260
307
|
}
|
|
261
308
|
|
|
262
309
|
.ct-datepicker__year[data-highlighted] {
|