@apliteni/apliteni-ui 0.12.0 → 0.23.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/package.json +1 -1
- package/react/dist/index.css +19 -0
- package/react/dist/index.d.ts +43 -2
- package/react/dist/index.js +98 -6
- package/src/assets/icons.js +5 -26
- package/src/components/account-nav.js +1 -1
- package/src/components/confirm.js +27 -28
- package/src/components/drawer.js +27 -28
- package/src/components/dropdown.js +26 -23
- package/src/components/index.js +9 -13
- package/src/components/loading.js +126 -0
- package/src/components/nav.js +7 -20
- package/src/components/overlay.js +7 -16
- package/src/components/shell.js +10 -9
- package/src/components/success.js +8 -17
- package/src/components/topbar.js +8 -12
- package/src/index.css +6 -1
- package/src/index.js +1 -0
- package/src/inline.js +4 -0
- package/src/styles/badge.css +1 -1
- package/src/styles/base.css +5 -18
- package/src/styles/button.css +18 -5
- package/src/styles/callout.css +37 -26
- package/src/styles/card.css +1 -1
- package/src/styles/code.css +7 -1
- package/src/styles/confirm.css +10 -13
- package/src/styles/drawer.css +9 -6
- package/src/styles/dropdown.css +14 -7
- package/src/styles/empty.css +1 -1
- package/src/styles/feedback.css +25 -16
- package/src/styles/footer.css +15 -6
- package/src/styles/input.css +28 -2
- package/src/styles/layout.css +14 -10
- package/src/styles/loading.css +129 -0
- package/src/styles/motion.css +21 -39
- package/src/styles/nav.css +14 -12
- package/src/styles/reduced-motion.css +21 -0
- package/src/styles/success.css +8 -8
- package/src/styles/table.css +20 -17
- package/src/styles/topbar.css +27 -12
- package/src/tokens/accents.css +10 -14
- package/src/tokens/tokens.css +95 -22
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
// The pending and denied states of a SCREEN, not of one control.
|
|
2
|
+
//
|
|
3
|
+
// busyRegion() is one live region that outlives what it reports on; setBusy()
|
|
4
|
+
// swaps its body and writes a line into the sr-only node already inside it.
|
|
5
|
+
// Three things land in that body: skeleton() while it fetches, your markup once
|
|
6
|
+
// the rows arrive, deniedState() when the answer came back 403.
|
|
7
|
+
//
|
|
8
|
+
// why: docs/specification.md#pending-and-denied-states
|
|
9
|
+
import { icon } from '../assets/icons.js';
|
|
10
|
+
import { button, esc } from './index.js';
|
|
11
|
+
|
|
12
|
+
const cx = (...a) => a.filter(Boolean).join(' ');
|
|
13
|
+
|
|
14
|
+
// ---- Skeleton ------------------------------------------------------------
|
|
15
|
+
// The placeholder shape. `lines` is a count, or an array of widths when the
|
|
16
|
+
// varied ragged edge of real prose matters (['100%','92%','60%']). `height`
|
|
17
|
+
// makes it one solid block instead — a chart, a map, an avatar. The shimmer is
|
|
18
|
+
// .m-skeleton from the motion library, so there is one animation to own and it
|
|
19
|
+
// is already inside that library's reduced-motion net.
|
|
20
|
+
export function skeleton({ lines = 3, width, height, radius, className = '' } = {}) {
|
|
21
|
+
const widths = Array.isArray(lines) ? lines : Array.isArray(width) ? width : null;
|
|
22
|
+
const n = widths ? widths.length : Math.max(1, lines | 0);
|
|
23
|
+
const styleFor = (i) => {
|
|
24
|
+
const w = widths ? widths[i] : (typeof width === 'string' ? width : null);
|
|
25
|
+
const bits = [w && `width:${w}`, height && `height:${height}`, radius && `border-radius:${radius}`];
|
|
26
|
+
const s = bits.filter(Boolean).join(';');
|
|
27
|
+
return s ? ` style="${esc(s)}"` : '';
|
|
28
|
+
};
|
|
29
|
+
const bars = Array.from({ length: n }, (_, i) =>
|
|
30
|
+
`<span class="ui-skel__bar m-skeleton"${styleFor(i)}></span>`).join('');
|
|
31
|
+
return `<div class="${cx('ui-skel', className)}" aria-hidden="true">${bars}</div>`;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// A table's worth of skeleton — `rows` × `cols` of bar, laid out on a grid so
|
|
35
|
+
// the placeholder has the column rhythm the real table will have. Screens that
|
|
36
|
+
// load a table are the common case, and hand-rolling this per screen is how
|
|
37
|
+
// four slightly different loading tables get shipped.
|
|
38
|
+
export function skeletonTable({ rows = 5, cols = 4, head = true } = {}) {
|
|
39
|
+
const row = (cls) => `<div class="${cls}">`
|
|
40
|
+
+ Array.from({ length: Math.max(1, cols | 0) }, () => '<span class="ui-skel__bar m-skeleton"></span>').join('')
|
|
41
|
+
+ '</div>';
|
|
42
|
+
const body = Array.from({ length: Math.max(1, rows | 0) }, () => row('ui-skel__row')).join('');
|
|
43
|
+
return `<div class="ui-skel ui-skel--table" style="--skel-cols:${Math.max(1, cols | 0)}" aria-hidden="true">`
|
|
44
|
+
+ `${head ? row('ui-skel__row ui-skel__row--head') : ''}${body}</div>`;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// ---- The region ----------------------------------------------------------
|
|
48
|
+
// `label` is what is spoken while it works, `readyLabel` the fallback for when
|
|
49
|
+
// it finishes without the caller supplying a line. Both are parked on the
|
|
50
|
+
// element so setBusy() can find them and the caller never repeats itself.
|
|
51
|
+
//
|
|
52
|
+
// `body` overrides the default skeleton (pass your own placeholder, or the
|
|
53
|
+
// already-loaded content when the region starts ready).
|
|
54
|
+
export function busyRegion({
|
|
55
|
+
label = 'Loading…', readyLabel = 'Loaded', busy = true,
|
|
56
|
+
body, lines = 3, className = '',
|
|
57
|
+
} = {}) {
|
|
58
|
+
const inner = body != null ? body : skeleton({ lines });
|
|
59
|
+
return `<div class="${cx('ui-busy', className)}" data-busy`
|
|
60
|
+
+ ` data-busy-label="${esc(label)}" data-busy-ready="${esc(readyLabel)}"`
|
|
61
|
+
+ ` role="status" aria-live="polite" aria-busy="${busy ? 'true' : 'false'}">`
|
|
62
|
+
+ `<span class="ui-sr" data-busy-msg>${esc(busy ? label : readyLabel)}</span>`
|
|
63
|
+
+ `<div class="ui-busy__body" data-busy-body>${inner}</div>`
|
|
64
|
+
+ '</div>';
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Flip a region between busy and ready, and say so. Writing into
|
|
68
|
+
// [data-busy-msg] IS the announcement. Callers pass `message` for the specific
|
|
69
|
+
// line ("14 invoices", "You don't have access to this report"); the region's
|
|
70
|
+
// own labels are the fallback.
|
|
71
|
+
//
|
|
72
|
+
// Accepts the region, a selector, or any ancestor of it. Returns the region, or
|
|
73
|
+
// null when there is nothing to update — safe to call against a torn-down view.
|
|
74
|
+
export function setBusy(root, { busy = false, message, body } = {}) {
|
|
75
|
+
const el = typeof root === 'string' ? document.querySelector(root) : root;
|
|
76
|
+
if (!el || typeof el.querySelector !== 'function') return null;
|
|
77
|
+
const region = el.matches && el.matches('[data-busy]') ? el : el.querySelector('[data-busy]');
|
|
78
|
+
if (!region) return null;
|
|
79
|
+
|
|
80
|
+
region.setAttribute('aria-busy', busy ? 'true' : 'false');
|
|
81
|
+
if (body != null) {
|
|
82
|
+
const slot = region.querySelector('[data-busy-body]');
|
|
83
|
+
if (slot) slot.innerHTML = body;
|
|
84
|
+
}
|
|
85
|
+
const msg = region.querySelector('[data-busy-msg]');
|
|
86
|
+
if (msg) {
|
|
87
|
+
const fallback = busy
|
|
88
|
+
? (region.dataset.busyLabel || 'Loading…')
|
|
89
|
+
: (region.dataset.busyReady || 'Loaded');
|
|
90
|
+
msg.textContent = message == null ? fallback : String(message);
|
|
91
|
+
}
|
|
92
|
+
return region;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// ---- Permission denied ---------------------------------------------------
|
|
96
|
+
// The 403 screen. Same shape as emptyState() — a mark, a title, a line, some
|
|
97
|
+
// actions — because to a reader they are the same event: the thing you came for
|
|
98
|
+
// is not here. What separates them is that this one owes an explanation, so
|
|
99
|
+
// `need` is a first-class slot rather than something to bury in `sub`.
|
|
100
|
+
//
|
|
101
|
+
// `need` names the scope or role the reader is missing, verbatim, as code. A
|
|
102
|
+
// reader who can act on "you need reports.read" acts on it; "insufficient
|
|
103
|
+
// permissions" sends them to open a ticket to find out what to ask for.
|
|
104
|
+
//
|
|
105
|
+
// No role and no live region here — see the file header. When this lands as the
|
|
106
|
+
// answer to a fetch, put it inside a busyRegion() and the region announces it.
|
|
107
|
+
export function deniedState({
|
|
108
|
+
title = 'You don’t have access',
|
|
109
|
+
sub = '',
|
|
110
|
+
need = '',
|
|
111
|
+
actions = [],
|
|
112
|
+
icon: ic = 'lock',
|
|
113
|
+
className = '',
|
|
114
|
+
} = {}) {
|
|
115
|
+
const needEl = need
|
|
116
|
+
? `<div class="ui-denied__need">Needs <code class="ui-code">${esc(need)}</code></div>`
|
|
117
|
+
: '';
|
|
118
|
+
const actionsEl = actions.length
|
|
119
|
+
? `<div class="ui-denied__actions">${actions.map((a) => button({ size: 'md', ...a })).join('')}</div>`
|
|
120
|
+
: '';
|
|
121
|
+
return `<div class="${cx('ui-denied', className)}">`
|
|
122
|
+
+ `<div class="ui-denied__seal" aria-hidden="true">${icon(ic)}</div>`
|
|
123
|
+
+ `<div class="ui-denied__title">${esc(title)}</div>`
|
|
124
|
+
+ `${sub ? `<div class="ui-denied__sub">${esc(sub)}</div>` : ''}`
|
|
125
|
+
+ `${needEl}${actionsEl}</div>`;
|
|
126
|
+
}
|
package/src/components/nav.js
CHANGED
|
@@ -1,24 +1,11 @@
|
|
|
1
|
-
// Navigation — the kit's primary wayfinding primitives.
|
|
2
|
-
//
|
|
3
|
-
//
|
|
1
|
+
// Navigation — the kit's primary wayfinding primitives. `nav({ variant })`
|
|
2
|
+
// dispatches to sidebarNav(), navTabs() and breadcrumbs(), each also exported
|
|
3
|
+
// directly: the three share tokens and classes but almost no markup, so a single
|
|
4
|
+
// options bag would mean wildly different fields per variant.
|
|
4
5
|
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
// variant: 'tabs' → navTabs() horizontal page tabs with an active
|
|
9
|
-
// underline (default) or pill.
|
|
10
|
-
// variant: 'breadcrumbs' → breadcrumbs() the `Finance / Payouts` trail.
|
|
11
|
-
//
|
|
12
|
-
// Why an umbrella AND named exports (unlike dropdown, which is one factory): the
|
|
13
|
-
// three variants don't share a body the way select/menu share a panel, so a
|
|
14
|
-
// single options bag would mean wildly different fields per variant. The named
|
|
15
|
-
// exports keep each call site honest; `nav()` stays the discoverable entry the
|
|
16
|
-
// issue asks for and the one thing to import when the variant is data-driven.
|
|
17
|
-
//
|
|
18
|
-
// These are NAVIGATION controls (links between locations), not tab panels — so
|
|
19
|
-
// tabs render as <nav> + <a aria-current="page">, not role="tablist" (that's
|
|
20
|
-
// what segmented() is for). Only the collapsible sidebar groups need JS; wire
|
|
21
|
-
// them once after mount with wireNav() (preview.js does this for Storybook).
|
|
6
|
+
// These are NAVIGATION controls, not tab panels — so tabs render as
|
|
7
|
+
// <nav> + <a aria-current="page">, not role="tablist" (that is what segmented()
|
|
8
|
+
// is for). Only the collapsible sidebar groups need JS; wire with wireNav().
|
|
22
9
|
import { esc, icon } from './index.js';
|
|
23
10
|
|
|
24
11
|
const cx = (...a) => a.filter(Boolean).join(' ');
|
|
@@ -154,23 +154,14 @@ export function pushOverlay(root, panel, dismiss, layer) {
|
|
|
154
154
|
/**
|
|
155
155
|
* Take on a root that arrived already open — markup rendered with `open: true`,
|
|
156
156
|
* which nobody called open…() for. Without this its aria-modal is a claim the
|
|
157
|
-
* page contradicts
|
|
157
|
+
* page contradicts.
|
|
158
158
|
*
|
|
159
|
-
* It goes in by
|
|
160
|
-
*
|
|
161
|
-
*
|
|
162
|
-
*
|
|
163
|
-
*
|
|
164
|
-
*
|
|
165
|
-
* E, steps 8 and 9). That comparison earns its keep whenever adoption order and
|
|
166
|
-
* document order disagree: a root inserted above one already on the stack and
|
|
167
|
-
* wired after it, and — in a browser only — a consumer stylesheet that lifts the
|
|
168
|
-
* drawer onto the confirm's layer, where wiring runs drawer-first however the
|
|
169
|
-
* markup is ordered. A browser refines the layer from the live z-index; JSDOM has
|
|
170
|
-
* none to give, so the tests hold OVERLAY_LAYER to the sheets instead. A root that
|
|
171
|
-
* renders closed is left alone: wiring is not an opening, and it waits for the one
|
|
172
|
-
* that is. A specimen never reaches here at all — it carries no hook for the
|
|
173
|
-
* wiring to find.
|
|
159
|
+
* It goes in by PAINT ORDER, because wiring has no history to order by. Document
|
|
160
|
+
* position only separates two overlays on the same layer, and there it is the
|
|
161
|
+
* right answer rather than a fallback: at equal stack levels the later root
|
|
162
|
+
* paints on top (CSS 2.2 Appendix E, steps 8 and 9).
|
|
163
|
+
*
|
|
164
|
+
* A root that renders closed is left alone — wiring is not an opening.
|
|
174
165
|
*/
|
|
175
166
|
export function adoptOverlay(root, panel, dismiss, layer) {
|
|
176
167
|
if (!root.classList.contains('is-open')) return;
|
package/src/components/shell.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
// with a breadcrumb trail the caller owns. `accountShell()` is a thin preset
|
|
4
4
|
// over it that keeps the topbar, so the published /account API still works.
|
|
5
5
|
// Call wireTopbar() once after mounting to wire the account menu + theme toggle.
|
|
6
|
-
// why: docs/
|
|
6
|
+
// why: docs/specification.md#the-page-shell
|
|
7
7
|
import { topbar as productTopbar } from './topbar.js';
|
|
8
8
|
import { esc, icon } from './index.js';
|
|
9
9
|
import { sidebarNav, breadcrumbs } from './nav.js';
|
|
@@ -89,16 +89,17 @@ const toTopbar = (t) => {
|
|
|
89
89
|
return out;
|
|
90
90
|
};
|
|
91
91
|
|
|
92
|
-
// `maxWidth` lands inside a style attribute,
|
|
93
|
-
//
|
|
94
|
-
//
|
|
95
|
-
//
|
|
96
|
-
//
|
|
97
|
-
|
|
92
|
+
// `maxWidth` lands inside a style attribute, so a length is all this accepts —
|
|
93
|
+
// a number and a unit, or `none`. Anything else yields '' and the caller writes
|
|
94
|
+
// no style attribute, letting layout.css fall through to --measure. It must
|
|
95
|
+
// REMOVE the property rather than pass a default or a bad value on: a custom
|
|
96
|
+
// property accepts any token stream, so garbage is a valid declaration that
|
|
97
|
+
// drops the column to `none`, the full track.
|
|
98
|
+
// why: docs/specification.md#widths
|
|
98
99
|
const LENGTH = /^(?:\d+|\d*\.\d+)(?:px|rem|em|ch|%|vw)$/;
|
|
99
100
|
const mainMax = (v) => {
|
|
100
101
|
const s = str(v).trim();
|
|
101
|
-
return s === 'none' || LENGTH.test(s) ? s :
|
|
102
|
+
return s === 'none' || LENGTH.test(s) ? s : '';
|
|
102
103
|
};
|
|
103
104
|
|
|
104
105
|
// The one pass. Each key names the function that settles it; nothing else in
|
|
@@ -194,7 +195,7 @@ export function appShell(options = {}) {
|
|
|
194
195
|
${rail}
|
|
195
196
|
${railUser(account)}
|
|
196
197
|
</div>
|
|
197
|
-
<main class="ui-app__main" style="--ui-app-main: ${maxWidth}">
|
|
198
|
+
<main class="ui-app__main"${maxWidth ? ` style="--ui-app-main: ${maxWidth}"` : ''}>
|
|
198
199
|
${crumbs.length ? breadcrumbs({ items: crumbs }) : ''}
|
|
199
200
|
${title ? `<h1>${title}</h1>` : ''}
|
|
200
201
|
${sub ? `<p class="ui-app__sub">${sub}</p>` : ''}
|
|
@@ -1,22 +1,13 @@
|
|
|
1
|
-
// Success / confirmation surface
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
// no confetti, no sweep). Accent-aware via the kit tokens; the success mark
|
|
6
|
-
// stays on the --green family. Styles ship in styles/success.css.
|
|
1
|
+
// Success / confirmation surface. One factory, three layouts and three
|
|
2
|
+
// backdrops, an SVG check that draws itself in, optional confetti and an
|
|
3
|
+
// optional auto-redirect countdown. Every motion path is reduced-motion safe.
|
|
4
|
+
// Accent-aware, but the success mark stays on the --green family.
|
|
7
5
|
//
|
|
8
|
-
// container.innerHTML = success({
|
|
9
|
-
// title: 'Feedback sent',
|
|
10
|
-
// body: 'It goes straight to the strategy owner.',
|
|
11
|
-
// actions: [
|
|
12
|
-
// { label: 'Back to strategy', variant: 'primary', icon: 'compass' },
|
|
13
|
-
// { label: 'Send another', variant: 'ghost' },
|
|
14
|
-
// ],
|
|
15
|
-
// });
|
|
6
|
+
// container.innerHTML = success({ title, body, actions: [{ label, variant }] });
|
|
16
7
|
//
|
|
17
|
-
// The check animation is pure CSS, so string-rendered markup animates on its
|
|
18
|
-
//
|
|
19
|
-
//
|
|
8
|
+
// The check animation is pure CSS, so string-rendered markup animates on its own
|
|
9
|
+
// once mounted. A live countdown is opt-in via wireSuccess(); the markup alone
|
|
10
|
+
// shows the ring sweep and a static number.
|
|
20
11
|
import { esc, button } from './index.js';
|
|
21
12
|
|
|
22
13
|
// Self-drawing check: a faint track disc, a filled accent disc that springs in,
|
package/src/components/topbar.js
CHANGED
|
@@ -52,19 +52,15 @@ export function versionSwitcher(versions = [], activeIdx = 0) {
|
|
|
52
52
|
`<div class="vsw__menu" data-dropdown-panel role="listbox" aria-label="Version">${opts}</div></div>`;
|
|
53
53
|
}
|
|
54
54
|
|
|
55
|
-
// `nav`
|
|
56
|
-
//
|
|
57
|
-
//
|
|
58
|
-
//
|
|
59
|
-
// drift #127 was filed about. Every field below is interpolated raw, so what
|
|
60
|
-
// arrives has to arrive escaped — accountMenuNav() is what does that.
|
|
55
|
+
// `nav` mirrors the account sidebar, DERIVED from the one ACCOUNT_NAV definition
|
|
56
|
+
// rather than restated: a second literal agreed with it by hand about the icon
|
|
57
|
+
// and disagreed about the encoding, which is the drift #127 was filed about.
|
|
58
|
+
// Every field below is interpolated raw, so what arrives has to arrive escaped.
|
|
61
59
|
//
|
|
62
|
-
// `initials` is the avatar
|
|
63
|
-
//
|
|
64
|
-
// the
|
|
65
|
-
//
|
|
66
|
-
// from the caller's own strings and passes it down beside them. Left out, it is
|
|
67
|
-
// computed here from `name` and `email`, exactly where it always came from.
|
|
60
|
+
// `initials` is the avatar. A derived value has to be derived BEFORE the
|
|
61
|
+
// escaping — `<Ada>` and `<Ada>` do not begin with the same character — so
|
|
62
|
+
// shell.js computes the mark from the caller's own strings and passes it down
|
|
63
|
+
// beside them. Left out, it is computed here from `name` and `email`.
|
|
68
64
|
export function accountMenu({
|
|
69
65
|
name = 'Ada Lovelace', email = 'ada@apliteni.com', active = 'prefs', nav, initials: mark,
|
|
70
66
|
} = {}) {
|
package/src/index.css
CHANGED
|
@@ -1,12 +1,16 @@
|
|
|
1
1
|
/* ============================================================================
|
|
2
2
|
* apliteni-ui — full stylesheet.
|
|
3
|
-
* Import once
|
|
3
|
+
* Import once: `import 'apliteni-ui/css'`. A single component sheet is NOT
|
|
4
|
+
* addressable — package.json `exports` has no wildcard for ./styles/*, so a deep
|
|
5
|
+
* path into src/styles/ does not resolve. To take one component, read it by name
|
|
6
|
+
* through `apliteni-ui/inline` (`import { topbarCss } from …`).
|
|
4
7
|
* Requires the Poppins font (loaded by the host page or Storybook preview).
|
|
5
8
|
* ========================================================================== */
|
|
6
9
|
@import "./tokens/brand.generated.css";
|
|
7
10
|
@import "./tokens/tokens.css";
|
|
8
11
|
@import "./tokens/accents.css";
|
|
9
12
|
@import "./styles/base.css";
|
|
13
|
+
@import "./styles/reduced-motion.css";
|
|
10
14
|
@import "./styles/motion.css";
|
|
11
15
|
@import "./styles/button.css";
|
|
12
16
|
@import "./styles/card.css";
|
|
@@ -27,3 +31,4 @@
|
|
|
27
31
|
@import "./styles/layout.css";
|
|
28
32
|
@import "./styles/feedback.css";
|
|
29
33
|
@import "./styles/success.css";
|
|
34
|
+
@import "./styles/loading.css";
|
package/src/index.js
CHANGED
|
@@ -12,6 +12,7 @@ export * from './components/footer.js';
|
|
|
12
12
|
export * from './components/feedback.js';
|
|
13
13
|
export * from './components/toasts.js';
|
|
14
14
|
export * from './components/success.js';
|
|
15
|
+
export * from './components/loading.js';
|
|
15
16
|
export * from './assets/icons.js';
|
|
16
17
|
export * from './assets/brand.js';
|
|
17
18
|
export * from './motion.js';
|
package/src/inline.js
CHANGED
|
@@ -28,6 +28,7 @@ export const topbarCss = read('styles/topbar.css');
|
|
|
28
28
|
// Individual component stylesheets, addressable by name.
|
|
29
29
|
export const styles = {
|
|
30
30
|
base: baseCss,
|
|
31
|
+
reducedMotion: read('styles/reduced-motion.css'),
|
|
31
32
|
motion: read('styles/motion.css'),
|
|
32
33
|
button: read('styles/button.css'),
|
|
33
34
|
card: read('styles/card.css'),
|
|
@@ -48,12 +49,14 @@ export const styles = {
|
|
|
48
49
|
layout: read('styles/layout.css'),
|
|
49
50
|
feedback: read('styles/feedback.css'),
|
|
50
51
|
success: read('styles/success.css'),
|
|
52
|
+
loading: read('styles/loading.css'),
|
|
51
53
|
};
|
|
52
54
|
|
|
53
55
|
// Everything, in the same order as index.css. `tokensCss` first so cascade is right.
|
|
54
56
|
export const cssText = [
|
|
55
57
|
tokensCss,
|
|
56
58
|
styles.base,
|
|
59
|
+
styles.reducedMotion,
|
|
57
60
|
styles.motion,
|
|
58
61
|
styles.button,
|
|
59
62
|
styles.card,
|
|
@@ -74,4 +77,5 @@ export const cssText = [
|
|
|
74
77
|
styles.layout,
|
|
75
78
|
styles.feedback,
|
|
76
79
|
styles.success,
|
|
80
|
+
styles.loading,
|
|
77
81
|
].join('\n');
|
package/src/styles/badge.css
CHANGED
|
@@ -72,7 +72,7 @@
|
|
|
72
72
|
.ui-dot.is-live {
|
|
73
73
|
background: var(--green);
|
|
74
74
|
box-shadow: 0 0 0 0 var(--glow-green);
|
|
75
|
-
animation: ui-pulse 2s var(--ease) infinite;
|
|
75
|
+
animation: ui-pulse 2s var(--ease) infinite; /* motion: ambient — a liveness heartbeat, not a response to anything the reader did */
|
|
76
76
|
}
|
|
77
77
|
@keyframes ui-pulse {
|
|
78
78
|
0% { box-shadow: 0 0 0 0 color-mix(in srgb, var(--green) 55%, transparent); }
|
package/src/styles/base.css
CHANGED
|
@@ -88,7 +88,7 @@ a {
|
|
|
88
88
|
}
|
|
89
89
|
|
|
90
90
|
/* Layout helpers used by the example apps */
|
|
91
|
-
.ui-container { width: 100%; max-width:
|
|
91
|
+
.ui-container { width: 100%; max-width: var(--container); margin: 0 auto; padding: 0 clamp(14px, 2.4vw, 26px); }
|
|
92
92
|
.ui-stack > * + * { margin-top: var(--space-4); }
|
|
93
93
|
|
|
94
94
|
/* Section eyebrow — uppercase caption used across cards and headers */
|
|
@@ -102,23 +102,10 @@ a {
|
|
|
102
102
|
|
|
103
103
|
/* Sensible default size for inline icons that a parent rule doesn't size.
|
|
104
104
|
A floor, not a ceiling: :where() holds the whole filter at zero specificity,
|
|
105
|
-
so this weighs (0,0,1)
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
It did, for every one of them bar three, and for `.ui-fbck` besides — that
|
|
110
|
-
one is a class on the svg itself, so it is easy to miss when you go looking.
|
|
111
|
-
The count is deliberately not written here: it moved once already. Two gates
|
|
112
|
-
keep the rules that compete with this one honest, each over the ground it
|
|
113
|
-
actually sweeps: src/styles/icon-size.test.js reads the stylesheets
|
|
114
|
-
src/index.css imports, and scripts/icon-size-surfaces.test.js reads the
|
|
115
|
-
surfaces the kit renders — the landing site's pages and the Storybook stories.
|
|
116
|
-
Neither claims more, and the second one's header lists what it declines to
|
|
117
|
-
claim; read it before trusting a green run. Two gaps are worth knowing here: a
|
|
118
|
-
reset scoped to an ancestor that a subject's own selector does not name is out
|
|
119
|
-
of reach of both, and .storybook/preview.js imports src/index.css into every
|
|
120
|
-
story iframe, which makes .storybook/ a third rendering surface neither gate
|
|
121
|
-
sweeps. Nothing in there sizes an icon today. */
|
|
105
|
+
so this weighs (0,0,1) and any component rule that sizes an icon outranks it.
|
|
106
|
+
The count of those rules is deliberately not written here — it moved once
|
|
107
|
+
already, and two gates hold it over the ground each actually sweeps.
|
|
108
|
+
why: CONTRIBUTING.md#the-reset-is-a-floor-and-its-specificity-is-the-whole-of-that */
|
|
122
109
|
svg:where(:not([width]):not([height])) {
|
|
123
110
|
width: 1.1em;
|
|
124
111
|
height: 1.1em;
|
package/src/styles/button.css
CHANGED
|
@@ -37,7 +37,7 @@
|
|
|
37
37
|
}
|
|
38
38
|
.ui-btn:active { transform: translateY(1px); }
|
|
39
39
|
|
|
40
|
-
.ui-btn svg { width: 16px; height: 16px; flex: none; }
|
|
40
|
+
.ui-btn svg { width: 16px; height: 16px; flex: none; stroke-width: 2.4; }
|
|
41
41
|
|
|
42
42
|
/* -- Variants ----------------------------------------------------------- */
|
|
43
43
|
.ui-btn--primary {
|
|
@@ -82,12 +82,25 @@
|
|
|
82
82
|
.ui-btn--block { width: 100%; }
|
|
83
83
|
|
|
84
84
|
/* -- States ------------------------------------------------------------- */
|
|
85
|
+
/* Off, in every variant. A disabled button has no variant identity — primary,
|
|
86
|
+
secondary and danger all take the same flat pair, because the accent IS the
|
|
87
|
+
"press me" and keeping a faded version of it was what put a disabled primary
|
|
88
|
+
at 1.48:1 (white on a washed-out purple). Dropping the accent is most of what
|
|
89
|
+
makes this read as inert; the quiet ink is the rest. #220 */
|
|
85
90
|
.ui-btn:disabled,
|
|
86
91
|
.ui-btn[aria-disabled="true"] {
|
|
87
|
-
|
|
92
|
+
background: var(--disabled-surface);
|
|
93
|
+
border-color: var(--disabled-border);
|
|
94
|
+
color: var(--disabled-ink);
|
|
88
95
|
cursor: not-allowed;
|
|
89
96
|
pointer-events: none;
|
|
90
97
|
}
|
|
98
|
+
/* A ghost button draws no box when it is on, so it draws none when it is off. */
|
|
99
|
+
.ui-btn--ghost:disabled,
|
|
100
|
+
.ui-btn--ghost[aria-disabled="true"] {
|
|
101
|
+
background: transparent;
|
|
102
|
+
border-color: transparent;
|
|
103
|
+
}
|
|
91
104
|
|
|
92
105
|
/* Busy: keep the label, run an indeterminate accent shimmer along the base, and
|
|
93
106
|
the button is disabled (not clickable). Pass busy + the .ui-btn__bars markup.
|
|
@@ -109,7 +122,7 @@
|
|
|
109
122
|
overflow: hidden;
|
|
110
123
|
/* Clean entrance: grow + fade in on state change, once. */
|
|
111
124
|
transform-origin: center;
|
|
112
|
-
animation: ui-bars-in
|
|
125
|
+
animation: ui-bars-in var(--dur-fast) var(--ease) both;
|
|
113
126
|
}
|
|
114
127
|
.ui-btn--sm .ui-btn__bars { left: 7px; right: 7px; bottom: 4px; }
|
|
115
128
|
.ui-btn--lg .ui-btn__bars { left: 14px; right: 14px; bottom: 7px; }
|
|
@@ -130,8 +143,8 @@
|
|
|
130
143
|
color-mix(in srgb, var(--accent-contrast) 35%, transparent),
|
|
131
144
|
var(--accent-contrast));
|
|
132
145
|
}
|
|
133
|
-
.ui-btn__bars i:nth-child(1) { width: 38%; animation: ui-bars1 2s cubic-bezier(0.65, 0.05, 0.35, 1) infinite; }
|
|
134
|
-
.ui-btn__bars i:nth-child(2) { width: 22%; animation: ui-bars2 2s cubic-bezier(0.65, 0.05, 0.35, 1) 0.8s infinite; }
|
|
146
|
+
.ui-btn__bars i:nth-child(1) { width: 38%; animation: ui-bars1 2s cubic-bezier(0.65, 0.05, 0.35, 1) infinite; } /* motion: ambient — an indeterminate loader loops until the work returns; its period is not a response time */
|
|
147
|
+
.ui-btn__bars i:nth-child(2) { width: 22%; animation: ui-bars2 2s cubic-bezier(0.65, 0.05, 0.35, 1) 0.8s infinite; } /* motion: ambient — the second bar runs the same loop 0.8s out of phase */
|
|
135
148
|
@keyframes ui-bars1 { 0% { left: -45%; } 100% { left: 110%; } }
|
|
136
149
|
@keyframes ui-bars2 { 0% { left: -28%; } 100% { left: 120%; } }
|
|
137
150
|
@keyframes ui-bars-in { from { opacity: 0; transform: scaleX(0.72); } to { opacity: 1; transform: scaleX(1); } }
|
package/src/styles/callout.css
CHANGED
|
@@ -13,18 +13,18 @@
|
|
|
13
13
|
color: var(--dim);
|
|
14
14
|
}
|
|
15
15
|
.ui-callout__icon { width: 18px; height: 18px; flex: none; margin-top: 1px; color: var(--muted); }
|
|
16
|
-
.ui-callout__icon svg { width: 100%; height: 100%; stroke: currentColor; fill: none; stroke-width: 1
|
|
16
|
+
.ui-callout__icon svg { width: 100%; height: 100%; stroke: currentColor; fill: none; stroke-width: 2.1; } /* 2.1 of a 24 box drawn at 18px = 1.58 CSS px, which is what buys the 3:1 graphic bar rather than the 4.5:1 text one. docs/specification.md#icons-and-glyphs */
|
|
17
17
|
.ui-callout b { color: var(--text); font-weight: var(--weight-semibold); }
|
|
18
18
|
.ui-callout code { font-family: var(--font-mono); font-size: 0.92em; color: var(--text); }
|
|
19
19
|
|
|
20
20
|
.ui-callout--info { background: var(--glow-cyan); }
|
|
21
|
-
.ui-callout--info .ui-callout__icon { color: var(--
|
|
21
|
+
.ui-callout--info .ui-callout__icon { color: var(--chip-info-ink); } /* the chip inks, not the raw signals: in light the raw signal cleared the graphic bar by a tenth on its own wash and read as a smudge, and a stroke is read the way text is. The two are the same value in dark by construction, so only light moves. #206 */
|
|
22
22
|
.ui-callout--success { background: var(--glow-green); }
|
|
23
|
-
.ui-callout--success .ui-callout__icon { color: var(--
|
|
23
|
+
.ui-callout--success .ui-callout__icon { color: var(--chip-success-ink); }
|
|
24
24
|
.ui-callout--warn { background: color-mix(in srgb, var(--amber) 13%, transparent); }
|
|
25
|
-
.ui-callout--warn .ui-callout__icon { color: var(--
|
|
25
|
+
.ui-callout--warn .ui-callout__icon { color: var(--chip-warn-ink); }
|
|
26
26
|
.ui-callout--danger { background: var(--glow-pink); }
|
|
27
|
-
.ui-callout--danger .ui-callout__icon { color: var(--
|
|
27
|
+
.ui-callout--danger .ui-callout__icon { color: var(--chip-danger-ink); }
|
|
28
28
|
|
|
29
29
|
/* ----------------------------------------------------------------------------
|
|
30
30
|
* Toast — a floating, dismissible notification.
|
|
@@ -42,30 +42,22 @@
|
|
|
42
42
|
color: var(--text);
|
|
43
43
|
font-size: var(--text-base);
|
|
44
44
|
width: 100%;
|
|
45
|
-
max-width:
|
|
45
|
+
max-width: var(--panel-md);
|
|
46
46
|
overflow: hidden; /* clips the timer bar + left marker to the rounded corners */
|
|
47
|
-
animation: ui-toast-in
|
|
47
|
+
animation: ui-toast-in var(--dur-med) var(--ease-out) both;
|
|
48
48
|
}
|
|
49
49
|
|
|
50
50
|
/* status → the paint tokens every style below consumes.
|
|
51
51
|
--toast-accent is the status as a line or a small mark, --toast-glow its wash,
|
|
52
|
-
--toast-on the glyph ink on the accent circle. --toast-solid
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
--toast-action-ink is separate from --toast-accent for the same reason the
|
|
57
|
-
fill is: the accent is a graphic colour, sized for a 3px rule and a 22px
|
|
58
|
-
circle, and in the light theme it is not a colour text can be set in. The
|
|
59
|
-
trailing action is the one part of a toast that is BOTH the status colour and
|
|
60
|
-
a piece of text, so it takes the chip inks, which are the text-grade version
|
|
61
|
-
of the same five statuses. In the dark theme the two are the same value by
|
|
62
|
-
construction — --chip-success-ink IS --green there — so this splits the light
|
|
63
|
-
theme apart and leaves the dark one exactly as it was. */
|
|
52
|
+
--toast-on the glyph ink on the accent circle. --toast-solid and
|
|
53
|
+
--toast-action-ink are separate from the accent on purpose: a fill and a piece
|
|
54
|
+
of text are different jobs from a 22px circle.
|
|
55
|
+
why: docs/specification.md#colour-and-contrast */
|
|
64
56
|
.ui-toast--success { --toast-accent: var(--green); --toast-action-ink: var(--chip-success-ink); --toast-glow: var(--glow-green); --toast-on: var(--signal-contrast); --toast-solid: var(--signal-solid-success); }
|
|
65
57
|
.ui-toast--danger { --toast-accent: var(--pink); --toast-action-ink: var(--chip-danger-ink); --toast-glow: var(--glow-pink); --toast-on: var(--danger-contrast); --toast-solid: var(--signal-solid-danger); }
|
|
66
58
|
.ui-toast--warn { --toast-accent: var(--amber); --toast-action-ink: var(--chip-warn-ink); --toast-glow: color-mix(in srgb, var(--amber) 14%, transparent); --toast-on: var(--signal-contrast); --toast-solid: var(--signal-solid-warn); }
|
|
67
59
|
.ui-toast--info { --toast-accent: var(--cyan); --toast-action-ink: var(--chip-info-ink); --toast-glow: var(--glow-cyan); --toast-on: var(--signal-contrast); --toast-solid: var(--signal-solid-info); }
|
|
68
|
-
.ui-toast--neutral { --toast-accent: var(--muted); --toast-action-ink: var(--muted); --toast-glow: var(--surface-2); --toast-on: var(--
|
|
60
|
+
.ui-toast--neutral { --toast-accent: var(--muted); --toast-action-ink: var(--muted); --toast-glow: var(--surface-2); --toast-on: var(--signal-solid-ink); --toast-solid: var(--signal-solid-neutral); }
|
|
69
61
|
|
|
70
62
|
/* left status marker (soft + outline; solid is already a full fill) */
|
|
71
63
|
.ui-toast--soft::before,
|
|
@@ -75,7 +67,7 @@
|
|
|
75
67
|
}
|
|
76
68
|
|
|
77
69
|
.ui-toast__icon { width: 22px; height: 22px; flex: none; margin-top: 1px; display: grid; place-items: center; border-radius: 50%; background: var(--toast-accent); color: var(--toast-on); }
|
|
78
|
-
.ui-toast__icon svg { width: 13px; height: 13px; stroke: currentColor; fill: none; stroke-width: 2; }
|
|
70
|
+
.ui-toast__icon svg { width: 13px; height: 13px; stroke: currentColor; fill: none; stroke-width: 2.8; } /* 2.8 of a 24 box drawn at 13px = 1.52 CSS px. The circle has no room for a bigger glyph, so the weight comes out of the stroke. docs/specification.md#icons-and-glyphs */
|
|
79
71
|
.ui-toast__body { flex: 1; min-width: 0; }
|
|
80
72
|
.ui-toast__title { font-weight: var(--weight-medium); color: var(--strong); }
|
|
81
73
|
.ui-toast__text { color: var(--dim); }
|
|
@@ -91,13 +83,32 @@
|
|
|
91
83
|
.ui-toast__action { flex: none; align-self: center; background: none; border: 0; padding: 4px 9px; border-radius: var(--radius-xs); font: inherit; font-weight: var(--weight-medium); color: var(--toast-action-ink); cursor: pointer; }
|
|
92
84
|
.ui-toast__action:hover { background: color-mix(in srgb, var(--bg) 55%, transparent); }
|
|
93
85
|
|
|
94
|
-
.ui-toast__close { flex: none; align-self: flex-start; background: none; border: 0; color: var(--muted); cursor: pointer; padding: 2px; display: grid; place-items: center; }
|
|
95
|
-
.ui-toast__close svg { width: 15px; height: 15px; stroke: currentColor; fill: none; stroke-width:
|
|
86
|
+
.ui-toast__close { flex: none; align-self: flex-start; background: none; border: 0; color: var(--muted); cursor: pointer; padding: 2px; display: grid; place-items: center; position: relative; }
|
|
87
|
+
.ui-toast__close svg { width: 15px; height: 15px; stroke: currentColor; fill: none; stroke-width: 2.5; }
|
|
88
|
+
/* The target, not the ink. WCAG 2.5.8 measures what a pointer can land on, and
|
|
89
|
+
the drawn button is 19x19 — a 15px glyph in 2px of padding. Growing the box
|
|
90
|
+
to 24 would push the glyph 2.5px off the corner it is aligned to, so the
|
|
91
|
+
TARGET grows instead and the glyph does not move at all.
|
|
92
|
+
Centred, so the overhang is 2.5px on every side. It has 12px of flex gap to
|
|
93
|
+
.ui-toast__action beside it and 12px between stacked toasts, so it reaches no
|
|
94
|
+
neighbouring target; .ui-toast's overflow: hidden clips at 15px of padding,
|
|
95
|
+
well outside it. Measured, not asserted:
|
|
96
|
+
stories/guidelines/accessibility-floor.test.js reads this rule out of the
|
|
97
|
+
sheet and reports the target it makes. #219 */
|
|
98
|
+
.ui-toast__close::before {
|
|
99
|
+
content: '';
|
|
100
|
+
position: absolute;
|
|
101
|
+
left: 50%;
|
|
102
|
+
top: 50%;
|
|
103
|
+
width: 24px;
|
|
104
|
+
height: 24px;
|
|
105
|
+
transform: translate(-50%, -50%);
|
|
106
|
+
}
|
|
96
107
|
.ui-toast__close:hover { color: var(--text); }
|
|
97
108
|
|
|
98
109
|
/* auto-dismiss progress bar — .is-running is added by wireToastStack() */
|
|
99
110
|
.ui-toast__timer { position: absolute; left: 0; bottom: 0; height: 3px; width: 100%; background: var(--toast-accent); opacity: 0.85; transform-origin: left; }
|
|
100
|
-
.ui-toast__timer.is-running { animation: ui-toast-timer var(--toast-dur, 5s) linear forwards; }
|
|
111
|
+
.ui-toast__timer.is-running { animation: ui-toast-timer var(--toast-dur, 5s) linear forwards; /* motion: ambient — the bar spends the toast's dismiss timer, which the caller sets */ }
|
|
101
112
|
|
|
102
113
|
/* style: soft — tinted surface */
|
|
103
114
|
.ui-toast--soft { background: var(--toast-glow); box-shadow: var(--shadow-sm); }
|
|
@@ -142,7 +153,7 @@
|
|
|
142
153
|
@keyframes ui-toast-in { from { opacity: 0; transform: translateX(18px); } to { opacity: 1; transform: none; } }
|
|
143
154
|
@keyframes ui-toast-out { to { opacity: 0; transform: translateX(24px); } }
|
|
144
155
|
@keyframes ui-toast-timer { from { transform: scaleX(1); } to { transform: scaleX(0); } }
|
|
145
|
-
.ui-toast.is-leaving { animation: ui-toast-out
|
|
156
|
+
.ui-toast.is-leaving { animation: ui-toast-out var(--dur-fast) var(--ease-sharp) forwards; }
|
|
146
157
|
|
|
147
158
|
@media (prefers-reduced-motion: reduce) {
|
|
148
159
|
.ui-toast, .ui-toast.is-leaving { animation: none; }
|
|
@@ -150,7 +161,7 @@
|
|
|
150
161
|
}
|
|
151
162
|
|
|
152
163
|
/* stack container for queued toasts */
|
|
153
|
-
.ui-toast-stack { display: flex; flex-direction: column; gap: 12px; width:
|
|
164
|
+
.ui-toast-stack { display: flex; flex-direction: column; gap: 12px; width: var(--panel-md); max-width: 100%; }
|
|
154
165
|
|
|
155
166
|
/* Success panel — centered confirmation, used after form submit */
|
|
156
167
|
.ui-success {
|
package/src/styles/card.css
CHANGED
package/src/styles/code.css
CHANGED
|
@@ -42,10 +42,16 @@
|
|
|
42
42
|
align-items: center;
|
|
43
43
|
gap: 6px;
|
|
44
44
|
padding: 2px 4px;
|
|
45
|
+
/* WCAG 2.5.8's 24 CSS px target floor, taken as a real box rather than as an
|
|
46
|
+
overlay: 2 + a 12px label at line-height 1.62 + 2 came to 23.44, so the
|
|
47
|
+
drawn button grows by 0.56px and the bar it sits in grows with it. Nothing
|
|
48
|
+
smaller than a rounding error moves, which is why this one is not a hit
|
|
49
|
+
area. Measured in stories/guidelines/accessibility-floor.test.js. #219 */
|
|
50
|
+
min-height: 24px;
|
|
45
51
|
transition: color var(--dur-fast) var(--ease);
|
|
46
52
|
}
|
|
47
53
|
.ui-snippet__copy:hover { color: var(--accent); }
|
|
48
|
-
.ui-snippet__copy svg { width: 13px; height: 13px; stroke: currentColor; fill: none; stroke-width:
|
|
54
|
+
.ui-snippet__copy svg { width: 13px; height: 13px; stroke: currentColor; fill: none; stroke-width: 2.8; }
|
|
49
55
|
|
|
50
56
|
/* Shell token highlighting (matches viz/account.mjs classes) */
|
|
51
57
|
.ui-snippet pre .k { color: var(--purple-mid); font-weight: var(--weight-medium); }
|