@tensor_1/react 2.47.0 → 2.49.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 +540 -2
- 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/Button.d.ts +12 -5
- package/dist/components/Button.js +7 -3
- 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/Chip.d.ts +7 -3
- package/dist/components/Chip.js +2 -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/DashboardCanvas.js +14 -0
- package/dist/components/DataWorkspace.d.ts +167 -0
- package/dist/components/DataWorkspace.js +380 -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/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.js +16 -0
- package/dist/components/DisplayPreferences.d.ts +83 -0
- package/dist/components/DisplayPreferences.js +185 -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/FocusOnMount.d.ts +282 -0
- package/dist/components/FocusOnMount.js +12 -0
- 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/Kanban.d.ts +14 -3
- package/dist/components/Kanban.js +9 -4
- 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 +2 -1
- package/dist/components/Modal.js +13 -4
- package/dist/components/MotionList.d.ts +303 -0
- package/dist/components/MotionList.js +175 -0
- 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 +11 -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 +22 -6
- package/dist/components/RelationshipGraphLayered.d.ts +1 -1
- package/dist/components/RelationshipGraphLayered.js +268 -118
- package/dist/components/RelationshipGraphList.js +33 -6
- package/dist/components/RuleBuilder.js +39 -7
- 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/SearchSuggest.d.ts +23 -3
- package/dist/components/SearchSuggest.js +56 -18
- package/dist/components/Segmented.d.ts +39 -4
- package/dist/components/Segmented.js +39 -5
- package/dist/components/Select.d.ts +3 -2
- package/dist/components/Select.js +7 -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 +147 -0
- package/dist/components/Stat.d.ts +28 -3
- package/dist/components/Stat.js +13 -3
- package/dist/components/States.d.ts +33 -1
- package/dist/components/States.js +43 -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 +9 -2
- package/dist/components/Table.js +4 -2
- 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 +10 -2
- 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.d.ts +1 -1
- package/dist/components/Toast.js +109 -4
- package/dist/components/Tooltip.d.ts +8 -4
- package/dist/components/Tooltip.js +86 -28
- 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 +143 -49
- package/dist/filter-model.d.ts +59 -3
- package/dist/filter-model.js +67 -2
- package/dist/hooks/useAnchoredPosition.js +14 -2
- package/dist/hooks/useChangeHighlight.d.ts +33 -0
- package/dist/hooks/useChangeHighlight.js +44 -0
- package/dist/hooks/useDeferredReorder.d.ts +32 -0
- package/dist/hooks/useDeferredReorder.js +47 -0
- package/dist/hooks/useDialog.d.ts +5 -0
- package/dist/hooks/useDialog.js +17 -3
- package/dist/hooks/useKeyboardGrab.d.ts +58 -0
- package/dist/hooks/useKeyboardGrab.js +118 -0
- 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/useSlidingIndicator.d.ts +18 -0
- package/dist/hooks/useSlidingIndicator.js +80 -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 +19687 -13423
- package/dist/index.d.cts +3419 -651
- package/dist/index.d.ts +48 -18
- package/dist/index.js +153 -9
- package/dist/labels-context.d.ts +93 -3
- package/dist/motion.d.ts +11 -0
- package/dist/motion.js +35 -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 +92 -3
- package/dist/relationship-graph-model.js +134 -15
- package/dist/reorder.d.ts +30 -0
- package/dist/reorder.js +74 -0
- package/dist/rich-text.cjs +46 -4
- 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 +22 -3
- package/dist/toast-store.js +26 -9
- package/package.json +2 -1
|
@@ -3,8 +3,47 @@ import { HTMLAttributes, ReactNode } from 'react';
|
|
|
3
3
|
|
|
4
4
|
type ShellNav = 'full' | 'rail' | 'hidden';
|
|
5
5
|
interface AppShellProps extends HTMLAttributes<HTMLDivElement> {
|
|
6
|
-
/**
|
|
6
|
+
/**
|
|
7
|
+
* `frame` (default) is the bordered, rounded box the docs show a shell in. `viewport` is an
|
|
8
|
+
* application's shell (HAR-1815): no frame, the full dynamic viewport height (`100dvh`), no fixed
|
|
9
|
+
* top-bar row (the sidebar and main both run the full height; a `topbar`, if given, sits at the
|
|
10
|
+
* top of the main column), main padding that widens on wide screens, and the phone nav tier by
|
|
11
|
+
* default (`phoneNav`), so below 40rem it is one column with the sidebar in a drawer.
|
|
12
|
+
*/
|
|
13
|
+
layout?: 'frame' | 'viewport';
|
|
14
|
+
/**
|
|
15
|
+
* Sidebar width tier: 'full' (default) → 'rail' (icon only) → 'hidden' (0). Controlled when
|
|
16
|
+
* set; leave it out to let the shell keep the tier (`defaultNav`, `navHotkey`,
|
|
17
|
+
* `navTierStorageKey`).
|
|
18
|
+
*/
|
|
7
19
|
nav?: ShellNav;
|
|
20
|
+
/** The starting tier when `nav` is not controlled. Default `full`. */
|
|
21
|
+
defaultNav?: ShellNav;
|
|
22
|
+
/**
|
|
23
|
+
* Called with the tier the hotkey asks for, and once on mount with a remembered tier that
|
|
24
|
+
* differs from `defaultNav` (HAR-1371), so a `Sidebar nav` you render can follow. With `nav`
|
|
25
|
+
* controlled, set it from here.
|
|
26
|
+
*/
|
|
27
|
+
onNavChange?: (nav: ShellNav) => void;
|
|
28
|
+
/**
|
|
29
|
+
* A key that switches the sidebar between `full` and `rail` (HAR-1371; MOTUS StaffShell), e.g.
|
|
30
|
+
* `"["`. It is ignored while typing in a field or with Ctrl/⌘/Alt held, and from `hidden` it
|
|
31
|
+
* goes to `full`. The switch is instant.
|
|
32
|
+
*/
|
|
33
|
+
navHotkey?: string;
|
|
34
|
+
/**
|
|
35
|
+
* Remember the tier in `localStorage` under this key (HAR-1371), so a reload keeps the rail.
|
|
36
|
+
* Applied before the first paint on the client; the server renders `defaultNav`.
|
|
37
|
+
*/
|
|
38
|
+
navTierStorageKey?: string;
|
|
39
|
+
/**
|
|
40
|
+
* The phone nav tier (HAR-1371): at 40rem and under, the sidebar column is hidden and a menu
|
|
41
|
+
* button (at the start of the topbar, or at the top of main) opens the sidebar in a start-side
|
|
42
|
+
* `Drawer`, shown full whatever the tier. Following a link in it closes the drawer. Default
|
|
43
|
+
* `drawer` with `layout="viewport"`, `none` with the frame; `none` keeps the sidebar column at
|
|
44
|
+
* every width.
|
|
45
|
+
*/
|
|
46
|
+
phoneNav?: 'drawer' | 'none';
|
|
8
47
|
/** Back-compat alias for nav="rail". Ignored when `nav` is set. */
|
|
9
48
|
collapsed?: boolean;
|
|
10
49
|
/**
|
|
@@ -26,7 +65,11 @@ interface AppShellProps extends HTMLAttributes<HTMLDivElement> {
|
|
|
26
65
|
skipToContentLabel?: ReactNode;
|
|
27
66
|
/** TENSOR RX-125 (UIX-04): translatable; English default. */
|
|
28
67
|
exitFocusLabel?: ReactNode;
|
|
68
|
+
/** The phone menu button's name. Default: `UixLabelsProvider` `appShell.openNav`, then "Open navigation". */
|
|
69
|
+
openNavLabel?: string;
|
|
70
|
+
/** The navigation drawer's title. Default: `appShell.navigation`, then "Navigation". */
|
|
71
|
+
navigationLabel?: ReactNode;
|
|
29
72
|
}
|
|
30
|
-
declare function AppShell({ nav, collapsed, focus, onExitFocus, mainBleed, mainId, sidebar, topbar, children, skipToContentLabel: skipToContentLabelProp, exitFocusLabel: exitFocusLabelProp, className, ...props }: AppShellProps): react.JSX.Element;
|
|
73
|
+
declare function AppShell({ layout, nav, defaultNav, onNavChange, navHotkey, navTierStorageKey, phoneNav: phoneNavProp, collapsed, focus, onExitFocus, mainBleed, mainId, sidebar, topbar, children, skipToContentLabel: skipToContentLabelProp, exitFocusLabel: exitFocusLabelProp, openNavLabel: openNavLabelProp, navigationLabel: navigationLabelProp, className, ...props }: AppShellProps): react.JSX.Element;
|
|
31
74
|
|
|
32
75
|
export { AppShell, type AppShellProps, type ShellNav };
|
|
@@ -1,10 +1,44 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
3
|
-
import { useEffect } from "react";
|
|
3
|
+
import { useCallback, useEffect, useLayoutEffect, useRef, useState } from "react";
|
|
4
4
|
import { cx } from "../cx.js";
|
|
5
5
|
import { useUixLabels } from "../labels-context.js";
|
|
6
|
+
import { MenuIcon } from "../icons/components.js";
|
|
7
|
+
import { Drawer } from "./Drawer.js";
|
|
8
|
+
import { ShellDrawerContext } from "../shell-context.js";
|
|
9
|
+
const NAV_TIERS = ["full", "rail", "hidden"];
|
|
10
|
+
const PHONE = "(max-width: 40rem)";
|
|
11
|
+
const useIsomorphicLayoutEffect = typeof document !== "undefined" ? useLayoutEffect : useEffect;
|
|
12
|
+
const readTier = (key) => {
|
|
13
|
+
try {
|
|
14
|
+
const stored = window.localStorage.getItem(key);
|
|
15
|
+
return NAV_TIERS.includes(stored) ? stored : void 0;
|
|
16
|
+
} catch {
|
|
17
|
+
return void 0;
|
|
18
|
+
}
|
|
19
|
+
};
|
|
20
|
+
const isNavHotkey = (event, key) => {
|
|
21
|
+
if (event.key !== key || event.defaultPrevented || event.repeat || event.isComposing) return false;
|
|
22
|
+
if (event.metaKey || event.ctrlKey && !event.altKey) return false;
|
|
23
|
+
const target = (typeof event.composedPath === "function" ? event.composedPath()[0] : null) ?? event.target;
|
|
24
|
+
if (target?.isContentEditable || target?.closest?.('input, textarea, select, [contenteditable=""], [contenteditable="true"], [role="textbox"], [role="combobox"]')) return false;
|
|
25
|
+
const modalOpen = Array.from(document.querySelectorAll("dialog[open]")).some((dialog) => {
|
|
26
|
+
try {
|
|
27
|
+
return dialog.matches(":modal");
|
|
28
|
+
} catch {
|
|
29
|
+
return true;
|
|
30
|
+
}
|
|
31
|
+
});
|
|
32
|
+
return !modalOpen;
|
|
33
|
+
};
|
|
6
34
|
function AppShell({
|
|
35
|
+
layout = "frame",
|
|
7
36
|
nav,
|
|
37
|
+
defaultNav = "full",
|
|
38
|
+
onNavChange,
|
|
39
|
+
navHotkey,
|
|
40
|
+
navTierStorageKey,
|
|
41
|
+
phoneNav: phoneNavProp,
|
|
8
42
|
collapsed,
|
|
9
43
|
focus,
|
|
10
44
|
onExitFocus,
|
|
@@ -15,13 +49,48 @@ function AppShell({
|
|
|
15
49
|
children,
|
|
16
50
|
skipToContentLabel: skipToContentLabelProp,
|
|
17
51
|
exitFocusLabel: exitFocusLabelProp,
|
|
52
|
+
openNavLabel: openNavLabelProp,
|
|
53
|
+
navigationLabel: navigationLabelProp,
|
|
18
54
|
className,
|
|
19
55
|
...props
|
|
20
56
|
}) {
|
|
21
57
|
const uixLabels = useUixLabels();
|
|
58
|
+
const phoneNav = (phoneNavProp ?? (layout === "viewport" ? "drawer" : "none")) === "drawer";
|
|
22
59
|
const skipToContentLabel = skipToContentLabelProp ?? uixLabels.appShell?.skipToContent ?? "Skip to content";
|
|
23
60
|
const exitFocusLabel = exitFocusLabelProp ?? uixLabels.appShell?.exitFocus ?? "Esc \xB7 Exit focus";
|
|
24
|
-
const
|
|
61
|
+
const openNavLabel = openNavLabelProp ?? uixLabels.appShell?.openNav ?? "Open navigation";
|
|
62
|
+
const navigationLabel = navigationLabelProp ?? uixLabels.appShell?.navigation ?? "Navigation";
|
|
63
|
+
const [ownNav, setOwnNav] = useState(defaultNav);
|
|
64
|
+
const controlled = nav != null || collapsed != null;
|
|
65
|
+
const navState = nav ?? (collapsed != null ? collapsed ? "rail" : "full" : ownNav);
|
|
66
|
+
useIsomorphicLayoutEffect(() => {
|
|
67
|
+
if (!navTierStorageKey) return;
|
|
68
|
+
const stored = readTier(navTierStorageKey);
|
|
69
|
+
if (stored && stored !== (controlled ? navState : defaultNav)) {
|
|
70
|
+
if (!controlled) setOwnNav(stored);
|
|
71
|
+
onNavChange?.(stored);
|
|
72
|
+
}
|
|
73
|
+
}, [navTierStorageKey]);
|
|
74
|
+
const changeNav = useCallback((next) => {
|
|
75
|
+
if (!controlled) setOwnNav(next);
|
|
76
|
+
if (navTierStorageKey) {
|
|
77
|
+
try {
|
|
78
|
+
window.localStorage.setItem(navTierStorageKey, next);
|
|
79
|
+
} catch {
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
onNavChange?.(next);
|
|
83
|
+
}, [controlled, navTierStorageKey, onNavChange]);
|
|
84
|
+
useEffect(() => {
|
|
85
|
+
if (!navHotkey) return;
|
|
86
|
+
const onKey = (event) => {
|
|
87
|
+
if (!isNavHotkey(event, navHotkey)) return;
|
|
88
|
+
event.preventDefault();
|
|
89
|
+
changeNav(navState === "full" ? "rail" : "full");
|
|
90
|
+
};
|
|
91
|
+
document.addEventListener("keydown", onKey);
|
|
92
|
+
return () => document.removeEventListener("keydown", onKey);
|
|
93
|
+
}, [navHotkey, navState, changeNav]);
|
|
25
94
|
useEffect(() => {
|
|
26
95
|
if (!focus || !onExitFocus) return;
|
|
27
96
|
const onKey = (e) => {
|
|
@@ -30,19 +99,61 @@ function AppShell({
|
|
|
30
99
|
document.addEventListener("keydown", onKey);
|
|
31
100
|
return () => document.removeEventListener("keydown", onKey);
|
|
32
101
|
}, [focus, onExitFocus]);
|
|
102
|
+
const [drawerOpen, setDrawerOpen] = useState(false);
|
|
103
|
+
useEffect(() => {
|
|
104
|
+
if (!phoneNav || !drawerOpen || typeof window.matchMedia !== "function") return;
|
|
105
|
+
const query = window.matchMedia(PHONE);
|
|
106
|
+
const onChange = () => {
|
|
107
|
+
if (!query.matches) setDrawerOpen(false);
|
|
108
|
+
};
|
|
109
|
+
query.addEventListener("change", onChange);
|
|
110
|
+
return () => query.removeEventListener("change", onChange);
|
|
111
|
+
}, [phoneNav, drawerOpen]);
|
|
112
|
+
const phoneButton = phoneNav && sidebar ? /* @__PURE__ */ jsx("button", { type: "button", className: "uix-shell__nav-button uix-btn uix-btn--ghost uix-btn--icon", "aria-label": openNavLabel, "aria-haspopup": "dialog", "aria-expanded": drawerOpen, onClick: () => setDrawerOpen(true), children: /* @__PURE__ */ jsx(MenuIcon, {}) }) : null;
|
|
113
|
+
const closeOnLink = (event) => {
|
|
114
|
+
if (event.target.closest("a[href]")) setDrawerOpen(false);
|
|
115
|
+
};
|
|
116
|
+
const [drawerMounted, setDrawerMounted] = useState(false);
|
|
117
|
+
const drawerBodyRef = useRef(null);
|
|
118
|
+
useEffect(() => {
|
|
119
|
+
if (drawerOpen) {
|
|
120
|
+
setDrawerMounted(true);
|
|
121
|
+
return void 0;
|
|
122
|
+
}
|
|
123
|
+
const dialog = drawerBodyRef.current?.closest("dialog");
|
|
124
|
+
if (!dialog) {
|
|
125
|
+
setDrawerMounted(false);
|
|
126
|
+
return void 0;
|
|
127
|
+
}
|
|
128
|
+
const style = getComputedStyle(dialog);
|
|
129
|
+
const ms = (list) => list.split(",").map((v) => (v.trim().endsWith("ms") ? parseFloat(v) : parseFloat(v) * 1e3) || 0);
|
|
130
|
+
const durations = ms(style.transitionDuration);
|
|
131
|
+
const delays = ms(style.transitionDelay);
|
|
132
|
+
const exit = Math.max(0, ...durations.map((d, i) => d + (delays[i] ?? 0)));
|
|
133
|
+
const timer = window.setTimeout(() => setDrawerMounted(false), exit);
|
|
134
|
+
return () => window.clearTimeout(timer);
|
|
135
|
+
}, [drawerOpen]);
|
|
33
136
|
return /* @__PURE__ */ jsxs(
|
|
34
137
|
"div",
|
|
35
138
|
{
|
|
36
|
-
className: cx("uix-shell", className),
|
|
139
|
+
className: cx("uix-shell", layout === "viewport" && "uix-shell--viewport", className),
|
|
37
140
|
"data-nav": navState !== "full" ? navState : void 0,
|
|
38
141
|
"data-collapsed": collapsed && nav == null ? "" : void 0,
|
|
39
142
|
"data-focus": focus || void 0,
|
|
143
|
+
"data-phone-nav": phoneNav ? "drawer" : void 0,
|
|
40
144
|
...props,
|
|
41
145
|
children: [
|
|
42
146
|
/* @__PURE__ */ jsx("a", { className: "uix-skiplink", href: `#${mainId}`, children: skipToContentLabel }),
|
|
43
147
|
sidebar && /* @__PURE__ */ jsx("div", { className: "uix-shell__sidebar", children: sidebar }),
|
|
44
|
-
topbar && /* @__PURE__ */
|
|
45
|
-
|
|
148
|
+
topbar && /* @__PURE__ */ jsxs("div", { className: "uix-shell__topbar", children: [
|
|
149
|
+
phoneButton,
|
|
150
|
+
topbar
|
|
151
|
+
] }),
|
|
152
|
+
/* @__PURE__ */ jsxs("main", { id: mainId, tabIndex: -1, className: cx("uix-shell__main", mainBleed && "uix-shell__main--bleed"), children: [
|
|
153
|
+
!topbar && phoneButton && /* @__PURE__ */ jsx("div", { className: "uix-shell__phone-bar", children: phoneButton }),
|
|
154
|
+
children
|
|
155
|
+
] }),
|
|
156
|
+
phoneButton && /* @__PURE__ */ jsx(Drawer, { open: drawerOpen, onClose: () => setDrawerOpen(false), side: "start", title: navigationLabel, className: "uix-shell__nav-drawer", children: (drawerOpen || drawerMounted) && /* @__PURE__ */ jsx(ShellDrawerContext.Provider, { value: true, children: /* @__PURE__ */ jsx("div", { ref: drawerBodyRef, className: "uix-shell__nav-drawer-body", onClick: closeOnLink, children: sidebar }) }) }),
|
|
46
157
|
focus && onExitFocus && /* @__PURE__ */ jsx("div", { className: "uix-shell__focus-exit", children: /* @__PURE__ */ jsx("button", { type: "button", className: "uix-btn uix-btn--secondary uix-btn--sm", onClick: onExitFocus, children: exitFocusLabel }) })
|
|
47
158
|
]
|
|
48
159
|
}
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import * as react from 'react';
|
|
2
|
+
import { HTMLAttributes, ReactNode } from 'react';
|
|
3
|
+
import { FilterChoice, FilterValue } from '../filter-model.js';
|
|
4
|
+
import { StatusPillProps } from './StatusPill.js';
|
|
5
|
+
|
|
6
|
+
/** One entry of an access or change history. The consumer supplies it free of personal health data. */
|
|
7
|
+
interface AuditLogEvent {
|
|
8
|
+
id: string;
|
|
9
|
+
/** Who did it: a name, or a node (an avatar and a name). */
|
|
10
|
+
actor: ReactNode;
|
|
11
|
+
/** What they did: a key of `actions`. */
|
|
12
|
+
action: string;
|
|
13
|
+
/** What it was done to: "Patient record 1042", "Invoice R-2026-118". */
|
|
14
|
+
target?: ReactNode;
|
|
15
|
+
/**
|
|
16
|
+
* When: a `Date`, epoch ms or an ISO string. An ISO date-time without an offset
|
|
17
|
+
* (`2026-10-22T09:30:00`) is read as UTC, as audit stores keep it, not as the viewer's local time.
|
|
18
|
+
*/
|
|
19
|
+
at: Date | number | string;
|
|
20
|
+
/** More about it, behind a Details disclosure: fields changed, the reason given, the IP. */
|
|
21
|
+
detail?: ReactNode;
|
|
22
|
+
}
|
|
23
|
+
/** How an action reads and looks; the consumer labels its own action enum. */
|
|
24
|
+
interface AuditLogAction {
|
|
25
|
+
label: string;
|
|
26
|
+
tone?: StatusPillProps['tone'];
|
|
27
|
+
}
|
|
28
|
+
/** The filters the person chose: `when` (a date range), `actor` and `action` (sets of ids). */
|
|
29
|
+
interface AuditLogFilters {
|
|
30
|
+
when?: Extract<FilterValue, {
|
|
31
|
+
kind: 'date-range';
|
|
32
|
+
}>;
|
|
33
|
+
actor?: Extract<FilterValue, {
|
|
34
|
+
kind: 'enum';
|
|
35
|
+
}>;
|
|
36
|
+
action?: Extract<FilterValue, {
|
|
37
|
+
kind: 'enum';
|
|
38
|
+
}>;
|
|
39
|
+
}
|
|
40
|
+
interface AuditLogLabels {
|
|
41
|
+
/** Names the list. */
|
|
42
|
+
list: string;
|
|
43
|
+
/** Names the filter group. */
|
|
44
|
+
filters: string;
|
|
45
|
+
when: string;
|
|
46
|
+
actor: string;
|
|
47
|
+
action: string;
|
|
48
|
+
clearFilters: string;
|
|
49
|
+
details: string;
|
|
50
|
+
loadMore: string;
|
|
51
|
+
/** Polite status while more entries load. */
|
|
52
|
+
loadingMore: string;
|
|
53
|
+
/** Polite status after more entries arrived. `{count}`. */
|
|
54
|
+
loaded: string;
|
|
55
|
+
/** Shown (as an alert) when `loadMoreError` is set; the Show older entries button retries. */
|
|
56
|
+
loadMoreFailed: string;
|
|
57
|
+
loading: string;
|
|
58
|
+
empty: string;
|
|
59
|
+
emptyDetail: string;
|
|
60
|
+
filteredEmpty: string;
|
|
61
|
+
filteredEmptyDetail: string;
|
|
62
|
+
error: string;
|
|
63
|
+
retry: string;
|
|
64
|
+
forbidden: string;
|
|
65
|
+
forbiddenDetail: string;
|
|
66
|
+
}
|
|
67
|
+
declare const DEFAULT_AUDIT_LOG_LABELS: AuditLogLabels;
|
|
68
|
+
interface AuditLogProps extends Omit<HTMLAttributes<HTMLElement>, 'children'> {
|
|
69
|
+
events: readonly AuditLogEvent[];
|
|
70
|
+
/** Every action the log can show, by key: its label and the tone of its pill. */
|
|
71
|
+
actions: Readonly<Record<string, AuditLogAction>>;
|
|
72
|
+
/**
|
|
73
|
+
* `ready` (default), `loading` (the first load: skeleton rows), `error`, or `forbidden`.
|
|
74
|
+
* A reload with rows on show is `refreshing`, not `loading`.
|
|
75
|
+
*/
|
|
76
|
+
status?: 'ready' | 'loading' | 'error' | 'forbidden';
|
|
77
|
+
/**
|
|
78
|
+
* A reload is running (the filters changed): the rows on show stay in a `BusyRegion`, busy at
|
|
79
|
+
* once, dimmed under a running bar after 300 ms. Skeleton rows are for `status="loading"` only.
|
|
80
|
+
*/
|
|
81
|
+
refreshing?: boolean;
|
|
82
|
+
onRetry?: () => void;
|
|
83
|
+
/** The actors offered in the Who filter. Without them there is no Who filter. */
|
|
84
|
+
actors?: readonly FilterChoice[];
|
|
85
|
+
/**
|
|
86
|
+
* Names for actor ids in `filters.actor` that are not among `actors` (restored from a URL or a
|
|
87
|
+
* saved view): the Who chip shows the names, not the ids.
|
|
88
|
+
*/
|
|
89
|
+
resolveActorLabels?: (ids: readonly string[]) => Promise<readonly FilterChoice[]>;
|
|
90
|
+
/** The filters in effect (controlled). With `onFiltersChange`, the filter bar shows. */
|
|
91
|
+
filters?: AuditLogFilters;
|
|
92
|
+
onFiltersChange?: (filters: AuditLogFilters) => void;
|
|
93
|
+
/** More, older entries exist: a Show older entries button calls `onLoadMore` (keyset paging). */
|
|
94
|
+
hasMore?: boolean;
|
|
95
|
+
onLoadMore?: () => void;
|
|
96
|
+
loadingMore?: boolean;
|
|
97
|
+
/** Loading older entries failed: the button stays to try again, with `labels.loadMoreFailed` beside it. */
|
|
98
|
+
loadMoreError?: boolean;
|
|
99
|
+
/** Locale of times and dates; a list falls back in order. */
|
|
100
|
+
locale?: string | readonly string[];
|
|
101
|
+
/**
|
|
102
|
+
* The exact time shown beside the relative one. Default: `Intl` medium date and short time in
|
|
103
|
+
* `locale`. Pass it where the browser lacks the locale's data (Chromium has none for `bs` and
|
|
104
|
+
* prints "2026 M10 22"), or for a fixed product format.
|
|
105
|
+
*/
|
|
106
|
+
formatExact?: (date: Date) => string;
|
|
107
|
+
/**
|
|
108
|
+
* Time zone of the times shown, e.g. the practice's `Europe/Sarajevo`. Default: the viewer's.
|
|
109
|
+
* A zone the browser does not know falls back to the viewer's (with a warning in development).
|
|
110
|
+
*/
|
|
111
|
+
timeZone?: string;
|
|
112
|
+
/** Formats `YYYY-MM-DD` in a filter chip. Default: as given. */
|
|
113
|
+
formatDate?: (date: string) => string;
|
|
114
|
+
/** The current time, for stable relative times in tests and server renders. */
|
|
115
|
+
now?: Date | number;
|
|
116
|
+
labels?: Partial<AuditLogLabels>;
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Who opened or changed what, and when (HAR-987; MEDx S50 "Historija pristupa", POSx PRT-01 /
|
|
120
|
+
* INT-01): an ordered list of entries (actor, a labelled action, the target, a relative time
|
|
121
|
+
* with the exact one beside it, and an optional Details disclosure), filters by period, person
|
|
122
|
+
* and action (chips that open `FilterEditor`s), keyset "Show older entries", and the first-load,
|
|
123
|
+
* empty, filtered-empty, error and forbidden states. Presentational: the consumer fetches, keeps
|
|
124
|
+
* the audit semantics, and supplies entries free of health data.
|
|
125
|
+
*/
|
|
126
|
+
declare function AuditLog({ events, actions, status, refreshing, onRetry, actors, resolveActorLabels, filters, onFiltersChange, hasMore, onLoadMore, loadingMore, loadMoreError, locale, formatExact, timeZone: zoneProp, formatDate, now, labels: labelOverrides, className, ...props }: AuditLogProps): react.JSX.Element;
|
|
127
|
+
|
|
128
|
+
export { AuditLog, type AuditLogAction, type AuditLogEvent, type AuditLogFilters, type AuditLogLabels, type AuditLogProps, DEFAULT_AUDIT_LOG_LABELS };
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { Fragment, 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 { useResolvedFilterLabels } from "../hooks/useResolvedFilterLabels.js";
|
|
8
|
+
import { ChevronDownIcon } from "../icons/components.js";
|
|
9
|
+
import { isFilterEmpty, summarizeFilter } from "../filter-model.js";
|
|
10
|
+
import { BusyRegion } from "./BusyRegion.js";
|
|
11
|
+
import { Button } from "./Button.js";
|
|
12
|
+
import { Chip } from "./Chip.js";
|
|
13
|
+
import { FilterEditor } from "./FilterEditor.js";
|
|
14
|
+
import { Popover } from "./Popover.js";
|
|
15
|
+
import { RelativeTime } from "./RelativeTime.js";
|
|
16
|
+
import { StatusPill } from "./StatusPill.js";
|
|
17
|
+
import { EmptyState, ErrorState, FilteredEmptyState, ForbiddenState, LoadingState } from "./States.js";
|
|
18
|
+
const DEFAULT_AUDIT_LOG_LABELS = {
|
|
19
|
+
list: "Access history",
|
|
20
|
+
filters: "Filter the history",
|
|
21
|
+
when: "When",
|
|
22
|
+
actor: "Who",
|
|
23
|
+
action: "What",
|
|
24
|
+
clearFilters: "Clear filters",
|
|
25
|
+
details: "Details",
|
|
26
|
+
loadMore: "Show older entries",
|
|
27
|
+
loadingMore: "Loading older entries\u2026",
|
|
28
|
+
loaded: "{count} older entries shown",
|
|
29
|
+
loadMoreFailed: "The older entries could not be loaded.",
|
|
30
|
+
loading: "Loading the history",
|
|
31
|
+
empty: "No access recorded yet",
|
|
32
|
+
emptyDetail: "Every time someone opens or changes this record, it shows here.",
|
|
33
|
+
filteredEmpty: "Nothing matches these filters",
|
|
34
|
+
filteredEmptyDetail: "Clear a filter or choose a longer period.",
|
|
35
|
+
error: "The history could not be loaded",
|
|
36
|
+
retry: "Try again",
|
|
37
|
+
forbidden: "You cannot see this history",
|
|
38
|
+
forbiddenDetail: "Ask an administrator of this practice for access."
|
|
39
|
+
};
|
|
40
|
+
const FIELD_IDS = ["when", "actor", "action"];
|
|
41
|
+
function AuditLog({
|
|
42
|
+
events,
|
|
43
|
+
actions,
|
|
44
|
+
status = "ready",
|
|
45
|
+
refreshing = false,
|
|
46
|
+
onRetry,
|
|
47
|
+
actors,
|
|
48
|
+
resolveActorLabels,
|
|
49
|
+
filters = {},
|
|
50
|
+
onFiltersChange,
|
|
51
|
+
hasMore = false,
|
|
52
|
+
onLoadMore,
|
|
53
|
+
loadingMore = false,
|
|
54
|
+
loadMoreError = false,
|
|
55
|
+
locale,
|
|
56
|
+
formatExact,
|
|
57
|
+
timeZone: zoneProp,
|
|
58
|
+
formatDate,
|
|
59
|
+
now,
|
|
60
|
+
labels: labelOverrides,
|
|
61
|
+
className,
|
|
62
|
+
...props
|
|
63
|
+
}) {
|
|
64
|
+
const labels = { ...DEFAULT_AUDIT_LOG_LABELS, ...useUixLabels().auditLog, ...labelOverrides };
|
|
65
|
+
const id = useId();
|
|
66
|
+
const [announced, setAnnounced] = useState("");
|
|
67
|
+
const countBefore = useRef(null);
|
|
68
|
+
useEffect(() => {
|
|
69
|
+
const before = countBefore.current;
|
|
70
|
+
if (before == null || loadingMore) return;
|
|
71
|
+
if (loadMoreError) {
|
|
72
|
+
countBefore.current = null;
|
|
73
|
+
setAnnounced("");
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
if (events.length === before) return;
|
|
77
|
+
countBefore.current = null;
|
|
78
|
+
if (events.length > before) setAnnounced(fillLabel(labels.loaded, { count: events.length - before }));
|
|
79
|
+
}, [events.length, loadingMore, loadMoreError, labels.loaded]);
|
|
80
|
+
const timeZone = knownTimeZone(zoneProp);
|
|
81
|
+
const intlExact = new Intl.DateTimeFormat(locale, { dateStyle: "medium", timeStyle: "short", timeZone });
|
|
82
|
+
const exact = formatExact ?? ((date) => intlExact.format(date));
|
|
83
|
+
const fields = {
|
|
84
|
+
when: { id: "when", label: labels.when, kind: "date-range" },
|
|
85
|
+
actor: actors?.length ? { id: "actor", label: labels.actor, kind: "enum", options: actors, resolveLabels: resolveActorLabels } : null,
|
|
86
|
+
action: { id: "action", label: labels.action, kind: "enum", options: Object.entries(actions).map(([value, a]) => ({ value, label: a.label })) }
|
|
87
|
+
};
|
|
88
|
+
const active = FIELD_IDS.filter((f) => !isFilterEmpty(filters[f]));
|
|
89
|
+
const setFilter = (field, value) => {
|
|
90
|
+
const next = { ...filters };
|
|
91
|
+
if (value === void 0 || isFilterEmpty(value)) delete next[field];
|
|
92
|
+
else next[field] = value;
|
|
93
|
+
onFiltersChange?.(next);
|
|
94
|
+
};
|
|
95
|
+
const filterBar = onFiltersChange && /* @__PURE__ */ jsxs("div", { className: "uix-audit-log__filters", role: "group", "aria-label": labels.filters, children: [
|
|
96
|
+
FIELD_IDS.map((f) => fields[f] && /* @__PURE__ */ jsx(AuditFilter, { id: `${id}-${f}`, field: fields[f], value: filters[f], formatDate, onApply: (v) => setFilter(f, v) }, f)),
|
|
97
|
+
active.length > 0 && /* @__PURE__ */ jsx(Button, { size: "sm", variant: "link", onClick: () => onFiltersChange({}), children: labels.clearFilters })
|
|
98
|
+
] });
|
|
99
|
+
let body;
|
|
100
|
+
if (status === "loading") body = /* @__PURE__ */ jsx(LoadingState, { rows: 5, label: labels.loading });
|
|
101
|
+
else if (status === "forbidden") body = /* @__PURE__ */ jsx(ForbiddenState, { title: labels.forbidden, detail: labels.forbiddenDetail });
|
|
102
|
+
else if (status === "error") body = /* @__PURE__ */ jsx(ErrorState, { title: labels.error, action: onRetry && /* @__PURE__ */ jsx(Button, { size: "sm", onClick: onRetry, children: labels.retry }) });
|
|
103
|
+
else if (events.length === 0 && active.length > 0) body = /* @__PURE__ */ jsx(FilteredEmptyState, { title: labels.filteredEmpty, action: /* @__PURE__ */ jsx(Button, { size: "sm", onClick: () => onFiltersChange?.({}), children: labels.clearFilters }), children: labels.filteredEmptyDetail });
|
|
104
|
+
else if (events.length === 0) body = /* @__PURE__ */ jsx(EmptyState, { title: labels.empty, children: labels.emptyDetail });
|
|
105
|
+
else body = /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
106
|
+
/* @__PURE__ */ jsx(BusyRegion, { busy: refreshing, className: "uix-audit-log__region", children: /* @__PURE__ */ jsx("ol", { className: "uix-audit uix-audit-log__list", children: events.map((event) => {
|
|
107
|
+
const action = actions[event.action];
|
|
108
|
+
const date = toDate(event.at);
|
|
109
|
+
return /* @__PURE__ */ jsxs("li", { className: "uix-audit__row uix-audit-log__row", children: [
|
|
110
|
+
/* @__PURE__ */ jsx("span", { className: "uix-audit__actor", children: event.actor }),
|
|
111
|
+
/* @__PURE__ */ jsxs("span", { className: "uix-audit__detail uix-audit-log__what", children: [
|
|
112
|
+
/* @__PURE__ */ jsx(StatusPill, { tone: action?.tone ?? "neutral", children: action?.label ?? event.action }),
|
|
113
|
+
event.target != null && /* @__PURE__ */ jsx("span", { className: "uix-audit-log__target", children: event.target })
|
|
114
|
+
] }),
|
|
115
|
+
/* @__PURE__ */ jsxs("span", { className: "uix-audit__time uix-audit-log__when", children: [
|
|
116
|
+
/* @__PURE__ */ jsx(RelativeTime, { value: date, now, locale, timeZone }),
|
|
117
|
+
!Number.isNaN(date.getTime()) && /* @__PURE__ */ jsx("span", { className: "uix-audit-log__exact", children: exact(date) })
|
|
118
|
+
] }),
|
|
119
|
+
event.detail != null && /* @__PURE__ */ jsxs("details", { className: "uix-audit-log__more", children: [
|
|
120
|
+
/* @__PURE__ */ jsxs("summary", { className: "uix-audit-log__more-summary", children: [
|
|
121
|
+
/* @__PURE__ */ jsx("span", { children: labels.details }),
|
|
122
|
+
/* @__PURE__ */ jsx("span", { className: "uix-collapsible__chevron", "aria-hidden": "true", children: /* @__PURE__ */ jsx(ChevronDownIcon, { size: "sm" }) })
|
|
123
|
+
] }),
|
|
124
|
+
/* @__PURE__ */ jsx("div", { className: "uix-audit-log__detail", children: event.detail })
|
|
125
|
+
] })
|
|
126
|
+
] }, event.id);
|
|
127
|
+
}) }) }),
|
|
128
|
+
hasMore && onLoadMore && /* @__PURE__ */ jsxs("div", { className: "uix-audit-log__more-row", children: [
|
|
129
|
+
loadMoreError && !loadingMore && /* @__PURE__ */ jsx("span", { className: "uix-audit-log__more-failed", role: "alert", children: labels.loadMoreFailed }),
|
|
130
|
+
/* @__PURE__ */ jsx(Button, { size: "sm", loading: loadingMore, onClick: () => {
|
|
131
|
+
if (loadingMore) return;
|
|
132
|
+
countBefore.current = events.length;
|
|
133
|
+
setAnnounced(labels.loadingMore);
|
|
134
|
+
onLoadMore();
|
|
135
|
+
}, children: loadMoreError && !loadingMore ? labels.retry : labels.loadMore })
|
|
136
|
+
] })
|
|
137
|
+
] });
|
|
138
|
+
return /* @__PURE__ */ jsxs("section", { className: cx("uix-audit-log", className), "aria-label": labels.list, ...props, children: [
|
|
139
|
+
filterBar,
|
|
140
|
+
body,
|
|
141
|
+
/* @__PURE__ */ jsx("span", { className: "uix-visually-hidden", role: "status", children: announced })
|
|
142
|
+
] });
|
|
143
|
+
}
|
|
144
|
+
const LOCAL_ISO = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}(:\d{2}(\.\d+)?)?$/;
|
|
145
|
+
const toDate = (at) => new Date(typeof at === "string" && LOCAL_ISO.test(at) ? `${at}Z` : at);
|
|
146
|
+
const warnedZones = /* @__PURE__ */ new Set();
|
|
147
|
+
function knownTimeZone(zone) {
|
|
148
|
+
if (!zone) return void 0;
|
|
149
|
+
try {
|
|
150
|
+
new Intl.DateTimeFormat(void 0, { timeZone: zone });
|
|
151
|
+
return zone;
|
|
152
|
+
} catch {
|
|
153
|
+
if (!warnedZones.has(zone) && typeof process !== "undefined" && process.env.NODE_ENV !== "production") {
|
|
154
|
+
warnedZones.add(zone);
|
|
155
|
+
console.warn(`uix AuditLog: timeZone "${zone}" is not known to this browser; showing the viewer's time zone.`);
|
|
156
|
+
}
|
|
157
|
+
return void 0;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
function AuditFilter({ id, field, value, formatDate, onApply }) {
|
|
161
|
+
const anchor = useRef(null);
|
|
162
|
+
const [draft, setDraft] = useState(value);
|
|
163
|
+
const set = !isFilterEmpty(value);
|
|
164
|
+
const popover = () => document.getElementById(`${id}-pop`);
|
|
165
|
+
const resolved = useResolvedFilterLabels(field, value);
|
|
166
|
+
return /* @__PURE__ */ jsxs("span", { ref: anchor, className: "uix-audit-log__filter", children: [
|
|
167
|
+
set ? /* @__PURE__ */ jsx(Chip, { "aria-haspopup": "dialog", onClick: () => {
|
|
168
|
+
setDraft(value);
|
|
169
|
+
popover()?.togglePopover();
|
|
170
|
+
}, onRemove: () => onApply(void 0), children: summarizeFilter(resolved.field, resolved.value, { formatDate }) }) : /* @__PURE__ */ jsx(Chip, { variant: "add", "aria-haspopup": "dialog", onClick: () => {
|
|
171
|
+
setDraft(value);
|
|
172
|
+
popover()?.togglePopover();
|
|
173
|
+
}, children: field.label }),
|
|
174
|
+
/* @__PURE__ */ jsx(Popover, { id: `${id}-pop`, anchor, role: "dialog", "aria-label": field.label, children: /* @__PURE__ */ jsx(
|
|
175
|
+
FilterEditor,
|
|
176
|
+
{
|
|
177
|
+
field,
|
|
178
|
+
value: draft,
|
|
179
|
+
onValueChange: setDraft,
|
|
180
|
+
onApply: (v) => {
|
|
181
|
+
onApply(v);
|
|
182
|
+
popover()?.hidePopover();
|
|
183
|
+
},
|
|
184
|
+
onClear: () => {
|
|
185
|
+
setDraft(void 0);
|
|
186
|
+
onApply(void 0);
|
|
187
|
+
popover()?.hidePopover();
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
) })
|
|
191
|
+
] });
|
|
192
|
+
}
|
|
193
|
+
export {
|
|
194
|
+
AuditLog,
|
|
195
|
+
DEFAULT_AUDIT_LOG_LABELS
|
|
196
|
+
};
|
|
@@ -4,6 +4,7 @@ import { useState } from "react";
|
|
|
4
4
|
import { cx } from "../cx.js";
|
|
5
5
|
import { normalizeHex } from "../color-model.js";
|
|
6
6
|
import { ColorPicker } from "./ColorPicker.js";
|
|
7
|
+
import { Select } from "./Select.js";
|
|
7
8
|
import { fillLabel } from "../fill-label.js";
|
|
8
9
|
const PROFILE_PROPERTIES = {
|
|
9
10
|
brand: "--uix-brand",
|
|
@@ -117,10 +118,10 @@ function BrandProfileEditor({
|
|
|
117
118
|
/* @__PURE__ */ jsxs("div", { className: "uix-brand-profiles__form", children: [
|
|
118
119
|
profiles.length > 0 && /* @__PURE__ */ jsxs("label", { className: "uix-field__label", children: [
|
|
119
120
|
labels.profile,
|
|
120
|
-
/* @__PURE__ */ jsx(
|
|
121
|
-
const selected = profiles.find((profile) => profile.id ===
|
|
121
|
+
/* @__PURE__ */ jsx(Select, { value: value.id, options: profiles.map((profile) => ({ value: profile.id, label: profile.name })), onValueChange: (next) => {
|
|
122
|
+
const selected = profiles.find((profile) => profile.id === next);
|
|
122
123
|
if (selected) onSelectProfile?.(selected);
|
|
123
|
-
}
|
|
124
|
+
} })
|
|
124
125
|
] }),
|
|
125
126
|
/* @__PURE__ */ jsxs("label", { className: "uix-field__label", children: [
|
|
126
127
|
labels.name,
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import * as react from 'react';
|
|
2
2
|
import { ReactNode, HTMLAttributes } from 'react';
|
|
3
|
+
import { UixRenderLink } from '../link.js';
|
|
3
4
|
|
|
4
5
|
interface BreadcrumbItem {
|
|
5
6
|
id?: string;
|
|
@@ -11,8 +12,14 @@ interface BreadcrumbsProps extends Omit<HTMLAttributes<HTMLElement>, 'children'>
|
|
|
11
12
|
items: readonly BreadcrumbItem[];
|
|
12
13
|
label: string;
|
|
13
14
|
separator?: ReactNode;
|
|
15
|
+
/**
|
|
16
|
+
* Renders each item's link with your router (HAR-1385): `renderLink={(p) => <NextLink {...p} />}`
|
|
17
|
+
* keeps client navigation and prefetch, and a locale-aware link adds the locale prefix. The
|
|
18
|
+
* same `UixRenderLink` adapter as `ButtonLink`, `Stat`, `Pagination` and `Steps`. Default: `<a>`.
|
|
19
|
+
*/
|
|
20
|
+
renderLink?: UixRenderLink;
|
|
14
21
|
}
|
|
15
|
-
/** Framework-neutral breadcrumb navigation
|
|
16
|
-
declare function Breadcrumbs({ items, label, separator, className, ...props }: BreadcrumbsProps): react.JSX.Element;
|
|
22
|
+
/** Framework-neutral breadcrumb navigation; `renderLink` renders the links with the app's router. */
|
|
23
|
+
declare function Breadcrumbs({ items, label, separator, renderLink, className, ...props }: BreadcrumbsProps): react.JSX.Element;
|
|
17
24
|
|
|
18
25
|
export { type BreadcrumbItem, Breadcrumbs, type BreadcrumbsProps };
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { Fragment } from "react";
|
|
3
|
-
|
|
3
|
+
import { renderUixLink } from "../link.js";
|
|
4
|
+
function Breadcrumbs({ items, label, separator = "/", renderLink, className, ...props }) {
|
|
4
5
|
return /* @__PURE__ */ jsx("nav", { "aria-label": label, className, ...props, children: /* @__PURE__ */ jsx("ol", { className: "uix-breadcrumbs", children: items.map((item, index) => {
|
|
5
6
|
const current = item.current ?? index === items.length - 1;
|
|
6
7
|
return /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
7
8
|
index > 0 && /* @__PURE__ */ jsx("li", { className: "uix-breadcrumbs__sep", "aria-hidden": "true", children: separator }),
|
|
8
|
-
/* @__PURE__ */ jsx("li", { children: item.href && !current ?
|
|
9
|
+
/* @__PURE__ */ jsx("li", { children: item.href && !current ? renderUixLink(renderLink, { href: item.href, children: item.label }) : /* @__PURE__ */ jsx("span", { "aria-current": current ? "page" : void 0, children: item.label }) })
|
|
9
10
|
] }, item.id ?? `${item.href ?? "current"}-${index}`);
|
|
10
11
|
}) }) });
|
|
11
12
|
}
|
|
@@ -41,10 +41,11 @@ interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
|
|
|
41
41
|
/**
|
|
42
42
|
* The button's keyboard shortcut, in `KbdCombo` key names: `['A']`, `['Mod', 'Enter']`
|
|
43
43
|
* (HAR-2031). Hover or focus shows the keys in a hint above the button (an icon button's
|
|
44
|
-
* `aria-label` before them
|
|
45
|
-
*
|
|
46
|
-
*
|
|
47
|
-
*
|
|
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.
|
|
48
49
|
*/
|
|
49
50
|
shortcut?: KeyShortcut;
|
|
50
51
|
children?: ReactNode;
|
|
@@ -65,13 +66,19 @@ interface ButtonLinkProps extends AnchorHTMLAttributes<HTMLAnchorElement> {
|
|
|
65
66
|
disabled?: boolean;
|
|
66
67
|
/** Your router's link (`(p) => <NextLink {...p} />`); default a plain `<a>`. */
|
|
67
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;
|
|
68
75
|
children?: ReactNode;
|
|
69
76
|
}
|
|
70
77
|
/**
|
|
71
78
|
* A link that looks like a `Button` (HAR-1348; TENSOR A4/B5, MOTUS C-3). Use it when the
|
|
72
79
|
* action navigates; use `Button` when it acts in place.
|
|
73
80
|
*/
|
|
74
|
-
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;
|
|
75
82
|
interface ButtonGroupProps {
|
|
76
83
|
children?: ReactNode;
|
|
77
84
|
className?: string;
|