@tensor_1/react 2.46.0 → 2.48.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (204) hide show
  1. package/README.md +509 -1
  2. package/dist/{SchedulingCalendar-B6TakO2h.d.ts → SchedulingCalendar-WWyFL3Eo.d.ts} +51 -2
  3. package/dist/announce.d.ts +10 -0
  4. package/dist/announce.js +30 -0
  5. package/dist/chart-analytic.cjs +33 -25
  6. package/dist/chart-preset.cjs +23 -15
  7. package/dist/chart.cjs +23 -15
  8. package/dist/combobox-model.d.ts +10 -1
  9. package/dist/combobox-model.js +15 -0
  10. package/dist/command-palette-model.d.ts +44 -0
  11. package/dist/command-palette-model.js +36 -0
  12. package/dist/components/AccountMenu.d.ts +44 -0
  13. package/dist/components/AccountMenu.js +43 -0
  14. package/dist/components/AppShell.d.ts +45 -2
  15. package/dist/components/AppShell.js +116 -5
  16. package/dist/components/AuditLog.d.ts +128 -0
  17. package/dist/components/AuditLog.js +196 -0
  18. package/dist/components/BrandProfiles.js +4 -3
  19. package/dist/components/Breadcrumbs.d.ts +9 -2
  20. package/dist/components/Breadcrumbs.js +3 -2
  21. package/dist/components/BusyBar.d.ts +13 -0
  22. package/dist/components/BusyBar.js +10 -0
  23. package/dist/components/BusyRegion.d.ts +18 -0
  24. package/dist/components/BusyRegion.js +12 -0
  25. package/dist/components/Button.d.ts +33 -5
  26. package/dist/components/Button.js +12 -6
  27. package/dist/components/ButtonFeedback.d.ts +30 -0
  28. package/dist/components/ButtonFeedback.js +78 -0
  29. package/dist/components/Card.d.ts +18 -1
  30. package/dist/components/Card.js +5 -1
  31. package/dist/components/ChangeHighlight.d.ts +30 -0
  32. package/dist/components/ChangeHighlight.js +21 -0
  33. package/dist/components/ChartCore.js +2 -1
  34. package/dist/components/Checkbox.d.ts +27 -1
  35. package/dist/components/Checkbox.js +31 -2
  36. package/dist/components/CodeDisplay.d.ts +65 -0
  37. package/dist/components/CodeDisplay.js +124 -0
  38. package/dist/components/Combobox.d.ts +27 -6
  39. package/dist/components/Combobox.js +195 -58
  40. package/dist/components/CommandPalette.d.ts +36 -1
  41. package/dist/components/CommandPalette.js +254 -2
  42. package/dist/components/ContactCard.d.ts +66 -0
  43. package/dist/components/ContactCard.js +74 -0
  44. package/dist/components/Count.d.ts +24 -0
  45. package/dist/components/Count.js +32 -0
  46. package/dist/components/DataWorkspace.d.ts +167 -0
  47. package/dist/components/DataWorkspace.js +383 -0
  48. package/dist/components/DateFieldParts.d.ts +2 -1
  49. package/dist/components/DateFieldParts.js +7 -0
  50. package/dist/components/DatePicker.d.ts +9 -2
  51. package/dist/components/DatePicker.js +67 -28
  52. package/dist/components/DateRangePicker.d.ts +12 -2
  53. package/dist/components/DateRangePicker.js +13 -12
  54. package/dist/components/DelayedRender.d.ts +15 -0
  55. package/dist/components/DelayedRender.js +10 -0
  56. package/dist/components/DetailPage.d.ts +7 -1
  57. package/dist/components/DetailPage.js +2 -1
  58. package/dist/components/Dialogs.d.ts +71 -10
  59. package/dist/components/Dialogs.js +130 -30
  60. package/dist/components/DiffViewer.d.ts +23 -3
  61. package/dist/components/DiffViewer.js +26 -12
  62. package/dist/components/DiscardPrompt.d.ts +1 -0
  63. package/dist/components/DiscardPrompt.js +16 -0
  64. package/dist/components/DisplayPreferences.d.ts +83 -0
  65. package/dist/components/DisplayPreferences.js +185 -0
  66. package/dist/components/Drawer.d.ts +1 -0
  67. package/dist/components/EditorialHome.d.ts +23 -3
  68. package/dist/components/EditorialHome.js +16 -11
  69. package/dist/components/EntityPicker.d.ts +20 -3
  70. package/dist/components/EntityPicker.js +32 -13
  71. package/dist/components/FilterEditor.d.ts +18 -4
  72. package/dist/components/FilterEditor.js +141 -19
  73. package/dist/components/InfoTip.js +11 -12
  74. package/dist/components/InlineEdit.d.ts +87 -20
  75. package/dist/components/InlineEdit.js +197 -54
  76. package/dist/components/Input.d.ts +12 -2
  77. package/dist/components/Input.js +5 -3
  78. package/dist/components/Kbd.d.ts +6 -1
  79. package/dist/components/Kbd.js +6 -6
  80. package/dist/components/Lightbox.d.ts +41 -5
  81. package/dist/components/Lightbox.js +95 -9
  82. package/dist/components/MatchReview.d.ts +40 -1
  83. package/dist/components/MatchReview.js +130 -4
  84. package/dist/components/MetricInput.js +3 -2
  85. package/dist/components/Modal.d.ts +3 -1
  86. package/dist/components/Modal.js +13 -4
  87. package/dist/components/NavFavourites.d.ts +5 -0
  88. package/dist/components/NavFavourites.js +7 -7
  89. package/dist/components/OtpInput.d.ts +33 -0
  90. package/dist/components/OtpInput.js +123 -0
  91. package/dist/components/PageHeader.d.ts +7 -1
  92. package/dist/components/PageHeader.js +2 -2
  93. package/dist/components/Pagination.js +6 -5
  94. package/dist/components/PasswordInput.d.ts +34 -0
  95. package/dist/components/PasswordInput.js +101 -0
  96. package/dist/components/Peek.js +3 -3
  97. package/dist/components/Popover.js +2 -0
  98. package/dist/components/PrintLayout.d.ts +63 -0
  99. package/dist/components/PrintLayout.js +114 -0
  100. package/dist/components/QrCode.d.ts +47 -0
  101. package/dist/components/QrCode.js +55 -0
  102. package/dist/components/QrScanner.d.ts +95 -0
  103. package/dist/components/QrScanner.js +266 -0
  104. package/dist/components/RelationshipGraph.d.ts +32 -1
  105. package/dist/components/RelationshipGraph.js +18 -5
  106. package/dist/components/RelationshipGraphLayered.d.ts +1 -1
  107. package/dist/components/RelationshipGraphLayered.js +190 -115
  108. package/dist/components/RelationshipGraphList.js +33 -6
  109. package/dist/components/RuleBuilder.js +39 -7
  110. package/dist/components/SaveStatus.d.ts +7 -1
  111. package/dist/components/SaveStatus.js +13 -4
  112. package/dist/components/SchedulingAgenda.d.ts +3 -1
  113. package/dist/components/SchedulingCalendar.d.ts +3 -1
  114. package/dist/components/SchedulingCalendar.js +43 -4
  115. package/dist/components/SchedulingResourceDay.d.ts +57 -0
  116. package/dist/components/SchedulingResourceDay.js +287 -0
  117. package/dist/components/SchedulingTimeGrid.d.ts +3 -1
  118. package/dist/components/SchedulingTimeline.d.ts +3 -1
  119. package/dist/components/SearchBar.d.ts +1 -0
  120. package/dist/components/SearchSuggest.d.ts +23 -3
  121. package/dist/components/SearchSuggest.js +48 -17
  122. package/dist/components/Segmented.d.ts +39 -4
  123. package/dist/components/Segmented.js +31 -3
  124. package/dist/components/Select.d.ts +3 -2
  125. package/dist/components/Select.js +1 -1
  126. package/dist/components/ShortcutHint.d.ts +11 -10
  127. package/dist/components/ShortcutHint.js +44 -38
  128. package/dist/components/Sidebar.d.ts +36 -4
  129. package/dist/components/Sidebar.js +37 -12
  130. package/dist/components/SortableList.d.ts +45 -0
  131. package/dist/components/SortableList.js +134 -0
  132. package/dist/components/Spinner.d.ts +7 -1
  133. package/dist/components/Spinner.js +7 -1
  134. package/dist/components/Stat.d.ts +28 -3
  135. package/dist/components/Stat.js +13 -3
  136. package/dist/components/States.d.ts +7 -1
  137. package/dist/components/States.js +7 -1
  138. package/dist/components/StatusPill.d.ts +14 -3
  139. package/dist/components/StatusPill.js +11 -3
  140. package/dist/components/Steps.js +2 -1
  141. package/dist/components/Table.d.ts +16 -2
  142. package/dist/components/Table.js +8 -4
  143. package/dist/components/TableControls.js +18 -58
  144. package/dist/components/TableOverflowProbe.d.ts +12 -0
  145. package/dist/components/TableOverflowProbe.js +27 -0
  146. package/dist/components/Tabs.d.ts +2 -1
  147. package/dist/components/Tabs.js +1 -0
  148. package/dist/components/TagInput.d.ts +67 -0
  149. package/dist/components/TagInput.js +342 -0
  150. package/dist/components/Textarea.d.ts +5 -0
  151. package/dist/components/Textarea.js +2 -2
  152. package/dist/components/TimePicker.d.ts +80 -0
  153. package/dist/components/TimePicker.js +347 -0
  154. package/dist/components/Toast.js +3 -2
  155. package/dist/components/Tooltip.d.ts +8 -4
  156. package/dist/components/Tooltip.js +24 -21
  157. package/dist/components/Tree.d.ts +8 -0
  158. package/dist/components/Tree.js +11 -5
  159. package/dist/display-preferences-model.d.ts +53 -0
  160. package/dist/display-preferences-model.js +64 -0
  161. package/dist/emoji.cjs +28 -35
  162. package/dist/filter-model.d.ts +59 -3
  163. package/dist/filter-model.js +67 -2
  164. package/dist/hooks/useChangeHighlight.d.ts +33 -0
  165. package/dist/hooks/useChangeHighlight.js +44 -0
  166. package/dist/hooks/useDelayedPending.d.ts +22 -0
  167. package/dist/hooks/useDelayedPending.js +35 -0
  168. package/dist/hooks/useDialog.d.ts +5 -0
  169. package/dist/hooks/useDialog.js +3 -2
  170. package/dist/hooks/useOptimisticValue.d.ts +51 -0
  171. package/dist/hooks/useOptimisticValue.js +61 -0
  172. package/dist/hooks/useReducedMotion.d.ts +4 -1
  173. package/dist/hooks/useReducedMotion.js +7 -1
  174. package/dist/hooks/useResolvedFilterLabels.d.ts +15 -0
  175. package/dist/hooks/useResolvedFilterLabels.js +34 -0
  176. package/dist/hooks/useVirtualRows.d.ts +21 -2
  177. package/dist/hooks/useVirtualRows.js +30 -3
  178. package/dist/icons/Icon.d.ts +1 -1
  179. package/dist/index.cjs +18977 -13378
  180. package/dist/index.d.cts +3065 -664
  181. package/dist/index.d.ts +46 -16
  182. package/dist/index.js +149 -7
  183. package/dist/labels-context.d.ts +93 -3
  184. package/dist/motion-timing.d.ts +8 -0
  185. package/dist/motion-timing.js +8 -0
  186. package/dist/overlay-stack.d.ts +6 -0
  187. package/dist/overlay-stack.js +13 -0
  188. package/dist/qr-model.d.ts +65 -0
  189. package/dist/qr-model.js +323 -0
  190. package/dist/relationship-graph-model.d.ts +78 -3
  191. package/dist/relationship-graph-model.js +112 -5
  192. package/dist/reorder.d.ts +30 -0
  193. package/dist/reorder.js +74 -0
  194. package/dist/rich-text.cjs +4 -2
  195. package/dist/scheduling-resource-model.d.ts +76 -0
  196. package/dist/scheduling-resource-model.js +74 -0
  197. package/dist/search-suggest-model.js +3 -1
  198. package/dist/shell-context.d.ts +11 -0
  199. package/dist/shell-context.js +7 -0
  200. package/dist/time-field-model.d.ts +46 -0
  201. package/dist/time-field-model.js +152 -0
  202. package/dist/toast-store.d.ts +17 -1
  203. package/dist/toast-store.js +23 -7
  204. package/package.json +2 -1
@@ -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 navState = nav ?? (collapsed ? "rail" : "full");
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__ */ jsx("div", { className: "uix-shell__topbar", children: topbar }),
45
- /* @__PURE__ */ jsx("main", { id: mainId, tabIndex: -1, className: cx("uix-shell__main", mainBleed && "uix-shell__main--bleed"), children }),
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("select", { className: "uix-select", value: value.id, onChange: (event) => {
121
- const selected = profiles.find((profile) => profile.id === event.currentTarget.value);
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
- }, children: profiles.map((profile) => /* @__PURE__ */ jsx("option", { value: profile.id, children: profile.name }, profile.id)) })
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. Route adapters can replace anchors at the application seam. */
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
- function Breadcrumbs({ items, label, separator = "/", className, ...props }) {
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 ? /* @__PURE__ */ jsx("a", { href: item.href, children: item.label }) : /* @__PURE__ */ jsx("span", { "aria-current": current ? "page" : void 0, children: item.label }) })
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
  }
@@ -0,0 +1,13 @@
1
+ import * as react from 'react';
2
+
3
+ /**
4
+ * The top indeterminate bar of a `BusyRegion` or a refreshing `TableWrap` (HAR-2582). It
5
+ * appears after 300 ms of `busy` and stays at least 500 ms; the parent dims its content
6
+ * while the bar shows (`:has(> .uix-busy-bar[data-shown])`), so the parent itself stays a
7
+ * server component. Decorative: the parent carries `aria-busy`.
8
+ */
9
+ declare function BusyBar({ busy }: {
10
+ busy: boolean;
11
+ }): react.JSX.Element;
12
+
13
+ export { BusyBar };
@@ -0,0 +1,10 @@
1
+ "use client";
2
+ import { jsx } from "react/jsx-runtime";
3
+ import { useDelayedPending } from "../hooks/useDelayedPending.js";
4
+ function BusyBar({ busy }) {
5
+ const shown = useDelayedPending(busy);
6
+ return /* @__PURE__ */ jsx("span", { className: "uix-busy-bar", "aria-hidden": "true", "data-shown": shown || void 0 });
7
+ }
8
+ export {
9
+ BusyBar
10
+ };
@@ -0,0 +1,18 @@
1
+ import * as react from 'react';
2
+ import { HTMLAttributes, ReactNode } from 'react';
3
+
4
+ interface BusyRegionProps extends HTMLAttributes<HTMLDivElement> {
5
+ /** The content is being fetched again (a filter, sort, page or saved-view change). */
6
+ busy: boolean;
7
+ children?: ReactNode;
8
+ }
9
+ /**
10
+ * Keeps what is on screen while it is fetched again (HAR-2582, motion standard §2.5): the
11
+ * children stay, dim, and an indeterminate bar runs along the top; `aria-busy` is set at
12
+ * once. The bar and the dim wait 300 ms, so a quick refetch shows nothing, and then stay at
13
+ * least 500 ms. A skeleton is for a first load only: swapping rows for skeletons on every
14
+ * refetch hides what changed. For a table, `TableWrap refreshing` does the same.
15
+ */
16
+ declare function BusyRegion({ busy, className, children, ...props }: BusyRegionProps): react.JSX.Element;
17
+
18
+ export { BusyRegion, type BusyRegionProps };
@@ -0,0 +1,12 @@
1
+ import { jsx, jsxs } from "react/jsx-runtime";
2
+ import { cx } from "../cx.js";
3
+ import { BusyBar } from "./BusyBar.js";
4
+ function BusyRegion({ busy, className, children, ...props }) {
5
+ return /* @__PURE__ */ jsxs("div", { className: cx("uix-busy-region", className), "aria-busy": busy || void 0, ...props, children: [
6
+ /* @__PURE__ */ jsx(BusyBar, { busy }),
7
+ /* @__PURE__ */ jsx("div", { className: "uix-busy-region__content", children })
8
+ ] });
9
+ }
10
+ export {
11
+ BusyRegion
12
+ };