@tensor_1/react 2.46.0 → 2.48.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +509 -1
- package/dist/{SchedulingCalendar-B6TakO2h.d.ts → SchedulingCalendar-WWyFL3Eo.d.ts} +51 -2
- package/dist/announce.d.ts +10 -0
- package/dist/announce.js +30 -0
- package/dist/chart-analytic.cjs +33 -25
- package/dist/chart-preset.cjs +23 -15
- package/dist/chart.cjs +23 -15
- package/dist/combobox-model.d.ts +10 -1
- package/dist/combobox-model.js +15 -0
- package/dist/command-palette-model.d.ts +44 -0
- package/dist/command-palette-model.js +36 -0
- package/dist/components/AccountMenu.d.ts +44 -0
- package/dist/components/AccountMenu.js +43 -0
- package/dist/components/AppShell.d.ts +45 -2
- package/dist/components/AppShell.js +116 -5
- package/dist/components/AuditLog.d.ts +128 -0
- package/dist/components/AuditLog.js +196 -0
- package/dist/components/BrandProfiles.js +4 -3
- package/dist/components/Breadcrumbs.d.ts +9 -2
- package/dist/components/Breadcrumbs.js +3 -2
- package/dist/components/BusyBar.d.ts +13 -0
- package/dist/components/BusyBar.js +10 -0
- package/dist/components/BusyRegion.d.ts +18 -0
- package/dist/components/BusyRegion.js +12 -0
- package/dist/components/Button.d.ts +33 -5
- package/dist/components/Button.js +12 -6
- package/dist/components/ButtonFeedback.d.ts +30 -0
- package/dist/components/ButtonFeedback.js +78 -0
- package/dist/components/Card.d.ts +18 -1
- package/dist/components/Card.js +5 -1
- package/dist/components/ChangeHighlight.d.ts +30 -0
- package/dist/components/ChangeHighlight.js +21 -0
- package/dist/components/ChartCore.js +2 -1
- package/dist/components/Checkbox.d.ts +27 -1
- package/dist/components/Checkbox.js +31 -2
- package/dist/components/CodeDisplay.d.ts +65 -0
- package/dist/components/CodeDisplay.js +124 -0
- package/dist/components/Combobox.d.ts +27 -6
- package/dist/components/Combobox.js +195 -58
- package/dist/components/CommandPalette.d.ts +36 -1
- package/dist/components/CommandPalette.js +254 -2
- package/dist/components/ContactCard.d.ts +66 -0
- package/dist/components/ContactCard.js +74 -0
- package/dist/components/Count.d.ts +24 -0
- package/dist/components/Count.js +32 -0
- package/dist/components/DataWorkspace.d.ts +167 -0
- package/dist/components/DataWorkspace.js +383 -0
- package/dist/components/DateFieldParts.d.ts +2 -1
- package/dist/components/DateFieldParts.js +7 -0
- package/dist/components/DatePicker.d.ts +9 -2
- package/dist/components/DatePicker.js +67 -28
- package/dist/components/DateRangePicker.d.ts +12 -2
- package/dist/components/DateRangePicker.js +13 -12
- package/dist/components/DelayedRender.d.ts +15 -0
- package/dist/components/DelayedRender.js +10 -0
- package/dist/components/DetailPage.d.ts +7 -1
- package/dist/components/DetailPage.js +2 -1
- package/dist/components/Dialogs.d.ts +71 -10
- package/dist/components/Dialogs.js +130 -30
- package/dist/components/DiffViewer.d.ts +23 -3
- package/dist/components/DiffViewer.js +26 -12
- package/dist/components/DiscardPrompt.d.ts +1 -0
- package/dist/components/DiscardPrompt.js +16 -0
- package/dist/components/DisplayPreferences.d.ts +83 -0
- package/dist/components/DisplayPreferences.js +185 -0
- package/dist/components/Drawer.d.ts +1 -0
- package/dist/components/EditorialHome.d.ts +23 -3
- package/dist/components/EditorialHome.js +16 -11
- package/dist/components/EntityPicker.d.ts +20 -3
- package/dist/components/EntityPicker.js +32 -13
- package/dist/components/FilterEditor.d.ts +18 -4
- package/dist/components/FilterEditor.js +141 -19
- package/dist/components/InfoTip.js +11 -12
- package/dist/components/InlineEdit.d.ts +87 -20
- package/dist/components/InlineEdit.js +197 -54
- package/dist/components/Input.d.ts +12 -2
- package/dist/components/Input.js +5 -3
- package/dist/components/Kbd.d.ts +6 -1
- package/dist/components/Kbd.js +6 -6
- package/dist/components/Lightbox.d.ts +41 -5
- package/dist/components/Lightbox.js +95 -9
- package/dist/components/MatchReview.d.ts +40 -1
- package/dist/components/MatchReview.js +130 -4
- package/dist/components/MetricInput.js +3 -2
- package/dist/components/Modal.d.ts +3 -1
- package/dist/components/Modal.js +13 -4
- package/dist/components/NavFavourites.d.ts +5 -0
- package/dist/components/NavFavourites.js +7 -7
- package/dist/components/OtpInput.d.ts +33 -0
- package/dist/components/OtpInput.js +123 -0
- package/dist/components/PageHeader.d.ts +7 -1
- package/dist/components/PageHeader.js +2 -2
- package/dist/components/Pagination.js +6 -5
- package/dist/components/PasswordInput.d.ts +34 -0
- package/dist/components/PasswordInput.js +101 -0
- package/dist/components/Peek.js +3 -3
- package/dist/components/Popover.js +2 -0
- package/dist/components/PrintLayout.d.ts +63 -0
- package/dist/components/PrintLayout.js +114 -0
- package/dist/components/QrCode.d.ts +47 -0
- package/dist/components/QrCode.js +55 -0
- package/dist/components/QrScanner.d.ts +95 -0
- package/dist/components/QrScanner.js +266 -0
- package/dist/components/RelationshipGraph.d.ts +32 -1
- package/dist/components/RelationshipGraph.js +18 -5
- package/dist/components/RelationshipGraphLayered.d.ts +1 -1
- package/dist/components/RelationshipGraphLayered.js +190 -115
- package/dist/components/RelationshipGraphList.js +33 -6
- package/dist/components/RuleBuilder.js +39 -7
- package/dist/components/SaveStatus.d.ts +7 -1
- package/dist/components/SaveStatus.js +13 -4
- package/dist/components/SchedulingAgenda.d.ts +3 -1
- package/dist/components/SchedulingCalendar.d.ts +3 -1
- package/dist/components/SchedulingCalendar.js +43 -4
- package/dist/components/SchedulingResourceDay.d.ts +57 -0
- package/dist/components/SchedulingResourceDay.js +287 -0
- package/dist/components/SchedulingTimeGrid.d.ts +3 -1
- package/dist/components/SchedulingTimeline.d.ts +3 -1
- package/dist/components/SearchBar.d.ts +1 -0
- package/dist/components/SearchSuggest.d.ts +23 -3
- package/dist/components/SearchSuggest.js +48 -17
- package/dist/components/Segmented.d.ts +39 -4
- package/dist/components/Segmented.js +31 -3
- package/dist/components/Select.d.ts +3 -2
- package/dist/components/Select.js +1 -1
- package/dist/components/ShortcutHint.d.ts +11 -10
- package/dist/components/ShortcutHint.js +44 -38
- package/dist/components/Sidebar.d.ts +36 -4
- package/dist/components/Sidebar.js +37 -12
- package/dist/components/SortableList.d.ts +45 -0
- package/dist/components/SortableList.js +134 -0
- package/dist/components/Spinner.d.ts +7 -1
- package/dist/components/Spinner.js +7 -1
- package/dist/components/Stat.d.ts +28 -3
- package/dist/components/Stat.js +13 -3
- package/dist/components/States.d.ts +7 -1
- package/dist/components/States.js +7 -1
- package/dist/components/StatusPill.d.ts +14 -3
- package/dist/components/StatusPill.js +11 -3
- package/dist/components/Steps.js +2 -1
- package/dist/components/Table.d.ts +16 -2
- package/dist/components/Table.js +8 -4
- package/dist/components/TableControls.js +18 -58
- package/dist/components/TableOverflowProbe.d.ts +12 -0
- package/dist/components/TableOverflowProbe.js +27 -0
- package/dist/components/Tabs.d.ts +2 -1
- package/dist/components/Tabs.js +1 -0
- package/dist/components/TagInput.d.ts +67 -0
- package/dist/components/TagInput.js +342 -0
- package/dist/components/Textarea.d.ts +5 -0
- package/dist/components/Textarea.js +2 -2
- package/dist/components/TimePicker.d.ts +80 -0
- package/dist/components/TimePicker.js +347 -0
- package/dist/components/Toast.js +3 -2
- package/dist/components/Tooltip.d.ts +8 -4
- package/dist/components/Tooltip.js +24 -21
- package/dist/components/Tree.d.ts +8 -0
- package/dist/components/Tree.js +11 -5
- package/dist/display-preferences-model.d.ts +53 -0
- package/dist/display-preferences-model.js +64 -0
- package/dist/emoji.cjs +28 -35
- package/dist/filter-model.d.ts +59 -3
- package/dist/filter-model.js +67 -2
- package/dist/hooks/useChangeHighlight.d.ts +33 -0
- package/dist/hooks/useChangeHighlight.js +44 -0
- package/dist/hooks/useDelayedPending.d.ts +22 -0
- package/dist/hooks/useDelayedPending.js +35 -0
- package/dist/hooks/useDialog.d.ts +5 -0
- package/dist/hooks/useDialog.js +3 -2
- package/dist/hooks/useOptimisticValue.d.ts +51 -0
- package/dist/hooks/useOptimisticValue.js +61 -0
- package/dist/hooks/useReducedMotion.d.ts +4 -1
- package/dist/hooks/useReducedMotion.js +7 -1
- package/dist/hooks/useResolvedFilterLabels.d.ts +15 -0
- package/dist/hooks/useResolvedFilterLabels.js +34 -0
- package/dist/hooks/useVirtualRows.d.ts +21 -2
- package/dist/hooks/useVirtualRows.js +30 -3
- package/dist/icons/Icon.d.ts +1 -1
- package/dist/index.cjs +18977 -13378
- package/dist/index.d.cts +3065 -664
- package/dist/index.d.ts +46 -16
- package/dist/index.js +149 -7
- package/dist/labels-context.d.ts +93 -3
- package/dist/motion-timing.d.ts +8 -0
- package/dist/motion-timing.js +8 -0
- package/dist/overlay-stack.d.ts +6 -0
- package/dist/overlay-stack.js +13 -0
- package/dist/qr-model.d.ts +65 -0
- package/dist/qr-model.js +323 -0
- package/dist/relationship-graph-model.d.ts +78 -3
- package/dist/relationship-graph-model.js +112 -5
- package/dist/reorder.d.ts +30 -0
- package/dist/reorder.js +74 -0
- package/dist/rich-text.cjs +4 -2
- package/dist/scheduling-resource-model.d.ts +76 -0
- package/dist/scheduling-resource-model.js +74 -0
- package/dist/search-suggest-model.js +3 -1
- package/dist/shell-context.d.ts +11 -0
- package/dist/shell-context.js +7 -0
- package/dist/time-field-model.d.ts +46 -0
- package/dist/time-field-model.js +152 -0
- package/dist/toast-store.d.ts +17 -1
- package/dist/toast-store.js +23 -7
- package/package.json +2 -1
|
@@ -2,6 +2,7 @@ import * as react from 'react';
|
|
|
2
2
|
import { ButtonHTMLAttributes, ReactNode, AnchorHTMLAttributes } from 'react';
|
|
3
3
|
import { UixRenderLink } from '../link.js';
|
|
4
4
|
import { KeyShortcut } from './Kbd.js';
|
|
5
|
+
import { ButtonResult, ButtonResultLabels } from './ButtonFeedback.js';
|
|
5
6
|
|
|
6
7
|
type ButtonVariant = 'primary' | 'secondary' | 'outline' | 'ghost' | 'danger' | 'link';
|
|
7
8
|
/** `xs` is 24 px: the smallest hit area WCAG 2.5.8 allows, for dense tool rows (HAR-1348). */
|
|
@@ -10,7 +11,27 @@ interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
|
|
|
10
11
|
variant?: ButtonVariant;
|
|
11
12
|
size?: ButtonSize;
|
|
12
13
|
icon?: boolean;
|
|
14
|
+
/**
|
|
15
|
+
* The button's action is running. The button stops taking presses at once and is announced
|
|
16
|
+
* busy; the spinner appears only after `loadingDelay` and then stays at least 500 ms, so a
|
|
17
|
+
* fast action never flashes one (HAR-2580, motion standard §2.3). The button keeps its full
|
|
18
|
+
* colour while it works: a spinner, not a greyed-out control.
|
|
19
|
+
*/
|
|
13
20
|
loading?: boolean;
|
|
21
|
+
/** Milliseconds before the spinner shows (default 300). `0` shows it at once. */
|
|
22
|
+
loadingDelay?: number;
|
|
23
|
+
/**
|
|
24
|
+
* How the button's last action ended (HAR-2581). When it changes to `success` or `error`,
|
|
25
|
+
* or an action ends with one, the label crossfades to a check and `resultLabels.success`
|
|
26
|
+
* ("Done") or an alert mark and `resultLabels.error` ("Failed") for 1.5 s, announced
|
|
27
|
+
* politely, then returns. Never on the first render. The button is always as wide as its
|
|
28
|
+
* widest wording, so nothing beside it moves. Pass `null` while there is no result (keep
|
|
29
|
+
* the prop defined); say what failed and how to recover somewhere that stays, such as a
|
|
30
|
+
* field error or a toast. e.g. `result={m.isSuccess ? 'success' : m.isError ? 'error' : null}`
|
|
31
|
+
*/
|
|
32
|
+
result?: ButtonResult | null;
|
|
33
|
+
/** The result words, e.g. `{ success: 'Assigned', error: 'Not assigned' }`; or `UixLabelsProvider` `buttonResult`. */
|
|
34
|
+
resultLabels?: Partial<ButtonResultLabels>;
|
|
14
35
|
/**
|
|
15
36
|
* Let a long label wrap onto more lines: the size becomes a minimum height instead of a fixed
|
|
16
37
|
* one (HAR-1813, WCAG 1.4.10 reflow). For human-language labels on public or narrow screens;
|
|
@@ -20,10 +41,11 @@ interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
|
|
|
20
41
|
/**
|
|
21
42
|
* The button's keyboard shortcut, in `KbdCombo` key names: `['A']`, `['Mod', 'Enter']`
|
|
22
43
|
* (HAR-2031). Hover or focus shows the keys in a hint above the button (an icon button's
|
|
23
|
-
* `aria-label` before them
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
44
|
+
* `aria-label` before them; ⌘ on Apple, Ctrl elsewhere). Screen readers hear the keys once, as
|
|
45
|
+
* the button's description after any `aria-describedby` of yours ("Control+Enter"), the same
|
|
46
|
+
* rule as a `Tooltip` with a shortcut (HAR-2416). Inside a `Tooltip` the Tooltip shows and says
|
|
47
|
+
* the keys. An `aria-keyshortcuts` you pass is kept as it is. The kit only shows the shortcut:
|
|
48
|
+
* the product binds the key.
|
|
27
49
|
*/
|
|
28
50
|
shortcut?: KeyShortcut;
|
|
29
51
|
children?: ReactNode;
|
|
@@ -44,13 +66,19 @@ interface ButtonLinkProps extends AnchorHTMLAttributes<HTMLAnchorElement> {
|
|
|
44
66
|
disabled?: boolean;
|
|
45
67
|
/** Your router's link (`(p) => <NextLink {...p} />`); default a plain `<a>`. */
|
|
46
68
|
renderLink?: UixRenderLink;
|
|
69
|
+
/**
|
|
70
|
+
* The link's keyboard shortcut, as on `Button` (HAR-2416): hover or keyboard focus shows the
|
|
71
|
+
* keys in a hint above it, and screen readers hear them once as its description. Not shown
|
|
72
|
+
* while `disabled`. The kit only shows the shortcut: the product binds the key.
|
|
73
|
+
*/
|
|
74
|
+
shortcut?: KeyShortcut;
|
|
47
75
|
children?: ReactNode;
|
|
48
76
|
}
|
|
49
77
|
/**
|
|
50
78
|
* A link that looks like a `Button` (HAR-1348; TENSOR A4/B5, MOTUS C-3). Use it when the
|
|
51
79
|
* action navigates; use `Button` when it acts in place.
|
|
52
80
|
*/
|
|
53
|
-
declare function ButtonLink({ href, variant, size, icon, wrap, disabled, renderLink, className, children, onClick, ...props }: ButtonLinkProps): react.JSX.Element;
|
|
81
|
+
declare function ButtonLink({ href, variant, size, icon, wrap, disabled, renderLink, shortcut, className, children, onClick, ...props }: ButtonLinkProps): react.JSX.Element;
|
|
54
82
|
interface ButtonGroupProps {
|
|
55
83
|
children?: ReactNode;
|
|
56
84
|
className?: string;
|
|
@@ -3,6 +3,8 @@ import { forwardRef } from "react";
|
|
|
3
3
|
import { cx } from "../cx.js";
|
|
4
4
|
import { renderUixLink } from "../link.js";
|
|
5
5
|
import { ShortcutHint } from "./ShortcutHint.js";
|
|
6
|
+
import { ButtonFeedback } from "./ButtonFeedback.js";
|
|
7
|
+
import { PENDING_DELAY_MS } from "../motion-timing.js";
|
|
6
8
|
const buttonClasses = (variant, size, icon, wrap, className) => cx(
|
|
7
9
|
"uix-btn",
|
|
8
10
|
`uix-btn--${variant}`,
|
|
@@ -13,7 +15,7 @@ const buttonClasses = (variant, size, icon, wrap, className) => cx(
|
|
|
13
15
|
);
|
|
14
16
|
let warnedIconButton = false;
|
|
15
17
|
const Button = forwardRef(
|
|
16
|
-
({ variant = "secondary", size = "md", icon = false, loading =
|
|
18
|
+
({ variant = "secondary", size = "md", icon = false, loading, loadingDelay = PENDING_DELAY_MS, result, resultLabels, wrap = false, shortcut, className, children, disabled, onClick, ...props }, ref) => {
|
|
17
19
|
if (!warnedIconButton && icon && children == null && !props["aria-label"] && !props["aria-labelledby"] && typeof process !== "undefined" && process.env.NODE_ENV !== "production") {
|
|
18
20
|
warnedIconButton = true;
|
|
19
21
|
console.warn("uix: icon-only <Button> has no accessible name \u2014 pass aria-label or aria-labelledby.");
|
|
@@ -26,7 +28,7 @@ const Button = forwardRef(
|
|
|
26
28
|
disabled,
|
|
27
29
|
"aria-disabled": loading || void 0,
|
|
28
30
|
"aria-busy": loading || void 0,
|
|
29
|
-
"data-
|
|
31
|
+
"data-pending": loading || void 0,
|
|
30
32
|
onClick: loading || onClick ? (e) => {
|
|
31
33
|
if (loading) {
|
|
32
34
|
e.preventDefault();
|
|
@@ -36,20 +38,24 @@ const Button = forwardRef(
|
|
|
36
38
|
} : void 0,
|
|
37
39
|
...props,
|
|
38
40
|
children: [
|
|
39
|
-
children,
|
|
40
|
-
shortcut && shortcut.length > 0 && /* @__PURE__ */ jsx(ShortcutHint, { keys: shortcut, label: icon ? props["aria-label"] : void 0,
|
|
41
|
+
loading !== void 0 || result !== void 0 ? /* @__PURE__ */ jsx(ButtonFeedback, { loading: !!loading, loadingDelay, result, resultLabels, children }) : children,
|
|
42
|
+
shortcut && shortcut.length > 0 && /* @__PURE__ */ jsx(ShortcutHint, { keys: shortcut, label: icon ? props["aria-label"] : void 0, describedBy: props["aria-describedby"] })
|
|
41
43
|
]
|
|
42
44
|
}
|
|
43
45
|
);
|
|
44
46
|
}
|
|
45
47
|
);
|
|
46
48
|
Button.displayName = "Button";
|
|
47
|
-
function ButtonLink({ href, variant = "secondary", size = "md", icon = false, wrap = false, disabled = false, renderLink, className, children, onClick, ...props }) {
|
|
49
|
+
function ButtonLink({ href, variant = "secondary", size = "md", icon = false, wrap = false, disabled = false, renderLink, shortcut, className, children, onClick, ...props }) {
|
|
48
50
|
const classes = buttonClasses(variant, size, icon, wrap, className);
|
|
49
51
|
if (disabled) {
|
|
50
52
|
return /* @__PURE__ */ jsx("a", { ...props, className: classes, role: "link", "aria-disabled": "true", children });
|
|
51
53
|
}
|
|
52
|
-
|
|
54
|
+
const content = shortcut && shortcut.length > 0 ? /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
55
|
+
children,
|
|
56
|
+
/* @__PURE__ */ jsx(ShortcutHint, { keys: shortcut, label: icon ? props["aria-label"] : void 0, describedBy: props["aria-describedby"] })
|
|
57
|
+
] }) : children;
|
|
58
|
+
return /* @__PURE__ */ jsx(Fragment, { children: renderUixLink(renderLink, { ...props, href, onClick, className: classes, children: content }) });
|
|
53
59
|
}
|
|
54
60
|
function ButtonGroup({ children, className }) {
|
|
55
61
|
return /* @__PURE__ */ jsx("div", { className: cx("uix-btn-group", className), children });
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import * as react from 'react';
|
|
2
|
+
import { ReactNode } from 'react';
|
|
3
|
+
|
|
4
|
+
type ButtonResult = 'success' | 'error';
|
|
5
|
+
/** The words a `Button` shows and announces for a moment after its action ends. */
|
|
6
|
+
interface ButtonResultLabels {
|
|
7
|
+
success: string;
|
|
8
|
+
error: string;
|
|
9
|
+
}
|
|
10
|
+
declare const DEFAULT_BUTTON_RESULT_LABELS: ButtonResultLabels;
|
|
11
|
+
interface ButtonFeedbackProps {
|
|
12
|
+
loading: boolean;
|
|
13
|
+
loadingDelay: number;
|
|
14
|
+
result?: ButtonResult | null;
|
|
15
|
+
resultLabels?: Partial<ButtonResultLabels>;
|
|
16
|
+
children?: ReactNode;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* The client half of `Button`'s `loading` and `result` (HAR-2580, HAR-2581). `Button` itself
|
|
20
|
+
* stays server-renderable; this child keeps the timers. It marks the button busy through a
|
|
21
|
+
* hidden marker (`[data-uix-btn-busy]`, read by `.uix-btn:has(…)`), so the button element is
|
|
22
|
+
* never re-created and keeps focus. With a `result` it also stacks the label and the result
|
|
23
|
+
* words in one grid cell, so the button is always as wide as the widest of them, and
|
|
24
|
+
* announces the result through a polite status region portalled to the body (inside the
|
|
25
|
+
* button it would become part of the button's name), or to the nearest dialog, since a modal
|
|
26
|
+
* dialog makes the body inert. An icon-only button shows the mark alone; the words are spoken.
|
|
27
|
+
*/
|
|
28
|
+
declare function ButtonFeedback({ loading, loadingDelay, result, resultLabels, children }: ButtonFeedbackProps): react.JSX.Element;
|
|
29
|
+
|
|
30
|
+
export { ButtonFeedback, type ButtonFeedbackProps, type ButtonResult, type ButtonResultLabels, DEFAULT_BUTTON_RESULT_LABELS };
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { useEffect, useRef, useState } from "react";
|
|
4
|
+
import { createPortal } from "react-dom";
|
|
5
|
+
import { useDelayedPending } from "../hooks/useDelayedPending.js";
|
|
6
|
+
import { useUixLabels } from "../labels-context.js";
|
|
7
|
+
import { RESULT_HOLD_MS } from "../motion-timing.js";
|
|
8
|
+
const DEFAULT_BUTTON_RESULT_LABELS = {
|
|
9
|
+
success: "Done",
|
|
10
|
+
error: "Failed"
|
|
11
|
+
};
|
|
12
|
+
const SuccessGlyph = () => /* @__PURE__ */ jsx("svg", { viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": "true", children: /* @__PURE__ */ jsx("path", { d: "M20 6 9 17l-5-5" }) });
|
|
13
|
+
const ErrorGlyph = () => /* @__PURE__ */ jsxs("svg", { viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": "true", children: [
|
|
14
|
+
/* @__PURE__ */ jsx("circle", { cx: "12", cy: "12", r: "10" }),
|
|
15
|
+
/* @__PURE__ */ jsx("line", { x1: "12", x2: "12", y1: "8", y2: "12" }),
|
|
16
|
+
/* @__PURE__ */ jsx("line", { x1: "12", x2: "12.01", y1: "16", y2: "16" })
|
|
17
|
+
] });
|
|
18
|
+
function ButtonFeedback({ loading, loadingDelay, result, resultLabels, children }) {
|
|
19
|
+
const busy = useDelayedPending(loading, { delay: loadingDelay });
|
|
20
|
+
const fromProvider = useUixLabels().buttonResult;
|
|
21
|
+
const text = { ...DEFAULT_BUTTON_RESULT_LABELS, ...fromProvider, ...resultLabels };
|
|
22
|
+
const [queued, setQueued] = useState(null);
|
|
23
|
+
const [shown, setShown] = useState(null);
|
|
24
|
+
const previous = useRef({ result: result ?? null, loading });
|
|
25
|
+
useEffect(() => {
|
|
26
|
+
const before = previous.current;
|
|
27
|
+
previous.current = { result: result ?? null, loading };
|
|
28
|
+
if (loading) {
|
|
29
|
+
setQueued(null);
|
|
30
|
+
setShown(null);
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
if (result && (result !== before.result || before.loading)) setQueued(result);
|
|
34
|
+
}, [result, loading]);
|
|
35
|
+
useEffect(() => {
|
|
36
|
+
if (!queued || busy || loading) return;
|
|
37
|
+
setShown((current) => ({ kind: queued, n: (current?.n ?? 0) + 1 }));
|
|
38
|
+
setQueued(null);
|
|
39
|
+
}, [queued, busy, loading]);
|
|
40
|
+
useEffect(() => {
|
|
41
|
+
if (!shown) return void 0;
|
|
42
|
+
const timer = setTimeout(() => setShown(null), RESULT_HOLD_MS);
|
|
43
|
+
return () => clearTimeout(timer);
|
|
44
|
+
}, [shown]);
|
|
45
|
+
const markerRef = useRef(null);
|
|
46
|
+
const [portalTarget, setPortalTarget] = useState(null);
|
|
47
|
+
const withResult = result !== void 0;
|
|
48
|
+
useEffect(() => {
|
|
49
|
+
if (withResult) setPortalTarget(markerRef.current?.closest("dialog") ?? document.body);
|
|
50
|
+
}, [withResult]);
|
|
51
|
+
const marker = /* @__PURE__ */ jsx("span", { ref: markerRef, hidden: true, "data-uix-btn-busy": busy || void 0 });
|
|
52
|
+
if (!withResult) return /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
53
|
+
marker,
|
|
54
|
+
children
|
|
55
|
+
] });
|
|
56
|
+
return /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
57
|
+
marker,
|
|
58
|
+
/* @__PURE__ */ jsxs("span", { className: "uix-btn__face", "data-result": shown?.kind, children: [
|
|
59
|
+
/* @__PURE__ */ jsx("span", { className: "uix-btn__label", children }),
|
|
60
|
+
/* @__PURE__ */ jsxs("span", { className: "uix-btn__result", "data-kind": "success", "aria-hidden": "true", children: [
|
|
61
|
+
/* @__PURE__ */ jsx(SuccessGlyph, {}),
|
|
62
|
+
/* @__PURE__ */ jsx("span", { className: "uix-btn__result-text", children: text.success })
|
|
63
|
+
] }),
|
|
64
|
+
/* @__PURE__ */ jsxs("span", { className: "uix-btn__result", "data-kind": "error", "aria-hidden": "true", children: [
|
|
65
|
+
/* @__PURE__ */ jsx(ErrorGlyph, {}),
|
|
66
|
+
/* @__PURE__ */ jsx("span", { className: "uix-btn__result-text", children: text.error })
|
|
67
|
+
] })
|
|
68
|
+
] }),
|
|
69
|
+
portalTarget && createPortal(
|
|
70
|
+
/* @__PURE__ */ jsx("span", { role: "status", className: "uix-visually-hidden", children: shown ? text[shown.kind] : "" }),
|
|
71
|
+
portalTarget
|
|
72
|
+
)
|
|
73
|
+
] });
|
|
74
|
+
}
|
|
75
|
+
export {
|
|
76
|
+
ButtonFeedback,
|
|
77
|
+
DEFAULT_BUTTON_RESULT_LABELS
|
|
78
|
+
};
|
|
@@ -33,5 +33,22 @@ interface CardLinkProps extends Omit<AnchorHTMLAttributes<HTMLAnchorElement>, 't
|
|
|
33
33
|
}
|
|
34
34
|
/** A whole-card anchor with visible hover and keyboard focus affordances. */
|
|
35
35
|
declare function CardLink({ title, subtitle, headerAction, footer, children, className, ...props }: CardLinkProps): react.JSX.Element;
|
|
36
|
+
interface InsetProps extends HTMLAttributes<HTMLDivElement> {
|
|
37
|
+
/** `md` (default) pads with `--uix-space-4`; `sm` with `--uix-space-3`, for a dense card. */
|
|
38
|
+
padding?: 'sm' | 'md';
|
|
39
|
+
/**
|
|
40
|
+
* `sunken` (default) is `--uix-bg-subtle` on the card's surface; `surface` is for a well on a
|
|
41
|
+
* panel that is already subtle.
|
|
42
|
+
*/
|
|
43
|
+
tone?: 'sunken' | 'surface';
|
|
44
|
+
children?: ReactNode;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* A bordered, sunken well inside a card body (HAR-1818), over `.uix-inset`: for a preview or a
|
|
48
|
+
* specimen (a rendered header, an image crop, a code sample). Use it instead of a `Card` in a
|
|
49
|
+
* `Card`, which brings a second shadow, card padding and card semantics. It lays out nothing:
|
|
50
|
+
* the content is yours. Give it `role="group"` and an `aria-label` when the preview needs a name.
|
|
51
|
+
*/
|
|
52
|
+
declare function Inset({ padding, tone, children, className, ...props }: InsetProps): react.JSX.Element;
|
|
36
53
|
|
|
37
|
-
export { Card, CardLink, type CardLinkProps, type CardProps };
|
|
54
|
+
export { Card, CardLink, type CardLinkProps, type CardProps, Inset, type InsetProps };
|
package/dist/components/Card.js
CHANGED
|
@@ -34,7 +34,11 @@ function CardLink({ title, subtitle, headerAction, footer, children, className,
|
|
|
34
34
|
footer != null && /* @__PURE__ */ jsx("div", { className: "uix-card__footer", children: footer })
|
|
35
35
|
] });
|
|
36
36
|
}
|
|
37
|
+
function Inset({ padding = "md", tone = "sunken", children, className, ...props }) {
|
|
38
|
+
return /* @__PURE__ */ jsx("div", { className: cx("uix-inset", padding === "sm" && "uix-inset--sm", tone === "surface" && "uix-inset--surface", className), ...props, children });
|
|
39
|
+
}
|
|
37
40
|
export {
|
|
38
41
|
Card,
|
|
39
|
-
CardLink
|
|
42
|
+
CardLink,
|
|
43
|
+
Inset
|
|
40
44
|
};
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import * as react from 'react';
|
|
2
|
+
import { HTMLAttributes, ReactNode } from 'react';
|
|
3
|
+
|
|
4
|
+
interface ChangeHighlightProps extends HTMLAttributes<HTMLElement> {
|
|
5
|
+
/** The value shown inside. When it changes after mount the element flashes; never on the first render. */
|
|
6
|
+
value: unknown;
|
|
7
|
+
/** The element to render (default `span`): `td`, `tr`, `div`, `li`… A `tr` tints its cells. */
|
|
8
|
+
as?: keyof HTMLElementTagNameMap;
|
|
9
|
+
/**
|
|
10
|
+
* `tint` (default) fades a background tint, for a row, a cell or a field; `ring` fades an
|
|
11
|
+
* outline instead, for something with a colour of its own, such as a status pill.
|
|
12
|
+
*/
|
|
13
|
+
variant?: 'tint' | 'ring';
|
|
14
|
+
/** Said politely when the value changes, e.g. "Status changed to Resolved by A. Novak". */
|
|
15
|
+
announce?: string;
|
|
16
|
+
/** No highlight, e.g. while the person is editing this value themselves. */
|
|
17
|
+
disabled?: boolean;
|
|
18
|
+
children?: ReactNode;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Shows that a value changed in place (HAR-2585, motion standard §2.5): a live update, the
|
|
22
|
+
* result of a transition, a rolled-back optimistic change. The element is tinted at once and
|
|
23
|
+
* the tint fades; under reduced motion or forced colours it is outlined for the same time
|
|
24
|
+
* instead. The flash is the signal and its place the location; pass `announce` for the
|
|
25
|
+
* description. For an element you render yourself, use `useChangeHighlight`; on plain markup,
|
|
26
|
+
* add `.uix-changed` (remove it, force a reflow and add it again to replay).
|
|
27
|
+
*/
|
|
28
|
+
declare const ChangeHighlight: react.ForwardRefExoticComponent<ChangeHighlightProps & react.RefAttributes<HTMLElement | null>>;
|
|
29
|
+
|
|
30
|
+
export { ChangeHighlight, type ChangeHighlightProps };
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { createElement, forwardRef, useCallback } from "react";
|
|
3
|
+
import { cx } from "../cx.js";
|
|
4
|
+
import { useChangeHighlight } from "../hooks/useChangeHighlight.js";
|
|
5
|
+
const ChangeHighlight = forwardRef(function ChangeHighlight2({ value, as = "span", variant = "tint", announce, disabled, className, children, ...props }, forwarded) {
|
|
6
|
+
const ref = useChangeHighlight(value, { announce, disabled });
|
|
7
|
+
const setRef = useCallback((node) => {
|
|
8
|
+
ref.current = node;
|
|
9
|
+
if (typeof forwarded === "function") forwarded(node);
|
|
10
|
+
else if (forwarded) forwarded.current = node;
|
|
11
|
+
}, [ref, forwarded]);
|
|
12
|
+
return createElement(as, {
|
|
13
|
+
...props,
|
|
14
|
+
ref: setRef,
|
|
15
|
+
className: cx("uix-change-highlight", variant === "ring" && "uix-change-highlight--ring", className)
|
|
16
|
+
}, children);
|
|
17
|
+
});
|
|
18
|
+
ChangeHighlight.displayName = "ChangeHighlight";
|
|
19
|
+
export {
|
|
20
|
+
ChangeHighlight
|
|
21
|
+
};
|
|
@@ -3,6 +3,7 @@ import { jsx, jsxs } from "react/jsx-runtime";
|
|
|
3
3
|
import { useEffect, useId, useRef } from "react";
|
|
4
4
|
import { cx } from "../cx.js";
|
|
5
5
|
import { mergeChartTheme, uixChartTheme, uixChartTokens } from "../chart-theme.js";
|
|
6
|
+
import { prefersReducedMotion } from "../hooks/useReducedMotion.js";
|
|
6
7
|
function readToken(name) {
|
|
7
8
|
if (typeof getComputedStyle === "undefined") return "";
|
|
8
9
|
return getComputedStyle(document.documentElement).getPropertyValue(name).trim();
|
|
@@ -15,7 +16,7 @@ function themed(option, theme, element) {
|
|
|
15
16
|
return theme === "none" ? resolved : mergeChartTheme(uixChartTheme(element), resolved);
|
|
16
17
|
}
|
|
17
18
|
function motionSafe(option) {
|
|
18
|
-
if (
|
|
19
|
+
if (prefersReducedMotion()) {
|
|
19
20
|
return { ...option, animation: false };
|
|
20
21
|
}
|
|
21
22
|
return option;
|
|
@@ -6,5 +6,31 @@ interface CheckboxProps extends Omit<InputHTMLAttributes<HTMLInputElement>, 'typ
|
|
|
6
6
|
indeterminate?: boolean;
|
|
7
7
|
}
|
|
8
8
|
declare const Checkbox: react.ForwardRefExoticComponent<CheckboxProps & react.RefAttributes<HTMLInputElement>>;
|
|
9
|
+
interface CheckboxGroupProps {
|
|
10
|
+
children: ReactNode;
|
|
11
|
+
/** The group's name, rendered as the `<legend>`: announced with each checkbox (HAR-1799). */
|
|
12
|
+
label: ReactNode;
|
|
13
|
+
/** Help under the boxes, tied to the group with `aria-describedby`. */
|
|
14
|
+
hint?: ReactNode;
|
|
15
|
+
/** Shown instead of the hint and announced (`role="alert"`). */
|
|
16
|
+
error?: ReactNode;
|
|
17
|
+
/** Draws the required marker after the legend. Which boxes must be ticked is your rule to check. */
|
|
18
|
+
required?: boolean;
|
|
19
|
+
/** Disables every checkbox in the group (a disabled `<fieldset>`). */
|
|
20
|
+
disabled?: boolean;
|
|
21
|
+
/**
|
|
22
|
+
* The most columns the boxes form. Default 1. A column is never narrower than 10rem: on a phone,
|
|
23
|
+
* or in a narrow card, the boxes fall back to fewer columns.
|
|
24
|
+
*/
|
|
25
|
+
columns?: number;
|
|
26
|
+
id?: string;
|
|
27
|
+
className?: string;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Checkboxes under one name (HAR-1799; MOTUS UXR-05): a `<fieldset>` whose `<legend>` names the
|
|
31
|
+
* group, so a screen reader says "Tags, group" before each box, with the field's hint and error
|
|
32
|
+
* slot below. The checkbox counterpart of `<RadioGroup label>`. Put `Checkbox`es inside.
|
|
33
|
+
*/
|
|
34
|
+
declare function CheckboxGroup({ children, label, hint, error, required, disabled, columns, id, className }: CheckboxGroupProps): react.JSX.Element;
|
|
9
35
|
|
|
10
|
-
export { Checkbox, type CheckboxProps };
|
|
36
|
+
export { Checkbox, CheckboxGroup, type CheckboxGroupProps, type CheckboxProps };
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
3
|
-
import { forwardRef, useEffect, useRef } from "react";
|
|
3
|
+
import { forwardRef, useEffect, useId, useRef } from "react";
|
|
4
4
|
import { cx } from "../cx.js";
|
|
5
5
|
const CheckIcon = () => /* @__PURE__ */ jsx("svg", { viewBox: "0 0 12 12", fill: "none", stroke: "currentColor", strokeWidth: 2.5, strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": "true", children: /* @__PURE__ */ jsx("polyline", { points: "1.5,6 4.5,9 10.5,3" }) });
|
|
6
6
|
const IndeterminateIcon = () => /* @__PURE__ */ jsx("svg", { viewBox: "0 0 12 12", "aria-hidden": "true", children: /* @__PURE__ */ jsx("rect", { x: "2", y: "5", width: "8", height: "2", rx: "1", fill: "currentColor" }) });
|
|
@@ -30,6 +30,35 @@ const Checkbox = forwardRef(
|
|
|
30
30
|
}
|
|
31
31
|
);
|
|
32
32
|
Checkbox.displayName = "Checkbox";
|
|
33
|
+
function CheckboxGroup({ children, label, hint, error, required, disabled, columns = 1, id, className }) {
|
|
34
|
+
const uid = useId();
|
|
35
|
+
const msgId = `${id ?? uid}-msg`;
|
|
36
|
+
const message = error != null && error !== false ? error : hint;
|
|
37
|
+
const cols = Math.max(1, Math.floor(columns));
|
|
38
|
+
return /* @__PURE__ */ jsxs(
|
|
39
|
+
"fieldset",
|
|
40
|
+
{
|
|
41
|
+
id,
|
|
42
|
+
className: cx("uix-checkbox-group", className),
|
|
43
|
+
disabled,
|
|
44
|
+
"aria-describedby": message != null && message !== false ? msgId : void 0,
|
|
45
|
+
"data-invalid": error != null && error !== false || void 0,
|
|
46
|
+
children: [
|
|
47
|
+
/* @__PURE__ */ jsx("legend", { className: "uix-checkbox-group__legend", "data-required": required || void 0, children: label }),
|
|
48
|
+
/* @__PURE__ */ jsx(
|
|
49
|
+
"div",
|
|
50
|
+
{
|
|
51
|
+
className: "uix-checkbox-group__options",
|
|
52
|
+
style: cols > 1 ? { "--uix-checkbox-group-columns": cols } : void 0,
|
|
53
|
+
children
|
|
54
|
+
}
|
|
55
|
+
),
|
|
56
|
+
/* @__PURE__ */ jsx("div", { className: "uix-field__msg", id: msgId, children: error != null && error !== false ? /* @__PURE__ */ jsx("span", { className: "uix-field__error", role: "alert", children: error }) : hint != null && hint !== false ? /* @__PURE__ */ jsx("span", { className: "uix-field__hint", children: hint }) : null })
|
|
57
|
+
]
|
|
58
|
+
}
|
|
59
|
+
);
|
|
60
|
+
}
|
|
33
61
|
export {
|
|
34
|
-
Checkbox
|
|
62
|
+
Checkbox,
|
|
63
|
+
CheckboxGroup
|
|
35
64
|
};
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import * as react from 'react';
|
|
2
|
+
import { HTMLAttributes, ReactNode } from 'react';
|
|
3
|
+
|
|
4
|
+
/** The words of a `CodeDisplay` (HAR-1253). */
|
|
5
|
+
interface CodeDisplayLabels {
|
|
6
|
+
/** The live countdown; `{time}` is `m:ss` (or `h:mm:ss`). */
|
|
7
|
+
expiresIn: string;
|
|
8
|
+
/** Announced once when `warnBefore` seconds are left; `{time}` is that time as `m:ss`. */
|
|
9
|
+
expiresSoon: string;
|
|
10
|
+
/** Shown, and announced once, when the code has expired. Text, so the state never rests on colour. */
|
|
11
|
+
expired: string;
|
|
12
|
+
/** The copy button's accessible name. */
|
|
13
|
+
copy: string;
|
|
14
|
+
/** Announced after a copy. */
|
|
15
|
+
copied: string;
|
|
16
|
+
/** Announced when the copy failed. */
|
|
17
|
+
copyFailed: string;
|
|
18
|
+
}
|
|
19
|
+
declare const DEFAULT_CODE_DISPLAY_LABELS: CodeDisplayLabels;
|
|
20
|
+
interface CodeDisplayProps extends Omit<HTMLAttributes<HTMLDivElement>, 'children'> {
|
|
21
|
+
/**
|
|
22
|
+
* The code, e.g. `"K7Q4PX9MW"` or `"K7Q-4PX-9MW"`. A code that already contains spaces or
|
|
23
|
+
* hyphens is shown in those groups; otherwise it is cut into groups of `groupSize`.
|
|
24
|
+
*/
|
|
25
|
+
code: string;
|
|
26
|
+
/** The visible name above the code, e.g. "Pairing code"; it also names the group. */
|
|
27
|
+
label: ReactNode;
|
|
28
|
+
/** Characters per group, or the length of each group in turn (`[4, 4, 4]`). Default 3. */
|
|
29
|
+
groupSize?: number | readonly number[];
|
|
30
|
+
/** Drawn between groups, never read out. Default `-`. */
|
|
31
|
+
separator?: string;
|
|
32
|
+
/** `md` (default) for a panel; `lg` for a code read across a counter. */
|
|
33
|
+
size?: 'md' | 'lg';
|
|
34
|
+
/** When the code stops working (an ISO instant, epoch milliseconds or a Date): shows a countdown, then the expired state. */
|
|
35
|
+
expiresAt?: string | number | Date;
|
|
36
|
+
/**
|
|
37
|
+
* Seconds before expiry when the countdown turns to a warning and a screen reader hears it
|
|
38
|
+
* once (`labels.expiresSoon`). Default 60; 0 turns the warning off.
|
|
39
|
+
*/
|
|
40
|
+
warnBefore?: number;
|
|
41
|
+
/** Called once when the countdown reaches zero while the component is on screen. */
|
|
42
|
+
onExpire?: () => void;
|
|
43
|
+
/** Adds a copy button while the code is live. */
|
|
44
|
+
copyable?: boolean;
|
|
45
|
+
/**
|
|
46
|
+
* What a screen reader says for one character. Default: the character itself, so "K7Q" is
|
|
47
|
+
* read "K, 7, Q". Use it for a spelling alphabet or to tell O from 0.
|
|
48
|
+
*/
|
|
49
|
+
spell?: (character: string) => string;
|
|
50
|
+
/** The words. Default: `UixLabelsProvider` `codeDisplay`, then English. */
|
|
51
|
+
labels?: Partial<CodeDisplayLabels>;
|
|
52
|
+
}
|
|
53
|
+
/** The code in its display groups. */
|
|
54
|
+
declare function codeGroups(code: string, groupSize?: number | readonly number[]): string[];
|
|
55
|
+
/** `m:ss`, or `h:mm:ss` from an hour up, for a number of whole seconds. */
|
|
56
|
+
declare function formatCountdown(seconds: number): string;
|
|
57
|
+
/**
|
|
58
|
+
* A one-time code shown large (HAR-1253; SHOPx PRE-08 till pairing, the shopper's voucher):
|
|
59
|
+
* monospace characters in groups, spelled character by character to a screen reader, an
|
|
60
|
+
* optional countdown that ends in an expired state said in words, and an optional copy button.
|
|
61
|
+
* The consumer makes the code and enforces its expiry; this only shows it.
|
|
62
|
+
*/
|
|
63
|
+
declare function CodeDisplay({ code, label, groupSize, separator, size, expiresAt, warnBefore, onExpire, copyable, spell, labels: labelsProp, className, ...props }: CodeDisplayProps): react.JSX.Element;
|
|
64
|
+
|
|
65
|
+
export { CodeDisplay, type CodeDisplayLabels, type CodeDisplayProps, DEFAULT_CODE_DISPLAY_LABELS, codeGroups, formatCountdown };
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { useEffect, useId, useRef, useState } from "react";
|
|
4
|
+
import { cx } from "../cx.js";
|
|
5
|
+
import { fillLabel } from "../fill-label.js";
|
|
6
|
+
import { useUixLabels } from "../labels-context.js";
|
|
7
|
+
import { CopyButton } from "./CopyButton.js";
|
|
8
|
+
const DEFAULT_CODE_DISPLAY_LABELS = {
|
|
9
|
+
expiresIn: "Expires in {time}",
|
|
10
|
+
expiresSoon: "{time} left before the code expires",
|
|
11
|
+
expired: "Expired. Ask for a new code.",
|
|
12
|
+
copy: "Copy code",
|
|
13
|
+
copied: "Copied",
|
|
14
|
+
copyFailed: "Could not copy. Select the code instead."
|
|
15
|
+
};
|
|
16
|
+
function codeGroups(code, groupSize = 3) {
|
|
17
|
+
const trimmed = code.trim();
|
|
18
|
+
if (/[\s-]/.test(trimmed)) return trimmed.split(/[\s-]+/).filter(Boolean);
|
|
19
|
+
const sizes = typeof groupSize === "number" ? [groupSize] : groupSize;
|
|
20
|
+
const groups = [];
|
|
21
|
+
for (let i = 0, at = 0; at < trimmed.length; i += 1) {
|
|
22
|
+
const size = Math.max(1, Math.floor(sizes[Math.min(i, sizes.length - 1)] ?? trimmed.length));
|
|
23
|
+
groups.push(trimmed.slice(at, at + size));
|
|
24
|
+
at += size;
|
|
25
|
+
}
|
|
26
|
+
return groups;
|
|
27
|
+
}
|
|
28
|
+
function formatCountdown(seconds) {
|
|
29
|
+
const s = Math.max(0, Math.ceil(seconds));
|
|
30
|
+
const hours = Math.floor(s / 3600);
|
|
31
|
+
const minutes = Math.floor(s % 3600 / 60);
|
|
32
|
+
const rest = String(s % 60).padStart(2, "0");
|
|
33
|
+
return hours > 0 ? `${hours}:${String(minutes).padStart(2, "0")}:${rest}` : `${minutes}:${rest}`;
|
|
34
|
+
}
|
|
35
|
+
const instant = (value) => value === void 0 ? void 0 : new Date(value).getTime();
|
|
36
|
+
function CodeDisplay({
|
|
37
|
+
code,
|
|
38
|
+
label,
|
|
39
|
+
groupSize = 3,
|
|
40
|
+
separator = "-",
|
|
41
|
+
size = "md",
|
|
42
|
+
expiresAt,
|
|
43
|
+
warnBefore = 60,
|
|
44
|
+
onExpire,
|
|
45
|
+
copyable = false,
|
|
46
|
+
spell,
|
|
47
|
+
labels: labelsProp,
|
|
48
|
+
className,
|
|
49
|
+
...props
|
|
50
|
+
}) {
|
|
51
|
+
const uixLabels = useUixLabels();
|
|
52
|
+
const labels = { ...DEFAULT_CODE_DISPLAY_LABELS, ...uixLabels.codeDisplay, ...labelsProp };
|
|
53
|
+
const labelId = useId();
|
|
54
|
+
const deadline = instant(expiresAt);
|
|
55
|
+
const [now, setNow] = useState(() => Date.now());
|
|
56
|
+
const remaining = deadline === void 0 || Number.isNaN(deadline) ? void 0 : deadline - now;
|
|
57
|
+
const expired = remaining !== void 0 && remaining <= 0;
|
|
58
|
+
const warnAt = Math.max(0, warnBefore) * 1e3;
|
|
59
|
+
const phase = remaining === void 0 ? "live" : remaining <= 0 ? "expired" : remaining <= warnAt ? "soon" : "live";
|
|
60
|
+
const [announcement, setAnnouncement] = useState("");
|
|
61
|
+
const shownPhase = useRef(phase);
|
|
62
|
+
const onExpireRef = useRef(onExpire);
|
|
63
|
+
onExpireRef.current = onExpire;
|
|
64
|
+
const labelsRef = useRef(labels);
|
|
65
|
+
labelsRef.current = labels;
|
|
66
|
+
useEffect(() => {
|
|
67
|
+
if (deadline === void 0 || Number.isNaN(deadline)) return void 0;
|
|
68
|
+
let timer;
|
|
69
|
+
const tick = () => {
|
|
70
|
+
const current = Date.now();
|
|
71
|
+
setNow(current);
|
|
72
|
+
const left = deadline - current;
|
|
73
|
+
if (left > 0) timer = setTimeout(tick, left % 1e3 || 1e3);
|
|
74
|
+
};
|
|
75
|
+
tick();
|
|
76
|
+
return () => clearTimeout(timer);
|
|
77
|
+
}, [deadline]);
|
|
78
|
+
useEffect(() => {
|
|
79
|
+
const before = shownPhase.current;
|
|
80
|
+
shownPhase.current = phase;
|
|
81
|
+
if (phase === before) return;
|
|
82
|
+
if (phase === "live") setAnnouncement("");
|
|
83
|
+
else if (phase === "soon" && before === "live") setAnnouncement(fillLabel(labelsRef.current.expiresSoon, { time: formatCountdown(warnAt / 1e3) }));
|
|
84
|
+
else if (phase === "expired") {
|
|
85
|
+
setAnnouncement(labelsRef.current.expired);
|
|
86
|
+
onExpireRef.current?.();
|
|
87
|
+
}
|
|
88
|
+
}, [phase, warnAt]);
|
|
89
|
+
const groups = codeGroups(code, groupSize);
|
|
90
|
+
const say = spell ?? ((character) => character);
|
|
91
|
+
const spoken = groups.map((group) => Array.from(group).map(say).join(" ")).join(", ");
|
|
92
|
+
return /* @__PURE__ */ jsxs(
|
|
93
|
+
"div",
|
|
94
|
+
{
|
|
95
|
+
className: cx("uix-code-display", size === "lg" && "uix-code-display--lg", className),
|
|
96
|
+
role: "group",
|
|
97
|
+
"aria-labelledby": labelId,
|
|
98
|
+
"data-state": phase,
|
|
99
|
+
...props,
|
|
100
|
+
children: [
|
|
101
|
+
/* @__PURE__ */ jsx("div", { className: "uix-code-display__label", id: labelId, children: label }),
|
|
102
|
+
/* @__PURE__ */ jsxs("div", { className: "uix-code-display__row", children: [
|
|
103
|
+
/* @__PURE__ */ jsx("span", { className: "uix-code-display__code", "aria-hidden": "true", translate: "no", children: groups.map((group, index) => (
|
|
104
|
+
// The separator ends its group, so a wrapped line never starts with one.
|
|
105
|
+
/* @__PURE__ */ jsxs("span", { className: "uix-code-display__group", children: [
|
|
106
|
+
group,
|
|
107
|
+
index < groups.length - 1 && /* @__PURE__ */ jsx("span", { className: "uix-code-display__sep", children: separator })
|
|
108
|
+
] }, index)
|
|
109
|
+
)) }),
|
|
110
|
+
/* @__PURE__ */ jsx("span", { className: "uix-code-display__spoken uix-visually-hidden", children: spoken }),
|
|
111
|
+
copyable && !expired && /* @__PURE__ */ jsx(CopyButton, { value: groups.join(""), label: labels.copy, copiedLabel: labels.copied, failedLabel: labels.copyFailed })
|
|
112
|
+
] }),
|
|
113
|
+
remaining !== void 0 && /* @__PURE__ */ jsx("div", { className: "uix-code-display__meta", suppressHydrationWarning: true, children: expired ? labels.expired : fillLabel(labels.expiresIn, { time: formatCountdown(remaining / 1e3) }) }),
|
|
114
|
+
/* @__PURE__ */ jsx("span", { role: "status", className: "uix-visually-hidden", children: announcement })
|
|
115
|
+
]
|
|
116
|
+
}
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
export {
|
|
120
|
+
CodeDisplay,
|
|
121
|
+
DEFAULT_CODE_DISPLAY_LABELS,
|
|
122
|
+
codeGroups,
|
|
123
|
+
formatCountdown
|
|
124
|
+
};
|