@apliteni/apliteni-ui 0.11.4 → 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 +51 -9
- package/src/components/account-nav.js +1 -1
- package/src/components/index.js +16 -2
- 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,12 +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
|
-
// iconCategories drives the Storybook grid.
|
|
7
|
+
// iconCategories drives the Storybook grid.
|
|
8
|
+
//
|
|
9
|
+
// Naming, grouping and provenance, and the gates that hold them:
|
|
10
|
+
// why: CONTRIBUTING.md#add-a-glyph
|
|
10
11
|
|
|
11
12
|
const NAV = {
|
|
12
13
|
chevronDown: '<path d="M6 9l6 6 6-6" stroke-linecap="round" stroke-linejoin="round"/>',
|
|
@@ -101,9 +102,6 @@ const COMMS = {
|
|
|
101
102
|
logout: '<path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4"/><polyline points="16 17 21 12 16 7"/><line x1="21" y1="12" x2="9" y2="12"/>',
|
|
102
103
|
plug: '<path d="M12 22v-5"/><path d="M15 8V2"/><path d="M17 8a1 1 0 0 1 1 1v4a4 4 0 0 1-4 4h-4a4 4 0 0 1-4-4V9a1 1 0 0 1 1-1z"/><path d="M9 8V2"/>',
|
|
103
104
|
sparkle: '<path d="M11.017 2.814a1 1 0 0 1 1.966 0l1.051 5.558a2 2 0 0 0 1.594 1.594l5.558 1.051a1 1 0 0 1 0 1.966l-5.558 1.051a2 2 0 0 0-1.594 1.594l-1.051 5.558a1 1 0 0 1-1.966 0l-1.051-5.558a2 2 0 0 0-1.594-1.594l-5.558-1.051a1 1 0 0 1 0-1.966l5.558-1.051a2 2 0 0 0 1.594-1.594z"/><path d="M20 2v4"/><path d="M22 4h-4"/><circle cx="4" cy="20" r="2"/>',
|
|
104
|
-
card: '<rect x="1" y="4" width="22" height="16" rx="2" ry="2"/><line x1="1" y1="10" x2="23" y2="10"/>',
|
|
105
|
-
doc: '<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/><line x1="16" y1="13" x2="8" y2="13"/><line x1="16" y1="17" x2="8" y2="17"/><polyline points="10 9 9 9 8 9"/>',
|
|
106
|
-
chart: '<line x1="18" y1="20" x2="18" y2="10"/><line x1="12" y1="20" x2="12" y2="4"/><line x1="6" y1="20" x2="6" y2="14"/>',
|
|
107
105
|
};
|
|
108
106
|
|
|
109
107
|
const BRAND = {
|
|
@@ -140,5 +138,49 @@ export const icon = (name, cls = '') =>
|
|
|
140
138
|
|
|
141
139
|
export const iconNames = Object.keys(ICONS);
|
|
142
140
|
|
|
141
|
+
// Which controls may be an icon and nothing else — a closed list, not a
|
|
142
|
+
// judgement call in review.
|
|
143
|
+
//
|
|
144
|
+
// The accessibility half was already held: every glyph is aria-hidden, so
|
|
145
|
+
// button({ iconOnly }) names itself from `label`. That says a nameless icon
|
|
146
|
+
// button cannot ship; it never said when a wordless one SHOULD. This does.
|
|
147
|
+
//
|
|
148
|
+
// A control whose action is on this list may drop its visible text. Everything
|
|
149
|
+
// else carries a label — including glyphs that feel obvious in isolation, like
|
|
150
|
+
// `gear` or `externalLink`, because a reader meets them one at a time and a
|
|
151
|
+
// toolbar is not a legend. Adding an entry is a decision recorded here, which
|
|
152
|
+
// is the point: the alternative rules read well and could not be gated.
|
|
153
|
+
export const iconOnlyAllowed = {
|
|
154
|
+
x: 'close or dismiss',
|
|
155
|
+
copy: 'copy to clipboard',
|
|
156
|
+
moreHorizontal: 'overflow menu',
|
|
157
|
+
moreVertical: 'overflow menu',
|
|
158
|
+
chevronDown: 'expand or collapse',
|
|
159
|
+
chevronUp: 'expand or collapse',
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
// What a glyph means when a component picks it for the reader rather than a
|
|
163
|
+
// caller naming it. Two rules hold the map together:
|
|
164
|
+
//
|
|
165
|
+
// a CIRCLED glyph is a state the system is in — circleCheck, circleX,
|
|
166
|
+
// circleAlert. It is reported to you and you cannot click it.
|
|
167
|
+
// a BARE glyph is an action you can take — x dismisses, check confirms,
|
|
168
|
+
// trash deletes.
|
|
169
|
+
//
|
|
170
|
+
// That split is what a danger toast needed: it used to render the same bare `x`
|
|
171
|
+
// twice, once meaning "this failed" and once meaning "make this go away". The
|
|
172
|
+
// bare x is now reserved for the close button, and status took the circle
|
|
173
|
+
// family the kit was already shipping and never using.
|
|
174
|
+
export const iconMeanings = {
|
|
175
|
+
circleCheck: 'a state: it succeeded',
|
|
176
|
+
circleX: 'a state: it failed',
|
|
177
|
+
circleAlert: 'a state: it needs attention',
|
|
178
|
+
info: 'a state: something worth knowing, no action required',
|
|
179
|
+
bolt: 'a state: it happened, with no verdict attached',
|
|
180
|
+
x: 'an action: close or dismiss this',
|
|
181
|
+
check: 'an action: confirm this',
|
|
182
|
+
trash: 'an action: delete this',
|
|
183
|
+
};
|
|
184
|
+
|
|
143
185
|
export const sun = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" aria-hidden="true" focusable="false"><circle cx="12" cy="12" r="4"/><path d="M12 2v2M12 20v2M4.9 4.9l1.4 1.4M17.7 17.7l1.4 1.4M2 12h2M20 12h2M4.9 19.1l1.4-1.4M17.7 6.3l1.4-1.4"/></svg>';
|
|
144
186
|
export const moon = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><path d="M21 12.8A9 9 0 1 1 11.2 3a7 7 0 0 0 9.8 9.8z"/></svg>';
|
|
@@ -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
|
package/src/components/index.js
CHANGED
|
@@ -16,6 +16,10 @@ export const esc = (s) => String(s == null ? '' : s).replace(/[&<>"]/g, (c) => (
|
|
|
16
16
|
// stops being decoration and becomes the control's ONLY accessible name. It is
|
|
17
17
|
// mirrored into aria-label + title, and an empty label falls back to the icon
|
|
18
18
|
// name rather than shipping a nameless button.
|
|
19
|
+
//
|
|
20
|
+
// That is the accessibility half, and it is not permission. WHEN a control may
|
|
21
|
+
// go wordless is a closed list — `iconOnlyAllowed` in src/assets/icons.js — and
|
|
22
|
+
// stories/guidelines/iconography.test.js reviews call sites against it.
|
|
19
23
|
export function button({
|
|
20
24
|
label = 'Button', variant = 'secondary', size = 'md', icon: ic, iconSvg, iconRight,
|
|
21
25
|
block = false, disabled = false, busy = false, type = 'button', href, iconOnly = false,
|
|
@@ -217,8 +221,18 @@ export function switchToggle({ checked = false, disabled = false, name, label =
|
|
|
217
221
|
export function callout({ variant, icon: ic = 'info', body } = {}) {
|
|
218
222
|
return `<div class="${cx('ui-callout', variant && `ui-callout--${variant}`)}"><span class="ui-callout__icon">${icon(ic)}</span><div>${body}</div></div>`;
|
|
219
223
|
}
|
|
220
|
-
// Default icon per status — overridable with `icon`.
|
|
221
|
-
|
|
224
|
+
// Default icon per status — overridable with `icon`. The circle family is not
|
|
225
|
+
// decoration: a circled glyph is a STATE the system reports, a bare one is an
|
|
226
|
+
// ACTION you can take (iconMeanings in src/assets/icons.js).
|
|
227
|
+
//
|
|
228
|
+
// This used to read `success: 'check', danger: 'x', warn: 'alert'`, which made
|
|
229
|
+
// a danger toast render the same bare `x` twice — once as the status, meaning
|
|
230
|
+
// "this failed", once as the close button, meaning "make this go away". Status
|
|
231
|
+
// took the circle glyphs the kit already shipped and never used, and the bare
|
|
232
|
+
// `x` now belongs to the close button alone.
|
|
233
|
+
const TOAST_ICON = {
|
|
234
|
+
success: 'circleCheck', danger: 'circleX', warn: 'circleAlert', info: 'info', neutral: 'bolt',
|
|
235
|
+
};
|
|
222
236
|
// A toast carries a status (colour) and a style (surface). Everything visual is
|
|
223
237
|
// token-driven: the status modifier sets --toast-accent/-glow/-on, the style
|
|
224
238
|
// modifier consumes them. `action` adds a trailing button ("Undo"/"Retry"),
|
|
@@ -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 */
|