@apliteni/apliteni-ui 0.12.0 → 0.23.1

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@apliteni/apliteni-ui",
3
- "version": "0.12.0",
3
+ "version": "0.23.1",
4
4
  "workspaces": [
5
5
  "react"
6
6
  ],
@@ -1,3 +1,22 @@
1
+ /* ../src/styles/reduced-motion.css */
2
+ @media (prefers-reduced-motion: reduce) {
3
+ html {
4
+ scroll-behavior: auto !important;
5
+ }
6
+ *,
7
+ ::before,
8
+ ::after {
9
+ animation-duration: 0.01ms !important;
10
+ animation-iteration-count: 1 !important;
11
+ transition-duration: 0.01ms !important;
12
+ scroll-behavior: auto !important;
13
+ }
14
+ [data-reveal] {
15
+ opacity: 1 !important;
16
+ transform: none !important;
17
+ }
18
+ }
19
+
1
20
  /* src/Modal.css */
2
21
  .rx-scrim {
3
22
  position: fixed;
@@ -12,9 +12,11 @@ type ButtonProps = {
12
12
  iconRight?: string;
13
13
  iconOnly?: boolean;
14
14
  block?: boolean;
15
+ /** In flight: aria-busy, disabled, and the kit's indeterminate bars. */
16
+ busy?: boolean;
15
17
  children?: ReactNode;
16
18
  } & ButtonHTMLAttributes<HTMLButtonElement>;
17
- declare function Button({ variant, size, icon, iconRight, iconOnly, block, children, type, ...rest }: ButtonProps): react.JSX.Element;
19
+ declare function Button({ variant, size, icon, iconRight, iconOnly, block, busy, children, type, disabled, ...rest }: ButtonProps): react.JSX.Element;
18
20
 
19
21
  declare function Badge({ variant, children }: {
20
22
  variant?: string;
@@ -55,4 +57,43 @@ declare function DataTable<T extends {
55
57
  name: string;
56
58
  }>({ columns, rows, pageSize, selected, onToggle, onTogglePage, }: DataTableProps<T>): react.JSX.Element;
57
59
 
58
- export { Badge, Button, type ButtonProps, Card, type Column, DataTable, type DataTableProps, Icon, Modal, type ModalProps };
60
+ type SkeletonProps = {
61
+ /** A count of bars, or explicit widths when a ragged prose edge matters. */
62
+ lines?: number | string[];
63
+ width?: string;
64
+ height?: string;
65
+ radius?: string;
66
+ className?: string;
67
+ };
68
+ declare function Skeleton({ lines, width, height, radius, className }: SkeletonProps): react.JSX.Element;
69
+ type SkeletonTableProps = {
70
+ rows?: number;
71
+ cols?: number;
72
+ head?: boolean;
73
+ };
74
+ declare function SkeletonTable({ rows, cols, head }: SkeletonTableProps): react.JSX.Element;
75
+ type BusyRegionProps = {
76
+ busy: boolean;
77
+ /** Spoken while it works. */
78
+ label?: string;
79
+ /** Spoken when it finishes — the specific line ("14 invoices") beats "Loaded". */
80
+ message?: string;
81
+ /** Placeholder while busy. Defaults to a three-bar skeleton. */
82
+ placeholder?: ReactNode;
83
+ className?: string;
84
+ children?: ReactNode;
85
+ };
86
+ declare function BusyRegion({ busy, label, message, placeholder, className, children, }: BusyRegionProps): react.JSX.Element;
87
+ type DeniedProps = {
88
+ title?: string;
89
+ sub?: string;
90
+ /** The scope or role the reader is missing, verbatim. */
91
+ need?: string;
92
+ icon?: string;
93
+ className?: string;
94
+ /** Buttons. Nothing is assumed about what a reader can do next. */
95
+ children?: ReactNode;
96
+ };
97
+ declare function Denied({ title, sub, need, icon, className, children, }: DeniedProps): react.JSX.Element;
98
+
99
+ export { Badge, BusyRegion, type BusyRegionProps, Button, type ButtonProps, Card, type Column, DataTable, type DataTableProps, Denied, type DeniedProps, Icon, Modal, type ModalProps, Skeleton, type SkeletonProps, SkeletonTable, type SkeletonTableProps };
@@ -24,8 +24,10 @@ function Button({
24
24
  iconRight,
25
25
  iconOnly,
26
26
  block,
27
+ busy,
27
28
  children,
28
29
  type = "button",
30
+ disabled,
29
31
  ...rest
30
32
  }) {
31
33
  const cls = cx(
@@ -38,11 +40,27 @@ function Button({
38
40
  const labelled = rest["aria-label"] != null || rest["aria-labelledby"] != null;
39
41
  const fallback = typeof children === "string" && children.trim() ? children.trim() : icon2;
40
42
  const named = iconOnly && !labelled && fallback ? { "aria-label": fallback, title: rest.title ?? fallback } : {};
41
- return /* @__PURE__ */ jsxs("button", { type, className: cls, ...rest, ...named, children: [
42
- icon2 && /* @__PURE__ */ jsx2(Icon, { name: icon2 }),
43
- !iconOnly && children != null && /* @__PURE__ */ jsx2("span", { children }),
44
- iconRight && /* @__PURE__ */ jsx2(Icon, { name: iconRight })
45
- ] });
43
+ return /* @__PURE__ */ jsxs(
44
+ "button",
45
+ {
46
+ type,
47
+ className: cls,
48
+ disabled: disabled || busy,
49
+ "aria-disabled": disabled || busy ? true : void 0,
50
+ "aria-busy": busy ? true : void 0,
51
+ ...rest,
52
+ ...named,
53
+ children: [
54
+ icon2 && /* @__PURE__ */ jsx2(Icon, { name: icon2 }),
55
+ !iconOnly && children != null && /* @__PURE__ */ jsx2("span", { children }),
56
+ iconRight && /* @__PURE__ */ jsx2(Icon, { name: iconRight }),
57
+ busy && /* @__PURE__ */ jsxs("span", { className: "ui-btn__bars", children: [
58
+ /* @__PURE__ */ jsx2("i", {}),
59
+ /* @__PURE__ */ jsx2("i", {})
60
+ ] })
61
+ ]
62
+ }
63
+ );
46
64
  }
47
65
 
48
66
  // src/primitives/Badge.tsx
@@ -244,11 +262,85 @@ function DataTable({
244
262
  ] })
245
263
  ] });
246
264
  }
265
+
266
+ // src/Loading.tsx
267
+ import { jsx as jsx7, jsxs as jsxs5 } from "react/jsx-runtime";
268
+ var cx2 = (...a) => a.filter(Boolean).join(" ");
269
+ function Skeleton({ lines = 3, width, height, radius, className }) {
270
+ const widths = Array.isArray(lines) ? lines : null;
271
+ const n = widths ? widths.length : Math.max(1, lines);
272
+ const styleFor = (i) => {
273
+ const w = widths ? widths[i] : width;
274
+ if (!w && !height && !radius) return void 0;
275
+ return { ...w && { width: w }, ...height && { height }, ...radius && { borderRadius: radius } };
276
+ };
277
+ return /* @__PURE__ */ jsx7("div", { className: cx2("ui-skel", className), "aria-hidden": "true", children: Array.from({ length: n }, (_, i) => /* @__PURE__ */ jsx7("span", { className: "ui-skel__bar m-skeleton", style: styleFor(i) }, i)) });
278
+ }
279
+ function SkeletonTable({ rows = 5, cols = 4, head = true }) {
280
+ const cells = Array.from({ length: Math.max(1, cols) }, (_, i) => /* @__PURE__ */ jsx7("span", { className: "ui-skel__bar m-skeleton" }, i));
281
+ return /* @__PURE__ */ jsxs5(
282
+ "div",
283
+ {
284
+ className: "ui-skel ui-skel--table",
285
+ style: { "--skel-cols": Math.max(1, cols) },
286
+ "aria-hidden": "true",
287
+ children: [
288
+ head && /* @__PURE__ */ jsx7("div", { className: "ui-skel__row ui-skel__row--head", children: cells }),
289
+ Array.from({ length: Math.max(1, rows) }, (_, i) => /* @__PURE__ */ jsx7("div", { className: "ui-skel__row", children: cells }, i))
290
+ ]
291
+ }
292
+ );
293
+ }
294
+ function BusyRegion({
295
+ busy,
296
+ label = "Loading\u2026",
297
+ message = "Loaded",
298
+ placeholder,
299
+ className,
300
+ children
301
+ }) {
302
+ return /* @__PURE__ */ jsxs5(
303
+ "div",
304
+ {
305
+ className: cx2("ui-busy", className),
306
+ role: "status",
307
+ "aria-live": "polite",
308
+ "aria-busy": busy,
309
+ children: [
310
+ /* @__PURE__ */ jsx7("span", { className: "ui-sr", children: busy ? label : message }),
311
+ /* @__PURE__ */ jsx7("div", { className: "ui-busy__body", children: busy ? placeholder ?? /* @__PURE__ */ jsx7(Skeleton, { lines: 3 }) : children })
312
+ ]
313
+ }
314
+ );
315
+ }
316
+ function Denied({
317
+ title = "You don\u2019t have access",
318
+ sub,
319
+ need,
320
+ icon: icon2 = "lock",
321
+ className,
322
+ children
323
+ }) {
324
+ return /* @__PURE__ */ jsxs5("div", { className: cx2("ui-denied", className), children: [
325
+ /* @__PURE__ */ jsx7("div", { className: "ui-denied__seal", children: /* @__PURE__ */ jsx7(Icon, { name: icon2 }) }),
326
+ /* @__PURE__ */ jsx7("div", { className: "ui-denied__title", children: title }),
327
+ sub && /* @__PURE__ */ jsx7("div", { className: "ui-denied__sub", children: sub }),
328
+ need && /* @__PURE__ */ jsxs5("div", { className: "ui-denied__need", children: [
329
+ "Needs ",
330
+ /* @__PURE__ */ jsx7("code", { className: "ui-code", children: need })
331
+ ] }),
332
+ children && /* @__PURE__ */ jsx7("div", { className: "ui-denied__actions", children })
333
+ ] });
334
+ }
247
335
  export {
248
336
  Badge,
337
+ BusyRegion,
249
338
  Button,
250
339
  Card,
251
340
  DataTable,
341
+ Denied,
252
342
  Icon,
253
- Modal
343
+ Modal,
344
+ Skeleton,
345
+ SkeletonTable
254
346
  };
@@ -1,34 +1,13 @@
1
- // Line-icon set — 24×24, stroke=currentColor, 1.7 weight, round caps/joins.
2
- // House style is Feather/Lucide (Lucide is the maintained Feather; our glyphs
3
- // match it 1:1). Delivery is inline SVG strings: no runtime dependency, works
4
- // in any framework, and every glyph inherits `currentColor` + a consistent
5
- // stroke so it sits right next to our type.
1
+ // Line-icon set — 24×24, stroke=currentColor, 1.7 weight, round caps/joins,
2
+ // Lucide house style. Delivery is inline SVG strings, so there is no runtime
3
+ // dependency and every glyph inherits `currentColor`.
6
4
  //
7
5
  // Each value is the INNER markup; icon() wraps it in the shared <svg>. Glyphs
8
6
  // are grouped by domain — the flat ICONS map is what icon() looks up, and
9
7
  // iconCategories drives the Storybook grid.
10
8
  //
11
- // ---- Adding a glyph -------------------------------------------------------
12
- //
13
- // NAME it for what it depicts, not for the one place it is used: `trash`, not
14
- // `deleteWorkspace`. camelCase, and a modifier follows its noun — `circleX`,
15
- // `eyeOff`, `trendingUp` — so the family sorts together. A name is taken once;
16
- // a second declaration of one is a gate failure, not a merge conflict.
17
- //
18
- // GROUP it by what it depicts, again rather than by caller. `chart` lives in
19
- // DATA because it draws data, even when a comms panel is what renders it. If
20
- // two groups both look right, the glyph belongs to the one whose other members
21
- // it would sit beside in the catalogue. Groups are not tags: exactly one.
22
- //
23
- // PROVENANCE: the path comes from Lucide, unmodified, at the 24×24 / 1.7 house
24
- // stroke — that is what keeps the set looking like one hand. Say which Lucide
25
- // name it came from in the commit if the two differ. A hand-drawn path needs a
26
- // reason in the commit message, because the next person cannot tell one from a
27
- // traced one by looking.
28
- //
29
- // The gates: src/assets/icons.test.js holds one-group-per-glyph and the
30
- // emitter's numbers, and stories/guidelines/iconography.test.js holds the
31
- // icon-only list below. The written rules are on Guidelines / Iconography.
9
+ // Naming, grouping and provenance, and the gates that hold them:
10
+ // why: CONTRIBUTING.md#add-a-glyph
32
11
 
33
12
  const NAV = {
34
13
  chevronDown: '<path d="M6 9l6 6 6-6" stroke-linecap="round" stroke-linejoin="round"/>',
@@ -3,7 +3,7 @@
3
3
  // shell.js and topbar.js need it and shell.js already imports topbar.js — the
4
4
  // other direction would be a cycle. shell.js re-exports ACCOUNT_NAV, which is
5
5
  // the published name docs/library.md documents.
6
- // why: docs/adr/0007-one-page-shell-built-from-the-kits-own-nav.md
6
+ // why: docs/specification.md#the-page-shell
7
7
  import { esc } from './index.js';
8
8
 
9
9
  // nav.js item objects. Labels are raw text — every nav primitive escapes, so a
@@ -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
+ }
@@ -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/adr/0007-one-page-shell-built-from-the-kits-own-nav.md
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, which is a declaration list: esc()
93
- // stops a quote closing the attribute, and `;` is the character that matters
94
- // there. So a length is all this accepts a number and a unit the reading
95
- // column can use, or `none`. Anything else falls back to the default rather
96
- // than throwing, because a shell that throws mid-render takes the page with it.
97
- const MAIN_MAX = '860px';
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 : MAIN_MAX;
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>` : ''}
package/src/index.css CHANGED
@@ -1,12 +1,16 @@
1
1
  /* ============================================================================
2
2
  * apliteni-ui — full stylesheet.
3
- * Import once (`import 'apliteni-ui/css'`) or cherry-pick from src/styles/*.
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');
@@ -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); }
@@ -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: 1180px; margin: 0 auto; padding: 0 clamp(14px, 2.4vw, 26px); }
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 */
@@ -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
- opacity: 0.45;
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 180ms var(--ease) both;
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); } }