@tensor_1/react 2.26.3 → 2.27.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 +51 -0
- package/dist/calendar-model.d.ts +4 -2
- package/dist/components/AppShell.d.ts +1 -1
- package/dist/components/AppShell.js +6 -2
- package/dist/components/Button.d.ts +28 -4
- package/dist/components/Button.js +19 -9
- package/dist/components/Card.d.ts +7 -1
- package/dist/components/Card.js +3 -3
- package/dist/components/Chip.d.ts +43 -0
- package/dist/components/Chip.js +73 -0
- package/dist/components/CollapsibleSection.d.ts +19 -1
- package/dist/components/CollapsibleSection.js +5 -2
- package/dist/components/CollapsibleSectionState.d.ts +11 -0
- package/dist/components/CollapsibleSectionState.js +67 -0
- package/dist/components/CommandPalette.d.ts +1 -1
- package/dist/components/CommandPalette.js +4 -1
- package/dist/components/DateRangePicker.js +3 -1
- package/dist/components/Dialogs.d.ts +54 -3
- package/dist/components/Dialogs.js +167 -6
- package/dist/components/Drawer.d.ts +6 -1
- package/dist/components/Drawer.js +5 -2
- package/dist/components/Kbd.d.ts +27 -0
- package/dist/components/Kbd.js +45 -0
- package/dist/components/Menu.d.ts +51 -0
- package/dist/components/Menu.js +177 -0
- package/dist/components/Modal.d.ts +1 -1
- package/dist/components/Modal.js +4 -1
- package/dist/components/Pagination.d.ts +36 -5
- package/dist/components/Pagination.js +59 -37
- package/dist/components/Peek.d.ts +1 -1
- package/dist/components/Peek.js +6 -1
- package/dist/components/SchedulingCalendar.d.ts +47 -2
- package/dist/components/SchedulingCalendar.js +69 -22
- package/dist/components/SearchSuggest.d.ts +1 -1
- package/dist/components/SearchSuggest.js +4 -1
- package/dist/components/Sidebar.d.ts +7 -2
- package/dist/components/Sidebar.js +15 -4
- package/dist/components/Stat.d.ts +16 -0
- package/dist/components/Stat.js +12 -5
- package/dist/components/Steps.d.ts +48 -0
- package/dist/components/Steps.js +42 -0
- package/dist/components/Toast.d.ts +2 -2
- package/dist/components/Toast.js +7 -2
- package/dist/components/Tooltip.d.ts +10 -4
- package/dist/components/Tooltip.js +2 -2
- package/dist/emoji.cjs +2 -2
- package/dist/index.cjs +2061 -1350
- package/dist/index.d.cts +596 -165
- package/dist/index.d.ts +10 -4
- package/dist/index.js +24 -2
- package/dist/labels-context.d.ts +72 -0
- package/dist/labels-context.js +23 -0
- package/dist/link.d.ts +20 -0
- package/dist/link.js +9 -0
- package/package.json +1 -1
|
@@ -1,10 +1,55 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
3
|
-
import { useEffect, useId, useState } from "react";
|
|
3
|
+
import { useEffect, useId, useRef, useState } from "react";
|
|
4
4
|
import { Button } from "./Button.js";
|
|
5
5
|
import { Input } from "./Input.js";
|
|
6
6
|
import { Modal } from "./Modal.js";
|
|
7
|
-
|
|
7
|
+
import { Popover } from "./Popover.js";
|
|
8
|
+
import { fillLabel } from "../fill-label.js";
|
|
9
|
+
import { resolveAnchor } from "../hooks/useAnchoredPosition.js";
|
|
10
|
+
import { useUixLabels } from "../labels-context.js";
|
|
11
|
+
function ConfirmDialog({
|
|
12
|
+
open,
|
|
13
|
+
title,
|
|
14
|
+
description,
|
|
15
|
+
confirmLabel,
|
|
16
|
+
cancelLabel,
|
|
17
|
+
closeLabel,
|
|
18
|
+
onConfirm,
|
|
19
|
+
onCancel,
|
|
20
|
+
pending,
|
|
21
|
+
destructive,
|
|
22
|
+
children,
|
|
23
|
+
error,
|
|
24
|
+
initialFocus,
|
|
25
|
+
tertiaryLabel,
|
|
26
|
+
onTertiary,
|
|
27
|
+
typeToConfirm,
|
|
28
|
+
typeToConfirmLabel: typeToConfirmLabelProp,
|
|
29
|
+
compensation,
|
|
30
|
+
compensationLabel: compensationLabelProp
|
|
31
|
+
}) {
|
|
32
|
+
const uixLabels = useUixLabels();
|
|
33
|
+
const typeToConfirmLabel = typeToConfirmLabelProp ?? uixLabels.confirmDialog?.typeToConfirm ?? "Type {value} to confirm";
|
|
34
|
+
const compensationLabel = compensationLabelProp ?? uixLabels.confirmDialog?.compensation ?? "How to undo this";
|
|
35
|
+
const id = useId();
|
|
36
|
+
const bodyId = `${id}-body`;
|
|
37
|
+
const typedId = `${id}-typed`;
|
|
38
|
+
const cancelRef = useRef(null);
|
|
39
|
+
const confirmRef = useRef(null);
|
|
40
|
+
const typedRef = useRef(null);
|
|
41
|
+
const [typed, setTyped] = useState("");
|
|
42
|
+
const typedMatches = typeToConfirm == null || typed === typeToConfirm;
|
|
43
|
+
const hasBody = description != null || children != null || compensation != null;
|
|
44
|
+
useEffect(() => {
|
|
45
|
+
if (open) setTyped("");
|
|
46
|
+
}, [open, typeToConfirm]);
|
|
47
|
+
useEffect(() => {
|
|
48
|
+
if (!open) return;
|
|
49
|
+
const focus = initialFocus ?? (destructive ? "cancel" : void 0);
|
|
50
|
+
const target = typeToConfirm != null ? typedRef.current : focus === "cancel" ? cancelRef.current : focus === "confirm" ? confirmRef.current : null;
|
|
51
|
+
target?.focus();
|
|
52
|
+
}, [open, initialFocus, destructive, typeToConfirm]);
|
|
8
53
|
return /* @__PURE__ */ jsx(
|
|
9
54
|
Modal,
|
|
10
55
|
{
|
|
@@ -13,11 +58,126 @@ function ConfirmDialog({ open, title, description, confirmLabel, cancelLabel, cl
|
|
|
13
58
|
title,
|
|
14
59
|
closeLabel,
|
|
15
60
|
role: "alertdialog",
|
|
61
|
+
"aria-describedby": hasBody ? bodyId : void 0,
|
|
16
62
|
footer: /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
17
|
-
/* @__PURE__ */ jsx(Button, { type: "button", onClick:
|
|
18
|
-
/* @__PURE__ */ jsx(Button, {
|
|
63
|
+
tertiaryLabel != null && onTertiary && /* @__PURE__ */ jsx(Button, { type: "button", className: "uix-confirm__tertiary", onClick: () => void onTertiary(), disabled: pending, children: tertiaryLabel }),
|
|
64
|
+
/* @__PURE__ */ jsx(Button, { ref: cancelRef, type: "button", onClick: onCancel, disabled: pending, children: cancelLabel }),
|
|
65
|
+
/* @__PURE__ */ jsx(Button, { ref: confirmRef, type: "button", variant: destructive ? "danger" : "primary", onClick: () => {
|
|
66
|
+
if (typedMatches) void onConfirm();
|
|
67
|
+
}, loading: pending, disabled: !typedMatches, children: confirmLabel })
|
|
19
68
|
] }),
|
|
20
|
-
children:
|
|
69
|
+
children: /* @__PURE__ */ jsxs("div", { className: "uix-confirm", children: [
|
|
70
|
+
hasBody && /* @__PURE__ */ jsxs("div", { id: bodyId, className: "uix-confirm__body", children: [
|
|
71
|
+
description != null && /* @__PURE__ */ jsx("div", { children: description }),
|
|
72
|
+
children,
|
|
73
|
+
compensation != null && /* @__PURE__ */ jsxs("div", { className: "uix-confirm__compensation", children: [
|
|
74
|
+
/* @__PURE__ */ jsx("strong", { className: "uix-confirm__compensation-title", children: compensationLabel }),
|
|
75
|
+
/* @__PURE__ */ jsx("div", { children: compensation })
|
|
76
|
+
] })
|
|
77
|
+
] }),
|
|
78
|
+
typeToConfirm != null && /* @__PURE__ */ jsxs("div", { className: "uix-field", children: [
|
|
79
|
+
/* @__PURE__ */ jsx("label", { className: "uix-field__label", htmlFor: typedId, children: fillLabel(typeToConfirmLabel, { value: typeToConfirm }) }),
|
|
80
|
+
/* @__PURE__ */ jsx(Input, { ref: typedRef, id: typedId, value: typed, autoComplete: "off", spellCheck: false, onChange: (event) => setTyped(event.currentTarget.value) })
|
|
81
|
+
] }),
|
|
82
|
+
error != null && /* @__PURE__ */ jsx("div", { className: "uix-confirm__error", role: "alert", children: error })
|
|
83
|
+
] })
|
|
84
|
+
}
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
function Popconfirm({
|
|
88
|
+
open,
|
|
89
|
+
anchor,
|
|
90
|
+
title,
|
|
91
|
+
description,
|
|
92
|
+
confirmLabel,
|
|
93
|
+
cancelLabel,
|
|
94
|
+
onConfirm,
|
|
95
|
+
onCancel,
|
|
96
|
+
pending,
|
|
97
|
+
destructive,
|
|
98
|
+
compensation,
|
|
99
|
+
compensationLabel: compensationLabelProp,
|
|
100
|
+
placement = "bottom-start"
|
|
101
|
+
}) {
|
|
102
|
+
const uixLabels = useUixLabels();
|
|
103
|
+
const compensationLabel = compensationLabelProp ?? uixLabels.confirmDialog?.compensation ?? "How to undo this";
|
|
104
|
+
const id = useId();
|
|
105
|
+
const panelId = `${id}-popconfirm`;
|
|
106
|
+
const cancelRef = useRef(null);
|
|
107
|
+
const onCancelRef = useRef(onCancel);
|
|
108
|
+
onCancelRef.current = onCancel;
|
|
109
|
+
const hasBody = description != null || compensation != null;
|
|
110
|
+
useEffect(() => {
|
|
111
|
+
const panel = document.getElementById(panelId);
|
|
112
|
+
if (!panel) return;
|
|
113
|
+
const shown = () => {
|
|
114
|
+
try {
|
|
115
|
+
return panel.matches(":popover-open");
|
|
116
|
+
} catch {
|
|
117
|
+
return false;
|
|
118
|
+
}
|
|
119
|
+
};
|
|
120
|
+
if (!open) {
|
|
121
|
+
if (shown()) {
|
|
122
|
+
const focusInside = panel.contains(document.activeElement);
|
|
123
|
+
try {
|
|
124
|
+
panel.hidePopover();
|
|
125
|
+
} catch {
|
|
126
|
+
}
|
|
127
|
+
if (focusInside) resolveAnchor(anchor)?.focus();
|
|
128
|
+
}
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
if (!shown()) {
|
|
132
|
+
try {
|
|
133
|
+
panel.showPopover();
|
|
134
|
+
} catch {
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
cancelRef.current?.focus();
|
|
138
|
+
const onKey = (event) => {
|
|
139
|
+
if (event.key !== "Escape") return;
|
|
140
|
+
event.preventDefault();
|
|
141
|
+
event.stopPropagation();
|
|
142
|
+
onCancelRef.current();
|
|
143
|
+
};
|
|
144
|
+
const onPointer = (event) => {
|
|
145
|
+
const target = event.target;
|
|
146
|
+
if (panel.contains(target) || resolveAnchor(anchor)?.contains(target)) return;
|
|
147
|
+
onCancelRef.current();
|
|
148
|
+
};
|
|
149
|
+
document.addEventListener("keydown", onKey, true);
|
|
150
|
+
document.addEventListener("pointerdown", onPointer, true);
|
|
151
|
+
return () => {
|
|
152
|
+
document.removeEventListener("keydown", onKey, true);
|
|
153
|
+
document.removeEventListener("pointerdown", onPointer, true);
|
|
154
|
+
};
|
|
155
|
+
}, [open, anchor, panelId]);
|
|
156
|
+
return /* @__PURE__ */ jsxs(
|
|
157
|
+
Popover,
|
|
158
|
+
{
|
|
159
|
+
id: panelId,
|
|
160
|
+
popover: "manual",
|
|
161
|
+
anchor,
|
|
162
|
+
placement,
|
|
163
|
+
role: "dialog",
|
|
164
|
+
"aria-labelledby": `${id}-title`,
|
|
165
|
+
"aria-describedby": hasBody ? `${id}-body` : void 0,
|
|
166
|
+
className: "uix-popconfirm",
|
|
167
|
+
children: [
|
|
168
|
+
/* @__PURE__ */ jsx("div", { id: `${id}-title`, className: "uix-popconfirm__title", children: title }),
|
|
169
|
+
hasBody && /* @__PURE__ */ jsxs("div", { id: `${id}-body`, className: "uix-popconfirm__body", children: [
|
|
170
|
+
description != null && /* @__PURE__ */ jsx("div", { children: description }),
|
|
171
|
+
compensation != null && /* @__PURE__ */ jsxs("div", { className: "uix-confirm__compensation", children: [
|
|
172
|
+
/* @__PURE__ */ jsx("strong", { className: "uix-confirm__compensation-title", children: compensationLabel }),
|
|
173
|
+
/* @__PURE__ */ jsx("div", { children: compensation })
|
|
174
|
+
] })
|
|
175
|
+
] }),
|
|
176
|
+
/* @__PURE__ */ jsxs("div", { className: "uix-popconfirm__actions", children: [
|
|
177
|
+
/* @__PURE__ */ jsx(Button, { ref: cancelRef, type: "button", size: "sm", onClick: onCancel, disabled: pending, children: cancelLabel }),
|
|
178
|
+
/* @__PURE__ */ jsx(Button, { type: "button", size: "sm", variant: destructive ? "danger" : "primary", onClick: () => void onConfirm(), loading: pending, children: confirmLabel })
|
|
179
|
+
] })
|
|
180
|
+
]
|
|
21
181
|
}
|
|
22
182
|
);
|
|
23
183
|
}
|
|
@@ -40,7 +200,7 @@ function PromptDialog({ open, title, description, inputLabel, defaultValue = "",
|
|
|
40
200
|
};
|
|
41
201
|
return /* @__PURE__ */ jsx(Modal, { open, onClose: onCancel, title, closeLabel, children: /* @__PURE__ */ jsxs("form", { className: "uix-prompt", onSubmit: handleSubmit, children: [
|
|
42
202
|
description != null && /* @__PURE__ */ jsx("div", { children: description }),
|
|
43
|
-
/* @__PURE__ */ jsx("label", { className: "uix-
|
|
203
|
+
/* @__PURE__ */ jsx("label", { className: "uix-field__label", htmlFor: inputId, children: inputLabel }),
|
|
44
204
|
/* @__PURE__ */ jsx(Input, { id: inputId, value, placeholder, "aria-describedby": error ? errorId : void 0, invalid: error != null, onChange: (event) => setValue(event.currentTarget.value), autoFocus: true }),
|
|
45
205
|
error != null && /* @__PURE__ */ jsx("div", { id: errorId, className: "uix-field__error", role: "alert", children: error }),
|
|
46
206
|
/* @__PURE__ */ jsxs("div", { className: "uix-dialog__footer uix-prompt__actions", children: [
|
|
@@ -51,5 +211,6 @@ function PromptDialog({ open, title, description, inputLabel, defaultValue = "",
|
|
|
51
211
|
}
|
|
52
212
|
export {
|
|
53
213
|
ConfirmDialog,
|
|
214
|
+
Popconfirm,
|
|
54
215
|
PromptDialog
|
|
55
216
|
};
|
|
@@ -12,10 +12,15 @@ interface DrawerProps extends Omit<HTMLAttributes<HTMLDialogElement>, 'title'> {
|
|
|
12
12
|
* can't discard it; Escape and the close button still close. Default `true`.
|
|
13
13
|
*/
|
|
14
14
|
dismissOnBackdrop?: boolean;
|
|
15
|
+
/**
|
|
16
|
+
* Edge the drawer slides from (HAR-1351). `end` (default) is the side sheet; `start` is
|
|
17
|
+
* the phone navigation drawer; `bottom` is a bottom sheet capped at 85% of the viewport.
|
|
18
|
+
*/
|
|
19
|
+
side?: 'end' | 'start' | 'bottom';
|
|
15
20
|
title?: ReactNode;
|
|
16
21
|
children?: ReactNode;
|
|
17
22
|
footer?: ReactNode;
|
|
18
23
|
}
|
|
19
|
-
declare function Drawer({ open, onClose, dismissOnBackdrop, title, children, footer, closeLabel, className, onClick, ...rest }: DrawerProps): react.JSX.Element;
|
|
24
|
+
declare function Drawer({ open, onClose, dismissOnBackdrop, side, title, children, footer, closeLabel: closeLabelProp, className, onClick, ...rest }: DrawerProps): react.JSX.Element;
|
|
20
25
|
|
|
21
26
|
export { Drawer, type DrawerProps };
|
|
@@ -2,13 +2,16 @@
|
|
|
2
2
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
3
3
|
import { useId } from "react";
|
|
4
4
|
import { cx } from "../cx.js";
|
|
5
|
+
import { useUixLabels } from "../labels-context.js";
|
|
5
6
|
import { useDialog } from "../hooks/useDialog.js";
|
|
6
7
|
import { backdropDismiss } from "../hooks/backdropDismiss.js";
|
|
7
8
|
const CloseIcon = () => /* @__PURE__ */ jsx("svg", { width: "16", height: "16", viewBox: "0 0 16 16", fill: "none", stroke: "currentColor", strokeWidth: 1.5, strokeLinecap: "round", "aria-hidden": "true", children: /* @__PURE__ */ jsx("path", { d: "M4 4l8 8M12 4l-8 8" }) });
|
|
8
|
-
function Drawer({ open, onClose, dismissOnBackdrop = true, title, children, footer, closeLabel
|
|
9
|
+
function Drawer({ open, onClose, dismissOnBackdrop = true, side = "end", title, children, footer, closeLabel: closeLabelProp, className, onClick, ...rest }) {
|
|
10
|
+
const uixLabels = useUixLabels();
|
|
11
|
+
const closeLabel = closeLabelProp ?? uixLabels.drawer?.close ?? "Close drawer";
|
|
9
12
|
const ref = useDialog(open, onClose);
|
|
10
13
|
const titleId = useId();
|
|
11
|
-
return /* @__PURE__ */ jsxs("dialog", { ref, className: cx("uix-drawer", className), "aria-labelledby": title ? titleId : void 0, ...rest, onClick: backdropDismiss(onClose, onClick, dismissOnBackdrop), children: [
|
|
14
|
+
return /* @__PURE__ */ jsxs("dialog", { ref, className: cx("uix-drawer", side !== "end" && `uix-drawer--${side}`, className), "aria-labelledby": title ? titleId : void 0, ...rest, onClick: backdropDismiss(onClose, onClick, dismissOnBackdrop), children: [
|
|
12
15
|
(title != null || onClose) && /* @__PURE__ */ jsxs("div", { className: "uix-drawer__header", children: [
|
|
13
16
|
title && /* @__PURE__ */ jsx("h2", { id: titleId, style: { fontWeight: 600, fontSize: "var(--uix-text-h3)", fontFamily: "inherit", letterSpacing: "inherit", lineHeight: "inherit" }, children: title }),
|
|
14
17
|
onClose && /* @__PURE__ */ jsx(
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import * as react from 'react';
|
|
2
|
+
import { HTMLAttributes, ReactNode } from 'react';
|
|
3
|
+
|
|
4
|
+
interface KbdProps extends HTMLAttributes<HTMLElement> {
|
|
5
|
+
children?: ReactNode;
|
|
6
|
+
}
|
|
7
|
+
/** One keyboard key over `.uix-kbd` (HAR-1361; TENSOR C14, MOTUS B-A16). */
|
|
8
|
+
declare function Kbd({ className, children, ...props }: KbdProps): react.JSX.Element;
|
|
9
|
+
type KbdPlatform = 'mac' | 'other';
|
|
10
|
+
interface KbdComboProps extends Omit<HTMLAttributes<HTMLSpanElement>, 'children'> {
|
|
11
|
+
/**
|
|
12
|
+
* Keys in order, e.g. `['Mod', 'K']`. `Mod` is ⌘ on Apple and Ctrl elsewhere; `Shift`,
|
|
13
|
+
* `Alt`, `Enter`, `Escape`, `Backspace`, `Tab` and the arrows get platform glyphs.
|
|
14
|
+
*/
|
|
15
|
+
keys: string[];
|
|
16
|
+
/** Fix the platform (tests, docs). Default: detected after mount; the server renders `other`. */
|
|
17
|
+
platform?: KbdPlatform;
|
|
18
|
+
/** Spoken names for keys, e.g. `{ Mod: 'Steuerung' }`; the glyphs stay. */
|
|
19
|
+
keyLabels?: Partial<Record<string, string>>;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* A shortcut such as ⌘K / Ctrl+K. Screen readers hear the key names ("Command+K"), not the
|
|
23
|
+
* glyphs. The platform is resolved at display time, after mount, so server and client agree.
|
|
24
|
+
*/
|
|
25
|
+
declare function KbdCombo({ keys, platform, keyLabels, className, ...props }: KbdComboProps): react.JSX.Element;
|
|
26
|
+
|
|
27
|
+
export { Kbd, KbdCombo, type KbdComboProps, type KbdPlatform, type KbdProps };
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { useEffect, useState } from "react";
|
|
4
|
+
import { cx } from "../cx.js";
|
|
5
|
+
function Kbd({ className, children, ...props }) {
|
|
6
|
+
return /* @__PURE__ */ jsx("kbd", { className: cx("uix-kbd", className), ...props, children });
|
|
7
|
+
}
|
|
8
|
+
const KEYS = {
|
|
9
|
+
Mod: { mac: ["\u2318", "Command"], other: ["Ctrl", "Control"] },
|
|
10
|
+
Ctrl: { mac: ["\u2303", "Control"], other: ["Ctrl", "Control"] },
|
|
11
|
+
Shift: { mac: ["\u21E7", "Shift"], other: ["Shift", "Shift"] },
|
|
12
|
+
Alt: { mac: ["\u2325", "Option"], other: ["Alt", "Alt"] },
|
|
13
|
+
Enter: { mac: ["\u21B5", "Enter"], other: ["Enter", "Enter"] },
|
|
14
|
+
Escape: { mac: ["Esc", "Escape"], other: ["Esc", "Escape"] },
|
|
15
|
+
Backspace: { mac: ["\u232B", "Backspace"], other: ["Backspace", "Backspace"] },
|
|
16
|
+
Tab: { mac: ["\u21E5", "Tab"], other: ["Tab", "Tab"] },
|
|
17
|
+
ArrowUp: { mac: ["\u2191", "Up arrow"], other: ["\u2191", "Up arrow"] },
|
|
18
|
+
ArrowDown: { mac: ["\u2193", "Down arrow"], other: ["\u2193", "Down arrow"] },
|
|
19
|
+
ArrowLeft: { mac: ["\u2190", "Left arrow"], other: ["\u2190", "Left arrow"] },
|
|
20
|
+
ArrowRight: { mac: ["\u2192", "Right arrow"], other: ["\u2192", "Right arrow"] }
|
|
21
|
+
};
|
|
22
|
+
const detectPlatform = () => {
|
|
23
|
+
if (typeof navigator === "undefined") return "other";
|
|
24
|
+
const nav = navigator;
|
|
25
|
+
return /mac|iphone|ipad|ipod/i.test(nav.userAgentData?.platform ?? navigator.platform ?? "") ? "mac" : "other";
|
|
26
|
+
};
|
|
27
|
+
function KbdCombo({ keys, platform, keyLabels, className, ...props }) {
|
|
28
|
+
const [detected, setDetected] = useState("other");
|
|
29
|
+
useEffect(() => {
|
|
30
|
+
if (!platform) setDetected(detectPlatform());
|
|
31
|
+
}, [platform]);
|
|
32
|
+
const active = platform ?? detected;
|
|
33
|
+
const parts = keys.map((key) => {
|
|
34
|
+
const known = KEYS[key]?.[active];
|
|
35
|
+
return { glyph: known?.[0] ?? key, spoken: keyLabels?.[key] ?? known?.[1] ?? key };
|
|
36
|
+
});
|
|
37
|
+
return /* @__PURE__ */ jsxs("span", { className: cx("uix-kbd-combo", className), ...props, children: [
|
|
38
|
+
/* @__PURE__ */ jsx("span", { className: "uix-visually-hidden", children: parts.map((p) => p.spoken).join("+") }),
|
|
39
|
+
/* @__PURE__ */ jsx("span", { className: "uix-kbd-combo__keys", "aria-hidden": "true", children: parts.map((p, i) => /* @__PURE__ */ jsx("kbd", { className: "uix-kbd", children: p.glyph }, `${keys[i]}-${i}`)) })
|
|
40
|
+
] });
|
|
41
|
+
}
|
|
42
|
+
export {
|
|
43
|
+
Kbd,
|
|
44
|
+
KbdCombo
|
|
45
|
+
};
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import * as react from 'react';
|
|
2
|
+
import { ReactElement, ReactNode } from 'react';
|
|
3
|
+
import { UixRenderLink } from '../link.js';
|
|
4
|
+
import { Placement } from '../overlay-position.js';
|
|
5
|
+
|
|
6
|
+
interface MenuProps {
|
|
7
|
+
/**
|
|
8
|
+
* The menu button, usually a `Button`. It receives `aria-haspopup="menu"`,
|
|
9
|
+
* `aria-expanded`, `aria-controls` and the open/keyboard handlers.
|
|
10
|
+
*/
|
|
11
|
+
trigger: ReactElement;
|
|
12
|
+
/** `MenuItem`, `MenuGroup` and `MenuSeparator`. */
|
|
13
|
+
children?: ReactNode;
|
|
14
|
+
/** Controlled open state; pair with `onOpenChange`. */
|
|
15
|
+
open?: boolean;
|
|
16
|
+
defaultOpen?: boolean;
|
|
17
|
+
onOpenChange?: (open: boolean) => void;
|
|
18
|
+
/** Accessible name of the menu. Default: labelled by the trigger. */
|
|
19
|
+
label?: string;
|
|
20
|
+
placement?: Placement;
|
|
21
|
+
className?: string;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* A menu button with an APG menu (HAR-1359; TENSOR C1, MOTUS B-A14): ↓/↑ move, Home/End,
|
|
25
|
+
* type-to-find, Enter/Space activate, Esc and Tab close, focus returns to the trigger.
|
|
26
|
+
* The surface is the kit `Popover`, so it escapes clipping containers.
|
|
27
|
+
*/
|
|
28
|
+
declare function Menu({ trigger, children, open: controlledOpen, defaultOpen, onOpenChange, label, placement, className }: MenuProps): react.JSX.Element;
|
|
29
|
+
interface MenuItemProps {
|
|
30
|
+
children?: ReactNode;
|
|
31
|
+
/** Runs on click, Enter or Space; the menu then closes and focus returns to the trigger. */
|
|
32
|
+
onSelect?: () => void;
|
|
33
|
+
/** A link item; it navigates instead of running `onSelect`. */
|
|
34
|
+
href?: string;
|
|
35
|
+
renderLink?: UixRenderLink;
|
|
36
|
+
icon?: ReactNode;
|
|
37
|
+
/** Shortcut hint at the end of the row, e.g. `<KbdCombo keys={['Mod', 'D']} />`. */
|
|
38
|
+
shortcut?: ReactNode;
|
|
39
|
+
tone?: 'default' | 'danger';
|
|
40
|
+
disabled?: boolean;
|
|
41
|
+
className?: string;
|
|
42
|
+
}
|
|
43
|
+
declare function MenuItem({ children, onSelect, href, renderLink, icon, shortcut, tone, disabled, className }: MenuItemProps): react.JSX.Element;
|
|
44
|
+
interface MenuGroupProps {
|
|
45
|
+
label: ReactNode;
|
|
46
|
+
children?: ReactNode;
|
|
47
|
+
}
|
|
48
|
+
declare function MenuGroup({ label, children }: MenuGroupProps): react.JSX.Element;
|
|
49
|
+
declare function MenuSeparator(): react.JSX.Element;
|
|
50
|
+
|
|
51
|
+
export { Menu, MenuGroup, type MenuGroupProps, MenuItem, type MenuItemProps, type MenuProps, MenuSeparator };
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { cloneElement, createContext, isValidElement, useContext, useEffect, useId, useRef, useState } from "react";
|
|
4
|
+
import { cx } from "../cx.js";
|
|
5
|
+
import { Popover } from "./Popover.js";
|
|
6
|
+
import { renderUixLink } from "../link.js";
|
|
7
|
+
const MenuContext = createContext(null);
|
|
8
|
+
const ITEM = '[role="menuitem"]:not([aria-disabled="true"])';
|
|
9
|
+
function Menu({ trigger, children, open: controlledOpen, defaultOpen = false, onOpenChange, label, placement = "bottom-start", className }) {
|
|
10
|
+
const id = useId();
|
|
11
|
+
const menuId = `${id}-menu`;
|
|
12
|
+
const triggerId = `${id}-trigger`;
|
|
13
|
+
const triggerRef = useRef(null);
|
|
14
|
+
const [internalOpen, setInternalOpen] = useState(defaultOpen);
|
|
15
|
+
const open = controlledOpen ?? internalOpen;
|
|
16
|
+
const focusOnOpen = useRef("first");
|
|
17
|
+
const typeahead = useRef({ text: "", at: 0 });
|
|
18
|
+
const setOpen = (next) => {
|
|
19
|
+
if (controlledOpen === void 0) setInternalOpen(next);
|
|
20
|
+
onOpenChange?.(next);
|
|
21
|
+
};
|
|
22
|
+
const menu = () => document.getElementById(menuId);
|
|
23
|
+
const items = () => Array.from(menu()?.querySelectorAll(ITEM) ?? []);
|
|
24
|
+
const close = (refocus) => {
|
|
25
|
+
setOpen(false);
|
|
26
|
+
if (refocus) triggerRef.current?.focus();
|
|
27
|
+
};
|
|
28
|
+
useEffect(() => {
|
|
29
|
+
const el = menu();
|
|
30
|
+
if (!el) return;
|
|
31
|
+
const shown = () => {
|
|
32
|
+
try {
|
|
33
|
+
return el.matches(":popover-open");
|
|
34
|
+
} catch {
|
|
35
|
+
return false;
|
|
36
|
+
}
|
|
37
|
+
};
|
|
38
|
+
if (!open) {
|
|
39
|
+
if (shown()) {
|
|
40
|
+
try {
|
|
41
|
+
el.hidePopover();
|
|
42
|
+
} catch {
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
if (!shown()) {
|
|
48
|
+
try {
|
|
49
|
+
el.showPopover();
|
|
50
|
+
} catch {
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
const list = items();
|
|
54
|
+
(focusOnOpen.current === "last" ? list[list.length - 1] : list[0])?.focus();
|
|
55
|
+
const onPointer = (event) => {
|
|
56
|
+
const target = event.target;
|
|
57
|
+
if (el.contains(target) || triggerRef.current?.contains(target)) return;
|
|
58
|
+
setOpen(false);
|
|
59
|
+
};
|
|
60
|
+
document.addEventListener("pointerdown", onPointer, true);
|
|
61
|
+
return () => document.removeEventListener("pointerdown", onPointer, true);
|
|
62
|
+
}, [open, menuId]);
|
|
63
|
+
const onMenuKeyDown = (event) => {
|
|
64
|
+
const list = items();
|
|
65
|
+
const index = list.indexOf(document.activeElement);
|
|
66
|
+
const move = (to) => {
|
|
67
|
+
event.preventDefault();
|
|
68
|
+
list[(to + list.length) % list.length]?.focus();
|
|
69
|
+
};
|
|
70
|
+
if (event.key === "ArrowDown") move(index + 1);
|
|
71
|
+
else if (event.key === "ArrowUp") move(index - 1);
|
|
72
|
+
else if (event.key === "Home") move(0);
|
|
73
|
+
else if (event.key === "End") move(list.length - 1);
|
|
74
|
+
else if (event.key === "Escape") {
|
|
75
|
+
event.preventDefault();
|
|
76
|
+
event.stopPropagation();
|
|
77
|
+
close(true);
|
|
78
|
+
} else if (event.key === "Tab") close(false);
|
|
79
|
+
else if (event.key.length === 1 && /\S/.test(event.key) && !event.ctrlKey && !event.metaKey && !event.altKey) {
|
|
80
|
+
const now = Date.now();
|
|
81
|
+
typeahead.current = { text: now - typeahead.current.at > 600 ? event.key.toLowerCase() : typeahead.current.text + event.key.toLowerCase(), at: now };
|
|
82
|
+
const start = index + (typeahead.current.text.length === 1 ? 1 : 0);
|
|
83
|
+
const order = [...list.slice(start), ...list.slice(0, start)];
|
|
84
|
+
order.find((item) => (item.textContent ?? "").trim().toLowerCase().startsWith(typeahead.current.text))?.focus();
|
|
85
|
+
}
|
|
86
|
+
};
|
|
87
|
+
const triggerProps = isValidElement(trigger) ? trigger.props : {};
|
|
88
|
+
const triggerElement = isValidElement(trigger) ? cloneElement(trigger, {
|
|
89
|
+
ref: (node) => {
|
|
90
|
+
triggerRef.current = node;
|
|
91
|
+
},
|
|
92
|
+
id: triggerProps.id ?? triggerId,
|
|
93
|
+
"aria-haspopup": "menu",
|
|
94
|
+
"aria-expanded": open,
|
|
95
|
+
"aria-controls": open ? menuId : void 0,
|
|
96
|
+
onClick: (event) => {
|
|
97
|
+
triggerProps.onClick?.(event);
|
|
98
|
+
if (event.defaultPrevented) return;
|
|
99
|
+
focusOnOpen.current = "first";
|
|
100
|
+
setOpen(!open);
|
|
101
|
+
},
|
|
102
|
+
onKeyDown: (event) => {
|
|
103
|
+
triggerProps.onKeyDown?.(event);
|
|
104
|
+
if (event.defaultPrevented) return;
|
|
105
|
+
if (event.key === "ArrowDown" || event.key === "ArrowUp") {
|
|
106
|
+
event.preventDefault();
|
|
107
|
+
focusOnOpen.current = event.key === "ArrowUp" ? "last" : "first";
|
|
108
|
+
if (open) {
|
|
109
|
+
const list = items();
|
|
110
|
+
(focusOnOpen.current === "last" ? list[list.length - 1] : list[0])?.focus();
|
|
111
|
+
} else setOpen(true);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}) : trigger;
|
|
115
|
+
return /* @__PURE__ */ jsxs(MenuContext.Provider, { value: { close }, children: [
|
|
116
|
+
triggerElement,
|
|
117
|
+
/* @__PURE__ */ jsx(
|
|
118
|
+
Popover,
|
|
119
|
+
{
|
|
120
|
+
id: menuId,
|
|
121
|
+
popover: "manual",
|
|
122
|
+
anchor: triggerRef,
|
|
123
|
+
placement,
|
|
124
|
+
role: "menu",
|
|
125
|
+
"aria-label": label,
|
|
126
|
+
"aria-labelledby": label ? void 0 : triggerProps.id ?? triggerId,
|
|
127
|
+
className: cx("uix-menu", className),
|
|
128
|
+
onKeyDown: onMenuKeyDown,
|
|
129
|
+
children: open ? children : null
|
|
130
|
+
}
|
|
131
|
+
)
|
|
132
|
+
] });
|
|
133
|
+
}
|
|
134
|
+
function MenuItem({ children, onSelect, href, renderLink, icon, shortcut, tone = "default", disabled, className }) {
|
|
135
|
+
const menu = useContext(MenuContext);
|
|
136
|
+
const classes = cx("uix-menu__item", tone === "danger" && "uix-menu__item--danger", className);
|
|
137
|
+
const body = /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
138
|
+
icon != null && /* @__PURE__ */ jsx("span", { className: "uix-menu__icon", "aria-hidden": "true", children: icon }),
|
|
139
|
+
/* @__PURE__ */ jsx("span", { className: "uix-menu__text", children }),
|
|
140
|
+
shortcut != null && /* @__PURE__ */ jsx("span", { className: "uix-menu__shortcut", children: shortcut })
|
|
141
|
+
] });
|
|
142
|
+
if (href != null && !disabled) {
|
|
143
|
+
return /* @__PURE__ */ jsx(Fragment, { children: renderUixLink(renderLink, { href, role: "menuitem", tabIndex: -1, className: classes, onClick: () => menu?.close(false), children: body }) });
|
|
144
|
+
}
|
|
145
|
+
return /* @__PURE__ */ jsx(
|
|
146
|
+
"button",
|
|
147
|
+
{
|
|
148
|
+
type: "button",
|
|
149
|
+
role: "menuitem",
|
|
150
|
+
tabIndex: -1,
|
|
151
|
+
className: classes,
|
|
152
|
+
"aria-disabled": disabled || void 0,
|
|
153
|
+
onClick: () => {
|
|
154
|
+
if (disabled) return;
|
|
155
|
+
onSelect?.();
|
|
156
|
+
menu?.close(true);
|
|
157
|
+
},
|
|
158
|
+
children: body
|
|
159
|
+
}
|
|
160
|
+
);
|
|
161
|
+
}
|
|
162
|
+
function MenuGroup({ label, children }) {
|
|
163
|
+
const id = useId();
|
|
164
|
+
return /* @__PURE__ */ jsxs("div", { role: "group", "aria-labelledby": id, children: [
|
|
165
|
+
/* @__PURE__ */ jsx("div", { id, className: "uix-menu__label", children: label }),
|
|
166
|
+
children
|
|
167
|
+
] });
|
|
168
|
+
}
|
|
169
|
+
function MenuSeparator() {
|
|
170
|
+
return /* @__PURE__ */ jsx("div", { role: "separator", className: "uix-menu__sep" });
|
|
171
|
+
}
|
|
172
|
+
export {
|
|
173
|
+
Menu,
|
|
174
|
+
MenuGroup,
|
|
175
|
+
MenuItem,
|
|
176
|
+
MenuSeparator
|
|
177
|
+
};
|
|
@@ -11,6 +11,6 @@ interface ModalProps extends Omit<HTMLAttributes<HTMLDialogElement>, 'title'> {
|
|
|
11
11
|
closeLabel?: string;
|
|
12
12
|
role?: 'dialog' | 'alertdialog';
|
|
13
13
|
}
|
|
14
|
-
declare function Modal({ open, onClose, title, children, footer, className, closeLabel, role, ...rest }: ModalProps): react.JSX.Element;
|
|
14
|
+
declare function Modal({ open, onClose, title, children, footer, className, closeLabel: closeLabelProp, role, ...rest }: ModalProps): react.JSX.Element;
|
|
15
15
|
|
|
16
16
|
export { Modal, type ModalProps };
|
package/dist/components/Modal.js
CHANGED
|
@@ -2,9 +2,12 @@
|
|
|
2
2
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
3
3
|
import { useId } from "react";
|
|
4
4
|
import { cx } from "../cx.js";
|
|
5
|
+
import { useUixLabels } from "../labels-context.js";
|
|
5
6
|
import { useDialog } from "../hooks/useDialog.js";
|
|
6
7
|
const CloseIcon = () => /* @__PURE__ */ jsx("svg", { width: "16", height: "16", viewBox: "0 0 16 16", fill: "none", stroke: "currentColor", strokeWidth: 1.5, strokeLinecap: "round", "aria-hidden": "true", children: /* @__PURE__ */ jsx("path", { d: "M4 4l8 8M12 4l-8 8" }) });
|
|
7
|
-
function Modal({ open, onClose, title, children, footer, className, closeLabel
|
|
8
|
+
function Modal({ open, onClose, title, children, footer, className, closeLabel: closeLabelProp, role = "dialog", ...rest }) {
|
|
9
|
+
const uixLabels = useUixLabels();
|
|
10
|
+
const closeLabel = closeLabelProp ?? uixLabels.modal?.close ?? "Close dialog";
|
|
8
11
|
const ref = useDialog(open, onClose);
|
|
9
12
|
const titleId = useId();
|
|
10
13
|
return /* @__PURE__ */ jsxs("dialog", { ref, className: cx("uix-dialog", className), role, "aria-labelledby": title != null ? titleId : void 0, ...rest, children: [
|
|
@@ -1,19 +1,50 @@
|
|
|
1
1
|
import * as react from 'react';
|
|
2
|
-
import { HTMLAttributes } from 'react';
|
|
2
|
+
import { HTMLAttributes, ReactNode } from 'react';
|
|
3
|
+
import { UixRenderLink } from '../link.js';
|
|
3
4
|
|
|
4
5
|
/** TENSOR RX-125 (UIX-04): the pager's accessible names. */
|
|
5
6
|
interface PaginationLabels {
|
|
6
7
|
region: string;
|
|
7
8
|
previous: string;
|
|
8
9
|
next: string;
|
|
10
|
+
/** Cursor mode's "back to the start" control (HAR-1357). */
|
|
11
|
+
first: string;
|
|
9
12
|
}
|
|
10
13
|
declare const DEFAULT_PAGINATION_LABELS: PaginationLabels;
|
|
11
14
|
interface PaginationProps extends Omit<HTMLAttributes<HTMLDivElement>, 'onChange'> {
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
+
/**
|
|
16
|
+
* `offset` (default): numbered pages, needs `page` and `pageCount`.
|
|
17
|
+
* `cursor` (HAR-1357; MOTUS B-A22): keyset paging with no total — First, Previous, an
|
|
18
|
+
* optional `summary` ("21–40") and Next.
|
|
19
|
+
*/
|
|
20
|
+
mode?: 'offset' | 'cursor';
|
|
21
|
+
/** Offset mode: the current page, from 1. */
|
|
22
|
+
page?: number;
|
|
23
|
+
/** Offset mode: the number of pages. */
|
|
24
|
+
pageCount?: number;
|
|
25
|
+
/** Offset mode, button pager: called with the page to show. */
|
|
26
|
+
onChange?: (page: number) => void;
|
|
27
|
+
/**
|
|
28
|
+
* Offset mode, link pager (HAR-1357; MOTUS B-P5): every page is a real link, so a
|
|
29
|
+
* server-rendered list pages without JavaScript. Wins over `onChange`.
|
|
30
|
+
*/
|
|
31
|
+
hrefFor?: (page: number) => string;
|
|
32
|
+
/** Your router's link for `hrefFor` and the cursor hrefs; default a plain `<a>`. */
|
|
33
|
+
renderLink?: UixRenderLink;
|
|
34
|
+
/** Cursor mode: whether an earlier page exists (enables First and Previous). */
|
|
35
|
+
hasPrevious?: boolean;
|
|
36
|
+
/** Cursor mode: whether a later page exists. */
|
|
37
|
+
hasNext?: boolean;
|
|
38
|
+
onFirst?: () => void;
|
|
39
|
+
onPrevious?: () => void;
|
|
40
|
+
onNext?: () => void;
|
|
41
|
+
firstHref?: string;
|
|
42
|
+
previousHref?: string;
|
|
43
|
+
nextHref?: string;
|
|
44
|
+
/** Cursor mode: text between the controls, e.g. "21–40". */
|
|
45
|
+
summary?: ReactNode;
|
|
15
46
|
labels?: Partial<PaginationLabels>;
|
|
16
47
|
}
|
|
17
|
-
declare function Pagination({ page, pageCount, onChange, labels: labelOverrides, className, ...props }: PaginationProps): react.JSX.Element;
|
|
48
|
+
declare function Pagination({ mode, page, pageCount, onChange, hrefFor, renderLink, hasPrevious, hasNext, onFirst, onPrevious, onNext, firstHref, previousHref, nextHref, summary, labels: labelOverrides, className, ...props }: PaginationProps): react.JSX.Element;
|
|
18
49
|
|
|
19
50
|
export { DEFAULT_PAGINATION_LABELS, Pagination, type PaginationLabels, type PaginationProps };
|