@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 +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/loading.js +126 -0
- package/src/components/shell.js +10 -9
- 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 +1 -1
- package/src/styles/button.css +18 -5
- package/src/styles/callout.css +37 -18
- package/src/styles/card.css +1 -1
- package/src/styles/code.css +7 -1
- package/src/styles/confirm.css +1 -1
- 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 +144 -0
- package/src/styles/motion.css +14 -24
- package/src/styles/nav.css +14 -12
- package/src/styles/reduced-motion.css +32 -0
- package/src/styles/success.css +8 -8
- package/src/styles/table.css +21 -12
- package/src/styles/topbar.css +27 -12
- package/src/tokens/accents.css +7 -7
- package/src/tokens/tokens.css +91 -10
package/package.json
CHANGED
package/react/dist/index.css
CHANGED
|
@@ -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;
|
package/react/dist/index.d.ts
CHANGED
|
@@ -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
|
-
|
|
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 };
|
package/react/dist/index.js
CHANGED
|
@@ -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(
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
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
|
};
|
package/src/assets/icons.js
CHANGED
|
@@ -1,34 +1,13 @@
|
|
|
1
|
-
// Line-icon set — 24×24, stroke=currentColor, 1.7 weight, round caps/joins
|
|
2
|
-
//
|
|
3
|
-
//
|
|
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
|
-
//
|
|
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/
|
|
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
|
+
}
|
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>` : ''}
|
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 */
|
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); } }
|