@apliteni/apliteni-ui 0.124.1 → 0.125.0
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/guidelines/colour-and-theming.md +12 -0
- package/package.json +1 -1
- package/react/README.md +11 -0
- package/react/dist/index.d.ts +5 -1
- package/react/dist/index.js +23 -6
- package/src/components/index.js +48 -9
- package/src/styles/command-palette.css +17 -0
- package/src/styles/layout.css +39 -1
- package/src/styles/nav.css +61 -0
- package/src/styles/pagination.css +19 -0
- package/src/styles/segmented.css +25 -5
|
@@ -70,3 +70,15 @@
|
|
|
70
70
|
**Except:** The sidebar rail's current row. Its mark may be neither an accent nor an edge — an outline there means focus — and in the page shell the rail is the card surface already. It takes the quiet fill a rung off its rail, label and all. Artur, r34 of [#475](https://github.com/apliteni/apliteni-ui/issues/475).
|
|
71
71
|
|
|
72
72
|
**Except:** A status chip carries a soft fill. Where its tone reports no verdict it takes a light tint of muted ink that leaves the panel in one direction per theme, so the neutral tones read as the same family as the coloured ones.
|
|
73
|
+
|
|
74
|
+
## Mark selection in forced colours
|
|
75
|
+
|
|
76
|
+
<!-- rule: selected-forced-colours -->
|
|
77
|
+
|
|
78
|
+
**Rule:** In forced colours, fill a selected item with the highlight colour and its label with the highlight text colour.
|
|
79
|
+
|
|
80
|
+
**Why:** The mode replaces author backgrounds. A specimen cannot switch it on, so this rule draws no pair.
|
|
81
|
+
|
|
82
|
+
**Do:** Ring a focused selected item in the page text colour.
|
|
83
|
+
|
|
84
|
+
**Don't:** Mark selection with an outline.
|
package/package.json
CHANGED
package/react/README.md
CHANGED
|
@@ -545,6 +545,17 @@ class and invalid styling as appropriate:
|
|
|
545
545
|
The frame supplies a stable ID, required state, and linked hint/error messages.
|
|
546
546
|
An optional `id` lets the caller choose the control ID; it must be unique.
|
|
547
547
|
|
|
548
|
+
A `Dropdown` is a control too. Spread the attributes onto it and its trigger takes
|
|
549
|
+
`aria-describedby` and `aria-invalid`, so the error and hint are read when it gets focus.
|
|
550
|
+
Its accessible name is the label and then the chosen value. Outside a `Field`,
|
|
551
|
+
`Dropdown` takes `aria-describedby` and `aria-invalid` as props and forwards them to the trigger.
|
|
552
|
+
|
|
553
|
+
```tsx
|
|
554
|
+
<Field label="Owner" error="That option is no longer available.">
|
|
555
|
+
{control => <Dropdown {...control} variant="select" items={owners} />}
|
|
556
|
+
</Field>
|
|
557
|
+
```
|
|
558
|
+
|
|
548
559
|
### Search
|
|
549
560
|
|
|
550
561
|
`SearchField` is the toolbar's search box: a search glyph, a native
|
package/react/dist/index.d.ts
CHANGED
|
@@ -501,6 +501,10 @@ type DropdownProps = {
|
|
|
501
501
|
/** A field above the rows that filters them as the reader types. */
|
|
502
502
|
search?: boolean | DropdownSearch;
|
|
503
503
|
ariaLabel?: string;
|
|
504
|
+
/** Names the elements that describe the trigger, e.g. a field's error and hint. */
|
|
505
|
+
'aria-describedby'?: string;
|
|
506
|
+
/** Marks the trigger invalid. `Field` supplies it while its error shows. */
|
|
507
|
+
'aria-invalid'?: boolean | 'true' | 'false';
|
|
504
508
|
id?: string;
|
|
505
509
|
panelClass?: string;
|
|
506
510
|
/** Controlled: the host owns the open state and answers `onOpenChange`. */
|
|
@@ -512,7 +516,7 @@ type DropdownProps = {
|
|
|
512
516
|
/** Draw the row yourself. `props` is everything the row needs; spread it. */
|
|
513
517
|
row?: (item: DropdownItem, props: DropdownRowProps) => ReactNode;
|
|
514
518
|
};
|
|
515
|
-
declare function Dropdown({ label, value, placeholder, variant, items, sections, header, footer, triggerContent, triggerClass, disabled, chevron, align, direction, scroll, search, ariaLabel, id, panelClass, open: openProp, defaultOpen, onOpenChange, onSelect, row, }: DropdownProps): react.JSX.Element;
|
|
519
|
+
declare function Dropdown({ label, value, placeholder, variant, items, sections, header, footer, triggerContent, triggerClass, disabled, chevron, align, direction, scroll, search, ariaLabel, id, panelClass, 'aria-describedby': describedBy, 'aria-invalid': invalid, open: openProp, defaultOpen, onOpenChange, onSelect, row, }: DropdownProps): react.JSX.Element;
|
|
516
520
|
|
|
517
521
|
type BackLinkOwnProps = {
|
|
518
522
|
/** The address. No address, or a `javascript:` one, and nothing is rendered. */
|
package/react/dist/index.js
CHANGED
|
@@ -2072,6 +2072,7 @@ function DatePicker({
|
|
|
2072
2072
|
import {
|
|
2073
2073
|
Fragment as Fragment5,
|
|
2074
2074
|
useCallback as useCallback4,
|
|
2075
|
+
useContext as useContext2,
|
|
2075
2076
|
useEffect as useEffect7,
|
|
2076
2077
|
useId as useId8,
|
|
2077
2078
|
useRef as useRef11,
|
|
@@ -2087,6 +2088,12 @@ import {
|
|
|
2087
2088
|
dropdownHeightFit,
|
|
2088
2089
|
filterPanelRow
|
|
2089
2090
|
} from "@apliteni/apliteni-ui";
|
|
2091
|
+
|
|
2092
|
+
// src/fieldContext.ts
|
|
2093
|
+
import { createContext as createContext2 } from "react";
|
|
2094
|
+
var FieldContext = createContext2(null);
|
|
2095
|
+
|
|
2096
|
+
// src/Dropdown.tsx
|
|
2090
2097
|
import { Fragment as Fragment6, jsx as jsx20, jsxs as jsxs16 } from "react/jsx-runtime";
|
|
2091
2098
|
var EXIT_SLACK_MS2 = 50;
|
|
2092
2099
|
var SETTLE_MS = 450;
|
|
@@ -2140,6 +2147,8 @@ function Dropdown({
|
|
|
2140
2147
|
ariaLabel,
|
|
2141
2148
|
id,
|
|
2142
2149
|
panelClass = "",
|
|
2150
|
+
"aria-describedby": describedBy,
|
|
2151
|
+
"aria-invalid": invalid,
|
|
2143
2152
|
open: openProp,
|
|
2144
2153
|
defaultOpen = false,
|
|
2145
2154
|
onOpenChange,
|
|
@@ -2159,6 +2168,8 @@ function Dropdown({
|
|
|
2159
2168
|
const at = useRef11("");
|
|
2160
2169
|
const auto = useId8().replace(/:/g, "");
|
|
2161
2170
|
const uid = id ?? auto;
|
|
2171
|
+
const inField = useContext2(FieldContext);
|
|
2172
|
+
const fieldName = inField && id === inField.id && !(ariaLabel && triggerContent != null) ? { id: `${uid}-trigger`, labelId: inField.labelId, labelledBy: `${inField.labelId} ${uid}-trigger` } : null;
|
|
2162
2173
|
const setOpen = useCallback4((next) => {
|
|
2163
2174
|
if (openProp === void 0) setSelfOpen(next);
|
|
2164
2175
|
onOpenChange?.(next);
|
|
@@ -2559,7 +2570,11 @@ function Dropdown({
|
|
|
2559
2570
|
disabled: disabled || void 0,
|
|
2560
2571
|
"aria-haspopup": sx ? "dialog" : listRole,
|
|
2561
2572
|
"aria-expanded": open,
|
|
2573
|
+
id: fieldName?.id,
|
|
2562
2574
|
"aria-label": ariaLabel && triggerContent != null ? ariaLabel : void 0,
|
|
2575
|
+
"aria-labelledby": fieldName?.labelledBy,
|
|
2576
|
+
"aria-describedby": describedBy,
|
|
2577
|
+
"aria-invalid": invalid,
|
|
2563
2578
|
ref: trigger,
|
|
2564
2579
|
onClick: (e) => {
|
|
2565
2580
|
e.stopPropagation();
|
|
@@ -2588,6 +2603,7 @@ function Dropdown({
|
|
|
2588
2603
|
"data-dropdown-panel": "",
|
|
2589
2604
|
role: sx ? "dialog" : listRole,
|
|
2590
2605
|
"aria-label": sx ? name : ariaLabel,
|
|
2606
|
+
"aria-labelledby": !sx && !ariaLabel ? fieldName?.labelId : void 0,
|
|
2591
2607
|
style: sx ? void 0 : cap,
|
|
2592
2608
|
ref: panel,
|
|
2593
2609
|
onClick: (e) => e.stopPropagation(),
|
|
@@ -3084,9 +3100,9 @@ import { jsx as jsx29, jsxs as jsxs24 } from "react/jsx-runtime";
|
|
|
3084
3100
|
function messages(id, hint, error) {
|
|
3085
3101
|
return [error && `${id}-error`, hint && `${id}-hint`].filter(Boolean).join(" ") || void 0;
|
|
3086
3102
|
}
|
|
3087
|
-
function Frame({ id, label, hint, error, required, children }) {
|
|
3103
|
+
function Frame({ id, labelId, label, hint, error, required, children }) {
|
|
3088
3104
|
return /* @__PURE__ */ jsxs24("div", { className: "ui-field", children: [
|
|
3089
|
-
/* @__PURE__ */ jsxs24("label", { className: "ui-field__label", htmlFor: id, children: [
|
|
3105
|
+
/* @__PURE__ */ jsxs24("label", { className: "ui-field__label", id: labelId, htmlFor: id, children: [
|
|
3090
3106
|
label,
|
|
3091
3107
|
required && /* @__PURE__ */ jsx29("span", { className: "ui-field__req", "aria-hidden": "true", children: "*" })
|
|
3092
3108
|
] }),
|
|
@@ -3109,7 +3125,8 @@ function wiring(id, hint, error) {
|
|
|
3109
3125
|
function Field({ id: suppliedId, label, hint, error, required, children }) {
|
|
3110
3126
|
const generatedId = useId10();
|
|
3111
3127
|
const id = suppliedId ?? generatedId;
|
|
3112
|
-
|
|
3128
|
+
const labelId = `${id}-label`;
|
|
3129
|
+
return /* @__PURE__ */ jsx29(Frame, { ...{ id, labelId, label, hint, error, required }, children: /* @__PURE__ */ jsx29(FieldContext.Provider, { value: { id, labelId }, children: children({ ...wiring(id, hint, error), required }) }) });
|
|
3113
3130
|
}
|
|
3114
3131
|
var TextField = forwardRef6(function TextField2({ label, hint, error, required, type = "text", unit, icon: glyph, className = "", ...props }, ref) {
|
|
3115
3132
|
const id = useId10();
|
|
@@ -3397,13 +3414,13 @@ var SearchField = forwardRef7(function SearchField2({ ariaLabel, className, ...p
|
|
|
3397
3414
|
});
|
|
3398
3415
|
|
|
3399
3416
|
// src/Toast.tsx
|
|
3400
|
-
import { createContext as
|
|
3417
|
+
import { createContext as createContext3, useCallback as useCallback5, useContext as useContext3, useEffect as useEffect12, useLayoutEffect as useLayoutEffect4, useRef as useRef18, useState as useState12 } from "react";
|
|
3401
3418
|
import { createPortal as createPortal4 } from "react-dom";
|
|
3402
3419
|
import { jsx as jsx32, jsxs as jsxs27 } from "react/jsx-runtime";
|
|
3403
|
-
var Context =
|
|
3420
|
+
var Context = createContext3(null);
|
|
3404
3421
|
var glyphs = { success: "circleCheck", danger: "circleX", warn: "circleAlert", info: "info", neutral: "bolt" };
|
|
3405
3422
|
function useToast() {
|
|
3406
|
-
const push =
|
|
3423
|
+
const push = useContext3(Context);
|
|
3407
3424
|
if (!push) throw new Error("useToast requires a Toast provider");
|
|
3408
3425
|
return push;
|
|
3409
3426
|
}
|
package/src/components/index.js
CHANGED
|
@@ -132,10 +132,10 @@ function withControlId(html) {
|
|
|
132
132
|
// aria-describedby, aria-invalid, required. Anything the control already spells
|
|
133
133
|
// out for itself wins — a caller who passed `invalid` to input() keeps their own
|
|
134
134
|
// aria-invalid. `true` renders a bare boolean attribute.
|
|
135
|
-
function
|
|
135
|
+
function withAttrsOn(html, openTag, attrs) {
|
|
136
136
|
const pairs = Object.entries(attrs).filter(([, v]) => v != null && v !== false);
|
|
137
137
|
if (!pairs.length) return html;
|
|
138
|
-
return html.replace(
|
|
138
|
+
return html.replace(openTag, (m, tag, rest, tail) => {
|
|
139
139
|
const add = pairs
|
|
140
140
|
.filter(([k]) => !new RegExp(`\\s${k}(?=[\\s=>/]|$)`).test(rest))
|
|
141
141
|
.map(([k, v]) => (v === true ? ` ${k}` : ` ${k}="${esc(v)}"`))
|
|
@@ -143,6 +143,38 @@ function withControlAttrs(html, attrs) {
|
|
|
143
143
|
return `<${tag}${rest}${add}${tail}>`;
|
|
144
144
|
});
|
|
145
145
|
}
|
|
146
|
+
const withControlAttrs = (html, attrs) => withAttrsOn(html, /<(input|textarea|select)\b([^>]*?)(\s*\/?)>/, attrs);
|
|
147
|
+
|
|
148
|
+
// A dropdown() is not a form element: its id sits on the outer div, which a
|
|
149
|
+
// <label for> cannot name, and its one input (the search field) is inside the
|
|
150
|
+
// panel. So field() wires the trigger button instead: the label and the value
|
|
151
|
+
// name it, and the hint or error describes it. why: docs/components.md#forms
|
|
152
|
+
const DD_ROOT = /<(div)\b((?=[^>]*\bdata-dropdown(?=[\s>]))[^>]*?)(\s*\/?)>/;
|
|
153
|
+
const DD_TRIGGER = /<(button)\b((?=[^>]*\bdata-dropdown-trigger\b)[^>]*?)(\s*\/?)>/;
|
|
154
|
+
const DD_PANEL = /<(div)\b((?=[^>]*\bdata-dropdown-panel\b)[^>]*?)(\s*\/?)>/;
|
|
155
|
+
const attrId = (tag) => tag?.match(/\sid="([^"]+)"/)?.[1] || null;
|
|
156
|
+
function withDropdownId(html, id) {
|
|
157
|
+
const root = html.match(DD_ROOT)[0];
|
|
158
|
+
const existing = attrId(root);
|
|
159
|
+
if (existing) return { html, id: existing };
|
|
160
|
+
const own = id || nextId('field');
|
|
161
|
+
return { html: html.replace(DD_ROOT, (m) => m.replace('<div', `<div id="${esc(own)}"`)), id: own };
|
|
162
|
+
}
|
|
163
|
+
function withDropdownAttrs(html, { rootId, labelId, describedby, invalid }) {
|
|
164
|
+
const trigger = html.match(DD_TRIGGER)?.[0] || '';
|
|
165
|
+
const triggerId = attrId(trigger) || `${rootId}-trigger`;
|
|
166
|
+
// A trigger that carries its own aria-label keeps it: the caller named it.
|
|
167
|
+
const named = labelId && !/\saria-label(?:ledby)?=/.test(trigger);
|
|
168
|
+
let out = withAttrsOn(html, DD_TRIGGER, {
|
|
169
|
+
id: named ? triggerId : null,
|
|
170
|
+
'aria-labelledby': named ? `${labelId} ${triggerId}` : null,
|
|
171
|
+
'aria-describedby': describedby,
|
|
172
|
+
'aria-invalid': invalid,
|
|
173
|
+
});
|
|
174
|
+
const panel = html.match(DD_PANEL)?.[0] || '';
|
|
175
|
+
if (labelId && !/\saria-label(?:ledby)?=/.test(panel)) out = withAttrsOn(out, DD_PANEL, { 'aria-labelledby': labelId });
|
|
176
|
+
return out;
|
|
177
|
+
}
|
|
146
178
|
|
|
147
179
|
// A labelled control with, optionally, a hint or an error under it.
|
|
148
180
|
//
|
|
@@ -156,21 +188,28 @@ function withControlAttrs(html, attrs) {
|
|
|
156
188
|
export function field({ label, hint, error, control = '', id, required = false } = {}) {
|
|
157
189
|
let ctl = control;
|
|
158
190
|
let forId = id;
|
|
191
|
+
const dd = DD_ROOT.test(control);
|
|
192
|
+
let ddId = null;
|
|
159
193
|
if (label && control) {
|
|
160
|
-
const r = withControlId(control);
|
|
194
|
+
const r = dd ? withDropdownId(control, id) : withControlId(control);
|
|
161
195
|
ctl = r.html;
|
|
162
196
|
forId = id || r.id;
|
|
197
|
+
if (dd) ddId = r.id;
|
|
163
198
|
}
|
|
164
199
|
// Error wins over hint — only one message shows, so only one id is minted.
|
|
165
200
|
const msg = error || hint;
|
|
166
201
|
const msgId = msg && control ? nextId(error ? 'field-err' : 'field-hint') : null;
|
|
167
|
-
ctl =
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
202
|
+
ctl = dd
|
|
203
|
+
? withDropdownAttrs(ctl, {
|
|
204
|
+
rootId: ddId, labelId: ddId && `${ddId}-label`, describedby: msgId, invalid: error ? 'true' : null,
|
|
205
|
+
})
|
|
206
|
+
: withControlAttrs(ctl, {
|
|
207
|
+
'aria-describedby': msgId,
|
|
208
|
+
'aria-invalid': error ? 'true' : null,
|
|
209
|
+
required: required || null,
|
|
210
|
+
});
|
|
172
211
|
const lab = label
|
|
173
|
-
? `<label class="ui-field__label"${forId ? ` for="${id ? esc(id) : forId}"` : ''}>${esc(label)}`
|
|
212
|
+
? `<label class="ui-field__label"${ddId ? ` id="${esc(ddId)}-label"` : ''}${forId ? ` for="${id ? esc(id) : forId}"` : ''}>${esc(label)}`
|
|
174
213
|
+ `${required ? '<span class="ui-field__req" aria-hidden="true">*</span>' : ''}</label>`
|
|
175
214
|
: '';
|
|
176
215
|
const foot = error
|
|
@@ -340,3 +340,20 @@
|
|
|
340
340
|
@media (prefers-reduced-motion: reduce) {
|
|
341
341
|
.ui-cmdk.is-open * { transition: none !important; }
|
|
342
342
|
}
|
|
343
|
+
|
|
344
|
+
/* Forced colours replace every author background, so a selected item takes the system
|
|
345
|
+
pair as its fill. `forced-color-adjust: none` is what keeps the pair from sitting on
|
|
346
|
+
Chromium's Canvas backplate, which would leave the label unreadable. Focus keeps its
|
|
347
|
+
own ring, drawn in CanvasText so the fill is never mistaken for it. */
|
|
348
|
+
@media (forced-colors: active) {
|
|
349
|
+
.ui-cmdk__item.is-active { forced-color-adjust: none; background: Highlight !important; color: HighlightText !important; --ring: var(--ring-width) solid CanvasText; }
|
|
350
|
+
.ui-cmdk__item.is-active .ui-cmdk__label,
|
|
351
|
+
.ui-cmdk__item.is-active .ui-cmdk__ic,
|
|
352
|
+
.ui-cmdk__item.is-active .ui-cmdk__desc,
|
|
353
|
+
.ui-cmdk__item.is-danger.is-active .ui-cmdk__label,
|
|
354
|
+
.ui-cmdk__item.is-danger.is-active .ui-cmdk__ic { color: HighlightText; }
|
|
355
|
+
.ui-cmdk--roomy .ui-cmdk__item.is-active .ui-cmdk__ic { background: none; }
|
|
356
|
+
.ui-cmdk__item.is-active .ui-cmdk__badge { background: HighlightText; color: Highlight; }
|
|
357
|
+
/* Shortcut caps inherit the adjust, so they take the pair too: the item's ink on its fill, with an ink edge. */
|
|
358
|
+
.ui-cmdk__item.is-active .ui-cmdk__key { background: Highlight; color: HighlightText; border-color: HighlightText; }
|
|
359
|
+
}
|
package/src/styles/layout.css
CHANGED
|
@@ -119,7 +119,7 @@
|
|
|
119
119
|
resting step. */
|
|
120
120
|
/* Re-tuned at #295, which took the light rail down a step with the page. Below 720px
|
|
121
121
|
the glyph is the whole row, so it is held at text grade rather than at 1.4.11's bar —
|
|
122
|
-
stories/apps/shell-states.test.js:
|
|
122
|
+
stories/apps/shell-states.test.js:1708 `a resting glyph is dimmer but still legible`. */
|
|
123
123
|
.ui-app__rail .ui-nav__ic svg { opacity: 0.66; }
|
|
124
124
|
|
|
125
125
|
/* A destructive row of the rail is a navigation row and rests like one; the danger
|
|
@@ -708,3 +708,41 @@
|
|
|
708
708
|
.ui-section-head { text-align: center; max-width: var(--panel-lg); margin: 0 auto clamp(40px, 6vh, 64px); }
|
|
709
709
|
.ui-section-head h2 { color: var(--strong); font-size: clamp(28px, 4vw, 40px); font-weight: 700; letter-spacing: -0.02em; margin-bottom: 14px; }
|
|
710
710
|
.ui-section-head p { color: var(--text); font-size: calc(var(--text-lg) - 1px); line-height: 1.6; }
|
|
711
|
+
|
|
712
|
+
/* Forced colours replace every author background, so a selected item takes the system
|
|
713
|
+
pair as its fill. `forced-color-adjust: none` is what keeps the pair from sitting on
|
|
714
|
+
Chromium's Canvas backplate, which would leave the label unreadable. Focus keeps its
|
|
715
|
+
own ring, drawn in CanvasText so the fill is never mistaken for it. The pair is
|
|
716
|
+
`!important` so the shell's danger ink and every hover or focus state yield to it. */
|
|
717
|
+
@media (forced-colors: active) {
|
|
718
|
+
.ui-app__rail .ui-nav__item:is(.is-active, .is-current):not(.is-disabled) { forced-color-adjust: none; background: Highlight !important; color: HighlightText !important; --ring: var(--ring-width) solid CanvasText; }
|
|
719
|
+
.ui-app__rail .ui-nav__item:is(.is-active, .is-current).is-disabled { forced-color-adjust: none; background: GrayText !important; color: Canvas !important; /* muted-ink: state — unavailable control */ }
|
|
720
|
+
.ui-app__rail .ui-nav__item.is-active:not(.is-disabled)::before { background: Highlight; }
|
|
721
|
+
.ui-app__rail .ui-nav__item.is-active.is-disabled::before { background: GrayText; }
|
|
722
|
+
/* A folded row is only an icon strip: the plate carries the fill, so the row paints none. */
|
|
723
|
+
.ui-app.is-collapsed .ui-app__rail .ui-nav__item:is(.is-active, .is-current) { isolation: isolate; background: none !important; }
|
|
724
|
+
.ui-app.is-collapsed .ui-app__rail .ui-nav__item.is-current:not(.is-active)::before {
|
|
725
|
+
content: ""; position: absolute; left: 0; top: 50%; width: var(--ui-nav-strip); height: 100%; border-radius: 10px;
|
|
726
|
+
transform: translateY(-50%); /* motion: still — a mark on the current group, centred and not travelling */
|
|
727
|
+
z-index: -1;
|
|
728
|
+
}
|
|
729
|
+
.ui-app.is-collapsed .ui-app__rail .ui-nav__item.is-current:not(.is-active):not(.is-disabled)::before { background: Highlight; }
|
|
730
|
+
.ui-app.is-collapsed .ui-app__rail .ui-nav__item.is-current.is-disabled:not(.is-active)::before { background: GrayText; }
|
|
731
|
+
/* The floating name of a selected folded row inherits the row's adjust, so it is painted in system colours. */
|
|
732
|
+
.ui-app.is-collapsed .ui-app__rail .ui-nav__item:is(.is-active, .is-current):is(:hover, :focus-visible) > .ui-nav__label {
|
|
733
|
+
forced-color-adjust: auto; background: Canvas; color: CanvasText; border-color: CanvasText;
|
|
734
|
+
}
|
|
735
|
+
}
|
|
736
|
+
@media (forced-colors: active) and (max-width: 720px) {
|
|
737
|
+
.ui-app .ui-app__rail .ui-nav__item:is(.is-active, .is-current) { isolation: isolate; background: none !important; }
|
|
738
|
+
.ui-app .ui-app__rail .ui-nav__item.is-current:not(.is-active)::before {
|
|
739
|
+
content: ""; position: absolute; left: 0; top: 50%; width: var(--ui-nav-strip); height: 100%; border-radius: 10px;
|
|
740
|
+
transform: translateY(-50%);
|
|
741
|
+
z-index: -1;
|
|
742
|
+
}
|
|
743
|
+
.ui-app .ui-app__rail .ui-nav__item.is-current:not(.is-active):not(.is-disabled)::before { background: Highlight; }
|
|
744
|
+
.ui-app .ui-app__rail .ui-nav__item.is-current.is-disabled:not(.is-active)::before { background: GrayText; }
|
|
745
|
+
.ui-app .ui-app__rail .ui-nav__item:is(.is-active, .is-current):is(:hover, :focus-visible) > .ui-nav__label {
|
|
746
|
+
forced-color-adjust: auto; background: Canvas; color: CanvasText; border-color: CanvasText;
|
|
747
|
+
}
|
|
748
|
+
}
|
package/src/styles/nav.css
CHANGED
|
@@ -365,3 +365,64 @@ a.ui-nav__crumb:hover { color: var(--strong); background: var(--surface); /* cod
|
|
|
365
365
|
a.ui-nav__crumb:focus-visible { outline: var(--ring); outline-offset: var(--ring-offset); }
|
|
366
366
|
.ui-nav__crumb.is-current { color: var(--strong); font-weight: 500; }
|
|
367
367
|
.ui-nav__crumb .ui-nav__ic svg { width: 15px; height: 15px; stroke: currentColor; fill: none; stroke-width: 2.5; }
|
|
368
|
+
|
|
369
|
+
/* Forced colours replace every author background, so a selected item takes the system
|
|
370
|
+
pair as its fill. `forced-color-adjust: none` is what keeps the pair from sitting on
|
|
371
|
+
Chromium's Canvas backplate, which would leave the label unreadable. Focus keeps its
|
|
372
|
+
own ring, drawn in CanvasText so the fill is never mistaken for it.
|
|
373
|
+
The pair is `!important` because it is the reader's palette: a danger wash, a hover
|
|
374
|
+
or a focus state must never repaint it. A child of the row inherits the adjust, so
|
|
375
|
+
each child that paints its own colour is repainted here as well. */
|
|
376
|
+
@media (forced-colors: active) {
|
|
377
|
+
.ui-nav__item.is-active:not(.is-disabled),
|
|
378
|
+
.ui-nav__item.is-current:not(.is-disabled),
|
|
379
|
+
.ui-nav__tab.is-active:not(.is-disabled),
|
|
380
|
+
.ui-nav--tabs.is-pill .ui-nav__tab.is-active:not(.is-disabled),
|
|
381
|
+
.ui-nav__crumb.is-current {
|
|
382
|
+
forced-color-adjust: none;
|
|
383
|
+
background: Highlight !important;
|
|
384
|
+
color: HighlightText !important;
|
|
385
|
+
--ring: var(--ring-width) solid CanvasText;
|
|
386
|
+
}
|
|
387
|
+
.ui-nav__item.is-active:not(.is-disabled) .ui-nav__ic svg,
|
|
388
|
+
.ui-nav__item.is-current:not(.is-disabled) .ui-nav__ic svg,
|
|
389
|
+
.ui-nav__tab.is-active:not(.is-disabled) .ui-nav__ic svg,
|
|
390
|
+
.ui-nav__crumb.is-current .ui-nav__ic svg { stroke: HighlightText; }
|
|
391
|
+
.ui-nav__item.is-active .ui-nav__caret,
|
|
392
|
+
.ui-nav__item.is-current .ui-nav__caret { border-color: currentColor; }
|
|
393
|
+
.ui-nav__item.is-active:not(.is-disabled) .ui-nav__badge,
|
|
394
|
+
.ui-nav__item.is-current:not(.is-disabled) .ui-nav__badge,
|
|
395
|
+
.ui-nav__tab.is-active:not(.is-disabled) .ui-nav__badge {
|
|
396
|
+
background: HighlightText !important;
|
|
397
|
+
color: Highlight !important;
|
|
398
|
+
}
|
|
399
|
+
/* A selected item that is unavailable must not look live, so it takes the disabled system pair. */
|
|
400
|
+
.ui-nav__item.is-active.is-disabled,
|
|
401
|
+
.ui-nav__item.is-current.is-disabled,
|
|
402
|
+
.ui-nav__tab.is-active.is-disabled {
|
|
403
|
+
forced-color-adjust: none;
|
|
404
|
+
background: GrayText !important;
|
|
405
|
+
color: Canvas !important; /* muted-ink: state — unavailable control */
|
|
406
|
+
}
|
|
407
|
+
.ui-nav__item.is-active.is-disabled .ui-nav__ic svg,
|
|
408
|
+
.ui-nav__item.is-current.is-disabled .ui-nav__ic svg,
|
|
409
|
+
.ui-nav__tab.is-active.is-disabled .ui-nav__ic svg { stroke: Canvas; }
|
|
410
|
+
.ui-nav__item.is-active.is-disabled .ui-nav__badge,
|
|
411
|
+
.ui-nav__item.is-current.is-disabled .ui-nav__badge,
|
|
412
|
+
.ui-nav__tab.is-active.is-disabled .ui-nav__badge {
|
|
413
|
+
background: Canvas !important;
|
|
414
|
+
color: GrayText !important; /* muted-ink: state — unavailable control */
|
|
415
|
+
}
|
|
416
|
+
/* A folded row is only an icon strip: the plate carries the fill, so the row paints none.
|
|
417
|
+
A current group takes the same plate as the current row it holds. */
|
|
418
|
+
.ui-nav--side.is-collapsed .ui-nav__item:is(.is-active, .is-current) { isolation: isolate; background: none !important; }
|
|
419
|
+
.ui-nav--side.is-collapsed .ui-nav__item.is-current:not(.is-active)::before {
|
|
420
|
+
content: ""; position: absolute; left: 0; top: 50%; width: var(--ui-nav-strip); height: 100%; border-radius: 10px;
|
|
421
|
+
transform: translateY(-50%); /* motion: still — a mark on the current group, centred and not travelling */
|
|
422
|
+
z-index: -1;
|
|
423
|
+
}
|
|
424
|
+
.ui-nav--side.is-collapsed .ui-nav__item:is(.is-active, .is-current):not(.is-disabled)::before { background: Highlight; }
|
|
425
|
+
.ui-nav--side.is-collapsed .ui-nav__item:is(.is-active, .is-current).is-disabled::before { background: GrayText; }
|
|
426
|
+
/* The fill is the one mark, so the underline bar, which would paint the accent inside it, is gone. */
|
|
427
|
+
.ui-nav--tabs.is-underline .ui-nav__tab.is-active::after { display: none; /* motion: still — the bar is not drawn while the fill marks the tab */ }
|
|
428
|
+
}
|
|
@@ -137,3 +137,22 @@ a.ui-pager__step,
|
|
|
137
137
|
a.ui-pager__page { color: var(--text); }
|
|
138
138
|
a.ui-pager__step:hover,
|
|
139
139
|
a.ui-pager__page:hover { color: var(--strong); }
|
|
140
|
+
|
|
141
|
+
/* Forced colours replace every author background, so a selected item takes the system
|
|
142
|
+
pair as its fill. `forced-color-adjust: none` is what keeps the pair from sitting on
|
|
143
|
+
Chromium's Canvas backplate, which would leave the label unreadable. Focus keeps its
|
|
144
|
+
own ring, drawn in CanvasText so the fill is never mistaken for it. */
|
|
145
|
+
@media (forced-colors: active) {
|
|
146
|
+
.ui-pager__page.is-current:not([disabled]) {
|
|
147
|
+
forced-color-adjust: none;
|
|
148
|
+
background: Highlight;
|
|
149
|
+
color: HighlightText;
|
|
150
|
+
--ring: var(--ring-width) solid CanvasText;
|
|
151
|
+
}
|
|
152
|
+
/* A busy pager must not look live, so its current page takes the disabled system colour. */
|
|
153
|
+
.ui-pager__page.is-current[disabled] {
|
|
154
|
+
forced-color-adjust: none;
|
|
155
|
+
background: GrayText;
|
|
156
|
+
color: Canvas; /* muted-ink: state — unavailable control */
|
|
157
|
+
}
|
|
158
|
+
}
|
package/src/styles/segmented.css
CHANGED
|
@@ -153,11 +153,31 @@
|
|
|
153
153
|
}
|
|
154
154
|
.ui-seg--underline button.is-active::before,
|
|
155
155
|
.ui-seg--underline button[aria-pressed="true"]::before { opacity: 1; transform: scaleX(1); }
|
|
156
|
-
|
|
157
|
-
|
|
156
|
+
.ui-seg button:focus-visible { outline: var(--ring); outline-offset: var(--ring-offset); }
|
|
157
|
+
.ui-seg button:disabled { /* muted-ink: state — unavailable choice */ color: var(--disabled-ink); cursor: not-allowed; }
|
|
158
|
+
|
|
159
|
+
/* Forced colours replace every author background, so a selected item takes the system
|
|
160
|
+
pair as its fill. `forced-color-adjust: none` is what keeps the pair from sitting on
|
|
161
|
+
Chromium's Canvas backplate, which would leave the label unreadable. Focus keeps its
|
|
162
|
+
own ring, drawn in CanvasText so the fill is never mistaken for it. */
|
|
158
163
|
@media (forced-colors: active) {
|
|
164
|
+
.ui-seg button.is-active,
|
|
165
|
+
.ui-seg button[aria-pressed="true"],
|
|
166
|
+
.ui-seg button[aria-selected="true"] {
|
|
167
|
+
forced-color-adjust: none;
|
|
168
|
+
background: Highlight;
|
|
169
|
+
color: HighlightText;
|
|
170
|
+
--ring: var(--ring-width) solid CanvasText;
|
|
171
|
+
}
|
|
172
|
+
/* The fill is the one mark, so the underline bar, which would paint the accent inside it, is gone. */
|
|
159
173
|
.ui-seg--underline button.is-active::before,
|
|
160
|
-
.ui-seg--underline button[aria-pressed="true"]::before
|
|
174
|
+
.ui-seg--underline button[aria-pressed="true"]::before,
|
|
175
|
+
.ui-seg--underline button[aria-selected="true"]::before { display: none; /* motion: still — the bar is not drawn while the fill marks the choice */ }
|
|
176
|
+
/* A disabled selected choice must not look live, so it takes the disabled system colour. */
|
|
177
|
+
.ui-seg button.is-active:disabled,
|
|
178
|
+
.ui-seg button[aria-pressed="true"]:disabled,
|
|
179
|
+
.ui-seg button[aria-selected="true"]:disabled {
|
|
180
|
+
background: GrayText;
|
|
181
|
+
color: Canvas; /* muted-ink: state — unavailable choice */
|
|
182
|
+
}
|
|
161
183
|
}
|
|
162
|
-
.ui-seg button:focus-visible { outline: var(--ring); outline-offset: var(--ring-offset); }
|
|
163
|
-
.ui-seg button:disabled { /* muted-ink: state — unavailable choice */ color: var(--disabled-ink); cursor: not-allowed; }
|