@tensor_1/react 2.26.2 → 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/EmojiPicker.d.ts +3 -0
- package/dist/components/EmojiPicker.js +109 -54
- 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/Popover.d.ts +15 -5
- package/dist/components/Popover.js +66 -6
- 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-image-WQLh8ZQP.d.cts → emoji-image-6mPZ15Zy.d.cts} +4 -1
- package/dist/emoji.cjs +326 -76
- package/dist/emoji.d.cts +3 -3
- package/dist/hooks/useAnchoredPosition.d.ts +13 -5
- package/dist/hooks/useAnchoredPosition.js +146 -15
- package/dist/index.cjs +2278 -1372
- package/dist/index.d.cts +622 -176
- 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/dist/{overlay-position-CEGSz6Z2.d.cts → overlay-position-Cjv5E7mn.d.cts} +7 -0
- package/dist/overlay-position.d.ts +7 -0
- package/dist/overlay-position.js +7 -2
- package/dist/rich-text.cjs +324 -74
- package/dist/rich-text.d.cts +3 -3
- package/package.json +1 -1
|
@@ -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 };
|
|
@@ -1,5 +1,7 @@
|
|
|
1
|
-
import { jsx, jsxs } from "react/jsx-runtime";
|
|
1
|
+
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Fragment as Fragment2 } from "react";
|
|
2
3
|
import { cx } from "../cx.js";
|
|
4
|
+
import { renderUixLink } from "../link.js";
|
|
3
5
|
function getPageNumbers(page, total) {
|
|
4
6
|
if (total <= 7) return Array.from({ length: total }, (_, i) => i + 1);
|
|
5
7
|
const pages = [1];
|
|
@@ -14,45 +16,65 @@ function getPageNumbers(page, total) {
|
|
|
14
16
|
const DEFAULT_PAGINATION_LABELS = {
|
|
15
17
|
region: "Pagination",
|
|
16
18
|
previous: "Previous page",
|
|
17
|
-
next: "Next page"
|
|
19
|
+
next: "Next page",
|
|
20
|
+
first: "First page"
|
|
18
21
|
};
|
|
19
|
-
function Pagination({
|
|
22
|
+
function Pagination({
|
|
23
|
+
mode = "offset",
|
|
24
|
+
page = 1,
|
|
25
|
+
pageCount = 1,
|
|
26
|
+
onChange,
|
|
27
|
+
hrefFor,
|
|
28
|
+
renderLink,
|
|
29
|
+
hasPrevious = false,
|
|
30
|
+
hasNext = false,
|
|
31
|
+
onFirst,
|
|
32
|
+
onPrevious,
|
|
33
|
+
onNext,
|
|
34
|
+
firstHref,
|
|
35
|
+
previousHref,
|
|
36
|
+
nextHref,
|
|
37
|
+
summary,
|
|
38
|
+
labels: labelOverrides,
|
|
39
|
+
className,
|
|
40
|
+
...props
|
|
41
|
+
}) {
|
|
20
42
|
const labels = { ...DEFAULT_PAGINATION_LABELS, ...labelOverrides };
|
|
21
|
-
const
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
{
|
|
43
|
+
const render = ({ key, content, label, current, disabled, href, onClick }) => {
|
|
44
|
+
if (href != null && !disabled) {
|
|
45
|
+
return /* @__PURE__ */ jsx(Fragment2, { children: renderUixLink(renderLink, {
|
|
46
|
+
href,
|
|
26
47
|
className: "uix-pagination__btn",
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
)
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
)
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
"
|
|
52
|
-
|
|
53
|
-
}
|
|
54
|
-
)
|
|
55
|
-
|
|
48
|
+
"aria-label": label,
|
|
49
|
+
"aria-current": current ? "page" : void 0,
|
|
50
|
+
children: content
|
|
51
|
+
}) }, key);
|
|
52
|
+
}
|
|
53
|
+
if (href != null || disabled && onClick == null) {
|
|
54
|
+
return /* @__PURE__ */ jsx("a", { className: "uix-pagination__btn", role: "link", "aria-disabled": "true", "aria-label": label, children: content }, key);
|
|
55
|
+
}
|
|
56
|
+
return /* @__PURE__ */ jsx("button", { type: "button", className: "uix-pagination__btn", "aria-label": label, "aria-current": current ? "page" : void 0, disabled, onClick, children: content }, key);
|
|
57
|
+
};
|
|
58
|
+
let controls;
|
|
59
|
+
if (mode === "cursor") {
|
|
60
|
+
const linked = firstHref != null || previousHref != null || nextHref != null;
|
|
61
|
+
controls = /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
62
|
+
(onFirst || firstHref != null) && render({ key: "first", content: "\xAB", label: labels.first, disabled: !hasPrevious, href: linked ? firstHref ?? "" : void 0, onClick: onFirst }),
|
|
63
|
+
render({ key: "prev", content: "\u2039", label: labels.previous, disabled: !hasPrevious, href: linked ? previousHref ?? "" : void 0, onClick: onPrevious }),
|
|
64
|
+
summary != null && /* @__PURE__ */ jsx("span", { className: "uix-pagination__summary", children: summary }),
|
|
65
|
+
render({ key: "next", content: "\u203A", label: labels.next, disabled: !hasNext, href: linked ? nextHref ?? "" : void 0, onClick: onNext })
|
|
66
|
+
] });
|
|
67
|
+
} else {
|
|
68
|
+
const go = (target) => hrefFor ? { href: hrefFor(target) } : { onClick: () => onChange?.(target) };
|
|
69
|
+
controls = /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
70
|
+
render({ key: "prev", content: "\u2039", label: labels.previous, disabled: page <= 1, ...go(page - 1) }),
|
|
71
|
+
getPageNumbers(page, pageCount).map(
|
|
72
|
+
(p, i) => p === "ellipsis" ? /* @__PURE__ */ jsx("span", { className: "uix-pagination__ellipsis", children: "\u2026" }, `e${i}`) : render({ key: `p${p}`, content: p, current: p === page, ...go(p) })
|
|
73
|
+
),
|
|
74
|
+
render({ key: "next", content: "\u203A", label: labels.next, disabled: page >= pageCount, ...go(page + 1) })
|
|
75
|
+
] });
|
|
76
|
+
}
|
|
77
|
+
return /* @__PURE__ */ jsx("nav", { "aria-label": labels.region, className: cx("uix-pagination", className), ...props, children: controls });
|
|
56
78
|
}
|
|
57
79
|
export {
|
|
58
80
|
DEFAULT_PAGINATION_LABELS,
|
|
@@ -17,6 +17,6 @@ interface PeekProps extends Omit<HTMLAttributes<HTMLDialogElement>, 'title'> {
|
|
|
17
17
|
onNavNext?: () => void;
|
|
18
18
|
hint?: string;
|
|
19
19
|
}
|
|
20
|
-
declare function Peek({ open, onClose, title, children, footer, onNavPrev, onNavNext, hint, previousLabel, nextLabel, closeLabel, className, onClick, ...rest }: PeekProps): react.JSX.Element;
|
|
20
|
+
declare function Peek({ open, onClose, title, children, footer, onNavPrev, onNavNext, hint, previousLabel: previousLabelProp, nextLabel: nextLabelProp, closeLabel: closeLabelProp, className, onClick, ...rest }: PeekProps): react.JSX.Element;
|
|
21
21
|
|
|
22
22
|
export { Peek, type PeekProps };
|
package/dist/components/Peek.js
CHANGED
|
@@ -2,12 +2,17 @@
|
|
|
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
9
|
const ChevronUp = () => /* @__PURE__ */ jsx("svg", { width: "12", height: "12", viewBox: "0 0 12 12", fill: "none", stroke: "currentColor", strokeWidth: 1.5, strokeLinecap: "round", "aria-hidden": "true", children: /* @__PURE__ */ jsx("path", { d: "M2 8l4-4 4 4" }) });
|
|
9
10
|
const ChevronDown = () => /* @__PURE__ */ jsx("svg", { width: "12", height: "12", viewBox: "0 0 12 12", fill: "none", stroke: "currentColor", strokeWidth: 1.5, strokeLinecap: "round", "aria-hidden": "true", children: /* @__PURE__ */ jsx("path", { d: "M2 4l4 4 4-4" }) });
|
|
10
|
-
function Peek({ open, onClose, title, children, footer, onNavPrev, onNavNext, hint, previousLabel
|
|
11
|
+
function Peek({ open, onClose, title, children, footer, onNavPrev, onNavNext, hint, previousLabel: previousLabelProp, nextLabel: nextLabelProp, closeLabel: closeLabelProp, className, onClick, ...rest }) {
|
|
12
|
+
const uixLabels = useUixLabels();
|
|
13
|
+
const previousLabel = previousLabelProp ?? uixLabels.peek?.previous ?? "Previous record";
|
|
14
|
+
const nextLabel = nextLabelProp ?? uixLabels.peek?.next ?? "Next record";
|
|
15
|
+
const closeLabel = closeLabelProp ?? uixLabels.peek?.close ?? "Close preview";
|
|
11
16
|
const ref = useDialog(open, onClose);
|
|
12
17
|
const hasNav = onNavPrev != null || onNavNext != null;
|
|
13
18
|
const titleId = useId();
|
|
@@ -10,16 +10,26 @@ interface PopoverProps extends HTMLAttributes<HTMLDivElement> {
|
|
|
10
10
|
popover?: 'auto' | 'manual';
|
|
11
11
|
/**
|
|
12
12
|
* Element (or ref) to anchor against. When set, the popover is placed with
|
|
13
|
-
* cross-browser JS positioning — flip when it won't fit, shift to stay on-screen
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
* the top layer (so it
|
|
13
|
+
* cross-browser JS positioning — flip when it won't fit, shift to stay on-screen.
|
|
14
|
+
* Where the browser supports CSS anchor positioning, the placement is written as an
|
|
15
|
+
* offset from the anchor, so it stays attached while the page scrolls; elsewhere it
|
|
16
|
+
* follows scroll from JS. The native Popover API still provides the top layer (so it
|
|
17
|
+
* escapes `overflow` clipping) and light-dismiss.
|
|
17
18
|
*/
|
|
18
19
|
anchor?: RefObject<HTMLElement | null> | HTMLElement | null;
|
|
19
20
|
/** Preferred placement when `anchor` is set. Default `'bottom-start'`. */
|
|
20
21
|
placement?: Placement;
|
|
21
22
|
/** Gap between the trigger and the popover, in px. Default 6. */
|
|
22
23
|
offset?: number;
|
|
24
|
+
/**
|
|
25
|
+
* Close the popover once its `anchor` has scrolled completely out of view (outside the
|
|
26
|
+
* viewport or a clipping ancestor), e.g. a picker opened from an editor that the user
|
|
27
|
+
* scrolls away from. Focus inside the popover is let go rather than sent back to the
|
|
28
|
+
* hidden anchor. Needs `anchor`. Default `false`.
|
|
29
|
+
*/
|
|
30
|
+
closeWhenAnchorHidden?: boolean;
|
|
31
|
+
/** Called when `closeWhenAnchorHidden` is about to close the popover. */
|
|
32
|
+
onAnchorHidden?: () => void;
|
|
23
33
|
children?: ReactNode;
|
|
24
34
|
}
|
|
25
35
|
/**
|
|
@@ -27,6 +37,6 @@ interface PopoverProps extends HTMLAttributes<HTMLDivElement> {
|
|
|
27
37
|
* things like a filter popover, menu, or rich select by putting controls inside.
|
|
28
38
|
* Pass `anchor` to get collision-aware, cross-browser placement (UIX-FIX-02).
|
|
29
39
|
*/
|
|
30
|
-
declare function Popover({ popover, anchor, placement, offset, className, children, ...props }: PopoverProps): react.JSX.Element;
|
|
40
|
+
declare function Popover({ popover, anchor, placement, offset, closeWhenAnchorHidden, onAnchorHidden, className, children, ...props }: PopoverProps): react.JSX.Element;
|
|
31
41
|
|
|
32
42
|
export { Popover, type PopoverProps };
|
|
@@ -1,19 +1,38 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { jsx } from "react/jsx-runtime";
|
|
3
|
-
import { useEffect, useRef, useState } from "react";
|
|
3
|
+
import { useEffect, useLayoutEffect, useRef, useState } from "react";
|
|
4
4
|
import { cx } from "../cx.js";
|
|
5
|
-
import { useAnchoredPosition } from "../hooks/useAnchoredPosition.js";
|
|
5
|
+
import { resolveAnchor, useAnchoredPosition } from "../hooks/useAnchoredPosition.js";
|
|
6
|
+
const useIsomorphicLayoutEffect = typeof document !== "undefined" ? useLayoutEffect : useEffect;
|
|
7
|
+
const PLACING = "data-uix-placing";
|
|
8
|
+
const isPopoverOpen = (el) => {
|
|
9
|
+
try {
|
|
10
|
+
return el.matches(":popover-open");
|
|
11
|
+
} catch {
|
|
12
|
+
return false;
|
|
13
|
+
}
|
|
14
|
+
};
|
|
15
|
+
const settle = (el) => {
|
|
16
|
+
if (!el.hasAttribute(PLACING)) return;
|
|
17
|
+
getComputedStyle(el).getPropertyValue("transform");
|
|
18
|
+
el.removeAttribute(PLACING);
|
|
19
|
+
};
|
|
6
20
|
function Popover({
|
|
7
21
|
popover = "auto",
|
|
8
22
|
anchor,
|
|
9
23
|
placement = "bottom-start",
|
|
10
24
|
offset = 6,
|
|
25
|
+
closeWhenAnchorHidden = false,
|
|
26
|
+
onAnchorHidden,
|
|
11
27
|
className,
|
|
12
28
|
children,
|
|
13
29
|
...props
|
|
14
30
|
}) {
|
|
15
31
|
const ref = useRef(null);
|
|
16
32
|
const [open, setOpen] = useState(false);
|
|
33
|
+
const openRef = useRef(false);
|
|
34
|
+
const onAnchorHiddenRef = useRef(onAnchorHidden);
|
|
35
|
+
onAnchorHiddenRef.current = onAnchorHidden;
|
|
17
36
|
const popoverAttr = { popover };
|
|
18
37
|
const reposition = useAnchoredPosition(anchor ?? null, ref, {
|
|
19
38
|
open: open && !!anchor,
|
|
@@ -23,14 +42,55 @@ function Popover({
|
|
|
23
42
|
useEffect(() => {
|
|
24
43
|
const el = ref.current;
|
|
25
44
|
if (!el || !anchor) return;
|
|
45
|
+
const hold = (event) => {
|
|
46
|
+
if (event.newState !== "open") return;
|
|
47
|
+
el.setAttribute(PLACING, "");
|
|
48
|
+
window.setTimeout(() => settle(el), 150);
|
|
49
|
+
};
|
|
26
50
|
const sync = () => {
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
51
|
+
if (!isPopoverOpen(el)) {
|
|
52
|
+
el.removeAttribute(PLACING);
|
|
53
|
+
setOpen(false);
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
reposition();
|
|
57
|
+
window.setTimeout(() => {
|
|
58
|
+
if (!isPopoverOpen(el)) return;
|
|
59
|
+
if (!openRef.current) {
|
|
60
|
+
setOpen(true);
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
reposition();
|
|
64
|
+
settle(el);
|
|
65
|
+
}, 0);
|
|
30
66
|
};
|
|
67
|
+
el.addEventListener("beforetoggle", hold);
|
|
31
68
|
el.addEventListener("toggle", sync);
|
|
32
|
-
return () =>
|
|
69
|
+
return () => {
|
|
70
|
+
el.removeEventListener("beforetoggle", hold);
|
|
71
|
+
el.removeEventListener("toggle", sync);
|
|
72
|
+
el.removeAttribute(PLACING);
|
|
73
|
+
};
|
|
33
74
|
}, [anchor, reposition]);
|
|
75
|
+
useIsomorphicLayoutEffect(() => {
|
|
76
|
+
openRef.current = open;
|
|
77
|
+
if (open && ref.current) settle(ref.current);
|
|
78
|
+
}, [open]);
|
|
79
|
+
useEffect(() => {
|
|
80
|
+
const el = ref.current;
|
|
81
|
+
const anchorEl = resolveAnchor(anchor);
|
|
82
|
+
if (!closeWhenAnchorHidden || !open || !el || !anchorEl || typeof IntersectionObserver !== "function") return;
|
|
83
|
+
const observer = new IntersectionObserver((entries) => {
|
|
84
|
+
const latest = entries[entries.length - 1];
|
|
85
|
+
if (!latest || latest.isIntersecting || !isPopoverOpen(el)) return;
|
|
86
|
+
onAnchorHiddenRef.current?.();
|
|
87
|
+
const active = el.ownerDocument.activeElement;
|
|
88
|
+
if (active && el.contains(active)) active.blur();
|
|
89
|
+
el.hidePopover();
|
|
90
|
+
});
|
|
91
|
+
observer.observe(anchorEl);
|
|
92
|
+
return () => observer.disconnect();
|
|
93
|
+
}, [closeWhenAnchorHidden, open, anchor]);
|
|
34
94
|
return /* @__PURE__ */ jsx("div", { ref, className: cx("uix-popover", className), ...popoverAttr, ...props, children });
|
|
35
95
|
}
|
|
36
96
|
export {
|
|
@@ -1,9 +1,17 @@
|
|
|
1
1
|
import * as react from 'react';
|
|
2
2
|
import { ReactNode } from 'react';
|
|
3
|
+
import { CalendarWeekday } from '../calendar-model.js';
|
|
3
4
|
|
|
4
5
|
type SchedulingCalendarView = 'month' | 'week' | 'agenda';
|
|
5
6
|
type SchedulingEntryState = 'scheduled' | 'conflicted' | 'in-progress' | 'blackout-violation';
|
|
6
7
|
type SchedulingOverlayKind = 'maintenance' | 'blackout';
|
|
8
|
+
/**
|
|
9
|
+
* Which date text a `formatDate` call is for (TENSOR B1/C9, HAR-1347):
|
|
10
|
+
* `day` names one day (each day cell's accessible name, the week-range title);
|
|
11
|
+
* `weekday` is the short column header (the date is any day of that weekday);
|
|
12
|
+
* `month` is the month-view title (the date is the first of the month).
|
|
13
|
+
*/
|
|
14
|
+
type SchedulingDatePart = 'day' | 'weekday' | 'month';
|
|
7
15
|
interface SchedulingCalendarEntry {
|
|
8
16
|
id: string;
|
|
9
17
|
title: string;
|
|
@@ -38,6 +46,12 @@ interface SchedulingCalendarLabels {
|
|
|
38
46
|
grid: string;
|
|
39
47
|
entry: string;
|
|
40
48
|
legend: string;
|
|
49
|
+
/** The "+N more" toggle on a day with more entries than `maxEntriesPerDay`. `{count}` placeholder. */
|
|
50
|
+
moreEntries: string;
|
|
51
|
+
/** Accessible name of that toggle. `{count}` and `{date}` placeholders. */
|
|
52
|
+
moreEntriesLabel: string;
|
|
53
|
+
/** The toggle once the day is expanded in place. */
|
|
54
|
+
fewerEntries: string;
|
|
41
55
|
states: Record<SchedulingEntryState, string>;
|
|
42
56
|
}
|
|
43
57
|
declare const DEFAULT_SCHEDULING_CALENDAR_LABELS: SchedulingCalendarLabels;
|
|
@@ -53,6 +67,37 @@ interface SchedulingCalendarProps {
|
|
|
53
67
|
filter?: (entry: SchedulingCalendarEntry) => boolean;
|
|
54
68
|
renderEntry?: (entry: SchedulingCalendarEntry) => ReactNode;
|
|
55
69
|
locale?: string;
|
|
70
|
+
/** First column of the month and week grids: 0 = Sunday … 6 = Saturday. Defaults to 1 (Monday). */
|
|
71
|
+
weekStartsOn?: CalendarWeekday;
|
|
72
|
+
/**
|
|
73
|
+
* Formats every calendar date the component writes (`date` is `YYYY-MM-DD`). Use it to
|
|
74
|
+
* apply a product date format such as DD.MM.YYYY; defaults to `Intl` in `locale`.
|
|
75
|
+
*/
|
|
76
|
+
formatDate?: (date: string, part: SchedulingDatePart) => string;
|
|
77
|
+
/**
|
|
78
|
+
* Formats an entry's start or end instant (ISO 8601) for the agenda and the entry names.
|
|
79
|
+
* The component does not shift it: render it in `timeZone` yourself. Defaults to `Intl`
|
|
80
|
+
* medium date + short time in `timeZone`.
|
|
81
|
+
*/
|
|
82
|
+
formatInstant?: (instant: string) => string;
|
|
83
|
+
/**
|
|
84
|
+
* Show at most this many entries in a month or week day; the rest collapse behind a
|
|
85
|
+
* "+N more" toggle. Unset shows every entry.
|
|
86
|
+
*/
|
|
87
|
+
maxEntriesPerDay?: number;
|
|
88
|
+
/**
|
|
89
|
+
* Called by the "+N more" toggle with the day and every entry on it. When set, the
|
|
90
|
+
* consumer shows them (a popover, a drawer, the day view) and the day stays collapsed;
|
|
91
|
+
* otherwise the day expands in place.
|
|
92
|
+
*/
|
|
93
|
+
onShowMore?: (date: string, entries: SchedulingCalendarEntry[]) => void;
|
|
94
|
+
/**
|
|
95
|
+
* A small badge beside a day's date, e.g. a collision count. Keep it short (a number or a
|
|
96
|
+
* glyph): it shares a narrow column with the date and is clipped, not wrapped. It sits
|
|
97
|
+
* outside the date button, so give it its own accessible text (a visually hidden word next
|
|
98
|
+
* to the number). Return null for no badge.
|
|
99
|
+
*/
|
|
100
|
+
renderDayBadge?: (date: string, entries: SchedulingCalendarEntry[]) => ReactNode;
|
|
56
101
|
loading?: boolean;
|
|
57
102
|
error?: string;
|
|
58
103
|
onRetry?: () => void;
|
|
@@ -60,6 +105,6 @@ interface SchedulingCalendarProps {
|
|
|
60
105
|
labels?: Partial<SchedulingCalendarLabels>;
|
|
61
106
|
}
|
|
62
107
|
/** Month/week/agenda calendar for UTC ranges rendered in an explicit IANA time zone. */
|
|
63
|
-
declare function SchedulingCalendar({ entries, anchorDate, timeZone, view: controlledView, onViewChange, onAnchorDateChange, onSelectEntry, overlays, filter, renderEntry, locale, loading, error, onRetry, className, labels: labelOverrides, }: SchedulingCalendarProps): react.JSX.Element;
|
|
108
|
+
declare function SchedulingCalendar({ entries, anchorDate, timeZone, view: controlledView, onViewChange, onAnchorDateChange, onSelectEntry, overlays, filter, renderEntry, locale, weekStartsOn, formatDate, formatInstant: formatInstantProp, maxEntriesPerDay, onShowMore, renderDayBadge, loading, error, onRetry, className, labels: labelOverrides, }: SchedulingCalendarProps): react.JSX.Element;
|
|
64
109
|
|
|
65
|
-
export { DEFAULT_SCHEDULING_CALENDAR_LABELS, SchedulingCalendar, type SchedulingCalendarEntry, type SchedulingCalendarLabels, type SchedulingCalendarOverlay, type SchedulingCalendarProps, type SchedulingCalendarView, type SchedulingEntryState, type SchedulingOverlayKind };
|
|
110
|
+
export { DEFAULT_SCHEDULING_CALENDAR_LABELS, SchedulingCalendar, type SchedulingCalendarEntry, type SchedulingCalendarLabels, type SchedulingCalendarOverlay, type SchedulingCalendarProps, type SchedulingCalendarView, type SchedulingDatePart, type SchedulingEntryState, type SchedulingOverlayKind };
|