@tensor_1/react 2.47.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 (183) hide show
  1. package/README.md +470 -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/Button.d.ts +12 -5
  22. package/dist/components/Button.js +7 -3
  23. package/dist/components/Card.d.ts +18 -1
  24. package/dist/components/Card.js +5 -1
  25. package/dist/components/ChangeHighlight.d.ts +30 -0
  26. package/dist/components/ChangeHighlight.js +21 -0
  27. package/dist/components/ChartCore.js +2 -1
  28. package/dist/components/Checkbox.d.ts +27 -1
  29. package/dist/components/Checkbox.js +31 -2
  30. package/dist/components/CodeDisplay.d.ts +65 -0
  31. package/dist/components/CodeDisplay.js +124 -0
  32. package/dist/components/Combobox.d.ts +27 -6
  33. package/dist/components/Combobox.js +195 -58
  34. package/dist/components/CommandPalette.d.ts +36 -1
  35. package/dist/components/CommandPalette.js +254 -2
  36. package/dist/components/ContactCard.d.ts +66 -0
  37. package/dist/components/ContactCard.js +74 -0
  38. package/dist/components/Count.d.ts +24 -0
  39. package/dist/components/Count.js +32 -0
  40. package/dist/components/DataWorkspace.d.ts +167 -0
  41. package/dist/components/DataWorkspace.js +383 -0
  42. package/dist/components/DateFieldParts.d.ts +2 -1
  43. package/dist/components/DateFieldParts.js +7 -0
  44. package/dist/components/DatePicker.d.ts +9 -2
  45. package/dist/components/DatePicker.js +67 -28
  46. package/dist/components/DateRangePicker.d.ts +12 -2
  47. package/dist/components/DateRangePicker.js +13 -12
  48. package/dist/components/DetailPage.d.ts +7 -1
  49. package/dist/components/DetailPage.js +2 -1
  50. package/dist/components/Dialogs.d.ts +71 -10
  51. package/dist/components/Dialogs.js +130 -30
  52. package/dist/components/DiffViewer.d.ts +23 -3
  53. package/dist/components/DiffViewer.js +26 -12
  54. package/dist/components/DiscardPrompt.js +16 -0
  55. package/dist/components/DisplayPreferences.d.ts +83 -0
  56. package/dist/components/DisplayPreferences.js +185 -0
  57. package/dist/components/EditorialHome.d.ts +23 -3
  58. package/dist/components/EditorialHome.js +16 -11
  59. package/dist/components/EntityPicker.d.ts +20 -3
  60. package/dist/components/EntityPicker.js +32 -13
  61. package/dist/components/FilterEditor.d.ts +18 -4
  62. package/dist/components/FilterEditor.js +141 -19
  63. package/dist/components/InfoTip.js +11 -12
  64. package/dist/components/InlineEdit.d.ts +87 -20
  65. package/dist/components/InlineEdit.js +197 -54
  66. package/dist/components/Input.d.ts +12 -2
  67. package/dist/components/Input.js +5 -3
  68. package/dist/components/Kbd.d.ts +6 -1
  69. package/dist/components/Kbd.js +6 -6
  70. package/dist/components/Lightbox.d.ts +41 -5
  71. package/dist/components/Lightbox.js +95 -9
  72. package/dist/components/MatchReview.d.ts +40 -1
  73. package/dist/components/MatchReview.js +130 -4
  74. package/dist/components/MetricInput.js +3 -2
  75. package/dist/components/Modal.d.ts +2 -1
  76. package/dist/components/Modal.js +13 -4
  77. package/dist/components/NavFavourites.d.ts +5 -0
  78. package/dist/components/NavFavourites.js +7 -7
  79. package/dist/components/OtpInput.d.ts +33 -0
  80. package/dist/components/OtpInput.js +123 -0
  81. package/dist/components/PageHeader.d.ts +7 -1
  82. package/dist/components/PageHeader.js +2 -2
  83. package/dist/components/Pagination.js +6 -5
  84. package/dist/components/PasswordInput.d.ts +34 -0
  85. package/dist/components/PasswordInput.js +101 -0
  86. package/dist/components/Peek.js +3 -3
  87. package/dist/components/Popover.js +2 -0
  88. package/dist/components/PrintLayout.d.ts +63 -0
  89. package/dist/components/PrintLayout.js +114 -0
  90. package/dist/components/QrCode.d.ts +47 -0
  91. package/dist/components/QrCode.js +55 -0
  92. package/dist/components/QrScanner.d.ts +95 -0
  93. package/dist/components/QrScanner.js +266 -0
  94. package/dist/components/RelationshipGraph.d.ts +32 -1
  95. package/dist/components/RelationshipGraph.js +18 -5
  96. package/dist/components/RelationshipGraphLayered.d.ts +1 -1
  97. package/dist/components/RelationshipGraphLayered.js +190 -115
  98. package/dist/components/RelationshipGraphList.js +33 -6
  99. package/dist/components/RuleBuilder.js +39 -7
  100. package/dist/components/SchedulingAgenda.d.ts +3 -1
  101. package/dist/components/SchedulingCalendar.d.ts +3 -1
  102. package/dist/components/SchedulingCalendar.js +43 -4
  103. package/dist/components/SchedulingResourceDay.d.ts +57 -0
  104. package/dist/components/SchedulingResourceDay.js +287 -0
  105. package/dist/components/SchedulingTimeGrid.d.ts +3 -1
  106. package/dist/components/SchedulingTimeline.d.ts +3 -1
  107. package/dist/components/SearchSuggest.d.ts +23 -3
  108. package/dist/components/SearchSuggest.js +48 -17
  109. package/dist/components/Segmented.d.ts +39 -4
  110. package/dist/components/Segmented.js +31 -3
  111. package/dist/components/Select.d.ts +3 -2
  112. package/dist/components/Select.js +1 -1
  113. package/dist/components/ShortcutHint.d.ts +11 -10
  114. package/dist/components/ShortcutHint.js +44 -38
  115. package/dist/components/Sidebar.d.ts +36 -4
  116. package/dist/components/Sidebar.js +37 -12
  117. package/dist/components/SortableList.d.ts +45 -0
  118. package/dist/components/SortableList.js +134 -0
  119. package/dist/components/Stat.d.ts +28 -3
  120. package/dist/components/Stat.js +13 -3
  121. package/dist/components/StatusPill.d.ts +14 -3
  122. package/dist/components/StatusPill.js +11 -3
  123. package/dist/components/Steps.js +2 -1
  124. package/dist/components/Table.d.ts +9 -2
  125. package/dist/components/Table.js +4 -2
  126. package/dist/components/TableControls.js +18 -58
  127. package/dist/components/TableOverflowProbe.d.ts +12 -0
  128. package/dist/components/TableOverflowProbe.js +27 -0
  129. package/dist/components/Tabs.d.ts +2 -1
  130. package/dist/components/Tabs.js +1 -0
  131. package/dist/components/TagInput.d.ts +67 -0
  132. package/dist/components/TagInput.js +342 -0
  133. package/dist/components/Textarea.d.ts +5 -0
  134. package/dist/components/Textarea.js +2 -2
  135. package/dist/components/TimePicker.d.ts +80 -0
  136. package/dist/components/TimePicker.js +347 -0
  137. package/dist/components/Toast.js +3 -2
  138. package/dist/components/Tooltip.d.ts +8 -4
  139. package/dist/components/Tooltip.js +24 -21
  140. package/dist/components/Tree.d.ts +8 -0
  141. package/dist/components/Tree.js +11 -5
  142. package/dist/display-preferences-model.d.ts +53 -0
  143. package/dist/display-preferences-model.js +64 -0
  144. package/dist/emoji.cjs +28 -35
  145. package/dist/filter-model.d.ts +59 -3
  146. package/dist/filter-model.js +67 -2
  147. package/dist/hooks/useChangeHighlight.d.ts +33 -0
  148. package/dist/hooks/useChangeHighlight.js +44 -0
  149. package/dist/hooks/useDialog.d.ts +5 -0
  150. package/dist/hooks/useDialog.js +3 -2
  151. package/dist/hooks/useOptimisticValue.d.ts +51 -0
  152. package/dist/hooks/useOptimisticValue.js +61 -0
  153. package/dist/hooks/useReducedMotion.d.ts +4 -1
  154. package/dist/hooks/useReducedMotion.js +7 -1
  155. package/dist/hooks/useResolvedFilterLabels.d.ts +15 -0
  156. package/dist/hooks/useResolvedFilterLabels.js +34 -0
  157. package/dist/hooks/useVirtualRows.d.ts +21 -2
  158. package/dist/hooks/useVirtualRows.js +30 -3
  159. package/dist/icons/Icon.d.ts +1 -1
  160. package/dist/index.cjs +18644 -13210
  161. package/dist/index.d.cts +3003 -699
  162. package/dist/index.d.ts +42 -16
  163. package/dist/index.js +139 -7
  164. package/dist/labels-context.d.ts +90 -3
  165. package/dist/overlay-stack.d.ts +6 -0
  166. package/dist/overlay-stack.js +13 -0
  167. package/dist/qr-model.d.ts +65 -0
  168. package/dist/qr-model.js +323 -0
  169. package/dist/relationship-graph-model.d.ts +78 -3
  170. package/dist/relationship-graph-model.js +112 -5
  171. package/dist/reorder.d.ts +30 -0
  172. package/dist/reorder.js +74 -0
  173. package/dist/rich-text.cjs +4 -2
  174. package/dist/scheduling-resource-model.d.ts +76 -0
  175. package/dist/scheduling-resource-model.js +74 -0
  176. package/dist/search-suggest-model.js +3 -1
  177. package/dist/shell-context.d.ts +11 -0
  178. package/dist/shell-context.js +7 -0
  179. package/dist/time-field-model.d.ts +46 -0
  180. package/dist/time-field-model.js +152 -0
  181. package/dist/toast-store.d.ts +17 -1
  182. package/dist/toast-store.js +23 -7
  183. package/package.json +2 -1
@@ -0,0 +1,53 @@
1
+ type ThemePreference = 'light' | 'dark' | 'system';
2
+ type ContrastPreference = 'standard' | 'more' | 'system';
3
+ type TextScalePreference = 'sm' | 'md' | 'lg' | 'xl';
4
+ type MotionPreference = 'system' | 'reduce';
5
+ type DensityPreference = 'compact' | 'standard' | 'comfortable';
6
+ interface DisplayPreferenceValues {
7
+ theme: ThemePreference;
8
+ contrast: ContrastPreference;
9
+ textScale: TextScalePreference;
10
+ motion: MotionPreference;
11
+ density: DensityPreference;
12
+ }
13
+ /** What the system says, for the `system` choices. */
14
+ interface SystemDisplayPreferences {
15
+ dark: boolean;
16
+ moreContrast: boolean;
17
+ }
18
+ /** The attributes written on <html>; `undefined` means the attribute is removed. */
19
+ interface DisplayAttributes {
20
+ 'data-theme': 'light' | 'dark';
21
+ 'data-contrast'?: 'more';
22
+ 'data-text-scale'?: 'sm' | 'lg' | 'xl';
23
+ 'data-motion'?: 'reduce';
24
+ 'data-density'?: 'compact' | 'comfortable';
25
+ }
26
+ declare const DEFAULT_DISPLAY_PREFERENCES: DisplayPreferenceValues;
27
+ /** The default `localStorage` key. */
28
+ declare const DISPLAY_PREFERENCES_STORAGE_KEY = "uix-display-preferences";
29
+ declare const DISPLAY_PREFERENCE_OPTIONS: {
30
+ readonly theme: readonly ["light", "dark", "system"];
31
+ readonly contrast: readonly ["standard", "more", "system"];
32
+ readonly textScale: readonly ["sm", "md", "lg", "xl"];
33
+ readonly motion: readonly ["system", "reduce"];
34
+ readonly density: readonly ["compact", "standard", "comfortable"];
35
+ };
36
+ /** Read stored preferences; anything missing or unknown falls back to `defaults`. */
37
+ declare function parseDisplayPreferences(raw: string | null | undefined, defaults?: DisplayPreferenceValues): DisplayPreferenceValues;
38
+ /** The <html> attributes for a set of preferences, resolving `system` against the system. */
39
+ declare function resolveDisplayAttributes(prefs: DisplayPreferenceValues, system: SystemDisplayPreferences): DisplayAttributes;
40
+ /** Write (or remove) the attributes on an element, normally `document.documentElement`. */
41
+ declare function applyDisplayAttributes(root: Element, attributes: DisplayAttributes): void;
42
+ /** The system's answers from `matchMedia`; all false where there is none (SSR, tests). */
43
+ declare function readSystemDisplayPreferences(): SystemDisplayPreferences;
44
+ /**
45
+ * An inline script for the document head that applies the stored preferences before the first
46
+ * paint, so a dark or large-text page never flashes light or small (HAR-1372). Self-contained ES5;
47
+ * it never throws. Put it in a `<script>` before the stylesheets:
48
+ * <script dangerouslySetInnerHTML={{ __html: displayPreferencesScript() }} />
49
+ * It mirrors parseDisplayPreferences + resolveDisplayAttributes (tested to agree).
50
+ */
51
+ declare function displayPreferencesScript(storageKey?: string, defaults?: DisplayPreferenceValues): string;
52
+
53
+ export { type ContrastPreference, DEFAULT_DISPLAY_PREFERENCES, DISPLAY_PREFERENCES_STORAGE_KEY, DISPLAY_PREFERENCE_OPTIONS, type DensityPreference, type DisplayAttributes, type DisplayPreferenceValues, type MotionPreference, type SystemDisplayPreferences, type TextScalePreference, type ThemePreference, applyDisplayAttributes, displayPreferencesScript, parseDisplayPreferences, readSystemDisplayPreferences, resolveDisplayAttributes };
@@ -0,0 +1,64 @@
1
+ const DEFAULT_DISPLAY_PREFERENCES = {
2
+ theme: "system",
3
+ contrast: "system",
4
+ textScale: "md",
5
+ motion: "system",
6
+ density: "standard"
7
+ };
8
+ const DISPLAY_PREFERENCES_STORAGE_KEY = "uix-display-preferences";
9
+ const DISPLAY_PREFERENCE_OPTIONS = {
10
+ theme: ["light", "dark", "system"],
11
+ contrast: ["standard", "more", "system"],
12
+ textScale: ["sm", "md", "lg", "xl"],
13
+ motion: ["system", "reduce"],
14
+ density: ["compact", "standard", "comfortable"]
15
+ };
16
+ function parseDisplayPreferences(raw, defaults = DEFAULT_DISPLAY_PREFERENCES) {
17
+ let stored = {};
18
+ try {
19
+ stored = raw ? JSON.parse(raw) ?? {} : {};
20
+ } catch {
21
+ stored = {};
22
+ }
23
+ if (typeof stored !== "object") stored = {};
24
+ const pick = (key) => {
25
+ const value = stored[key];
26
+ return DISPLAY_PREFERENCE_OPTIONS[key].includes(value) ? value : defaults[key];
27
+ };
28
+ return { theme: pick("theme"), contrast: pick("contrast"), textScale: pick("textScale"), motion: pick("motion"), density: pick("density") };
29
+ }
30
+ function resolveDisplayAttributes(prefs, system) {
31
+ const theme = prefs.theme === "system" ? system.dark ? "dark" : "light" : prefs.theme;
32
+ const more = prefs.contrast === "more" || prefs.contrast === "system" && system.moreContrast;
33
+ return {
34
+ "data-theme": theme,
35
+ "data-contrast": more ? "more" : void 0,
36
+ "data-text-scale": prefs.textScale === "md" ? void 0 : prefs.textScale,
37
+ "data-motion": prefs.motion === "reduce" ? "reduce" : void 0,
38
+ "data-density": prefs.density === "standard" ? void 0 : prefs.density
39
+ };
40
+ }
41
+ function applyDisplayAttributes(root, attributes) {
42
+ for (const name of ["data-theme", "data-contrast", "data-text-scale", "data-motion", "data-density"]) {
43
+ const value = attributes[name];
44
+ if (value == null) root.removeAttribute(name);
45
+ else if (root.getAttribute(name) !== value) root.setAttribute(name, value);
46
+ }
47
+ }
48
+ function readSystemDisplayPreferences() {
49
+ const mm = (query) => typeof matchMedia === "function" && matchMedia(query).matches;
50
+ return { dark: mm("(prefers-color-scheme: dark)"), moreContrast: mm("(prefers-contrast: more)") };
51
+ }
52
+ function displayPreferencesScript(storageKey = DISPLAY_PREFERENCES_STORAGE_KEY, defaults = DEFAULT_DISPLAY_PREFERENCES) {
53
+ return `(function(){try{var d=document.documentElement,p={};try{p=JSON.parse(localStorage.getItem(${JSON.stringify(storageKey)})||'{}')||{}}catch(e){}if(typeof p!=='object')p={};var D=${JSON.stringify(defaults)},O=${JSON.stringify(DISPLAY_PREFERENCE_OPTIONS)};for(var k in O)if(O[k].indexOf(p[k])<0)p[k]=D[k];var m=function(q){return !!(window.matchMedia&&window.matchMedia(q).matches)};var s=function(n,v){if(v)d.setAttribute(n,v);else d.removeAttribute(n)};s('data-theme',p.theme==='light'||p.theme==='dark'?p.theme:(m('(prefers-color-scheme: dark)')?'dark':'light'));s('data-contrast',p.contrast==='more'||(p.contrast!=='standard'&&m('(prefers-contrast: more)'))?'more':'');s('data-text-scale',p.textScale==='sm'||p.textScale==='lg'||p.textScale==='xl'?p.textScale:'');s('data-motion',p.motion==='reduce'?'reduce':'');s('data-density',p.density==='compact'||p.density==='comfortable'?p.density:'')}catch(e){}})();`;
54
+ }
55
+ export {
56
+ DEFAULT_DISPLAY_PREFERENCES,
57
+ DISPLAY_PREFERENCES_STORAGE_KEY,
58
+ DISPLAY_PREFERENCE_OPTIONS,
59
+ applyDisplayAttributes,
60
+ displayPreferencesScript,
61
+ parseDisplayPreferences,
62
+ readSystemDisplayPreferences,
63
+ resolveDisplayAttributes
64
+ };
package/dist/emoji.cjs CHANGED
@@ -500,6 +500,8 @@ function Popover({
500
500
  };
501
501
  const onKeyDown = (event) => {
502
502
  if (event.key !== "Escape" || !isPopoverOpen(el)) return;
503
+ event.preventDefault();
504
+ event.stopPropagation();
503
505
  window.clearTimeout(timer);
504
506
  const hadFocus = el.contains(el.ownerDocument.activeElement);
505
507
  try {
@@ -1122,32 +1124,21 @@ function useKbdPlatform(platform) {
1122
1124
  const detected = (0, import_react5.useSyncExternalStore)(noSubscription, detectPlatform, serverPlatform);
1123
1125
  return platform ?? detected;
1124
1126
  }
1125
- var ARIA_KEYS = {
1126
- Mod: { mac: "Meta", other: "Control" },
1127
- Ctrl: { mac: "Control", other: "Control" },
1128
- Esc: { mac: "Escape", other: "Escape" },
1129
- Del: { mac: "Delete", other: "Delete" },
1130
- " ": { mac: "Space", other: "Space" },
1131
- "+": { mac: "Plus", other: "Plus" }
1132
- };
1133
- function ariaKeyShortcuts(keys, platform) {
1134
- return keys.map((key) => ARIA_KEYS[key]?.[platform] ?? (key.length === 1 ? key.toUpperCase() : key)).join("+");
1135
- }
1136
1127
  var isKeyShortcut = (value) => Array.isArray(value) && value.length > 0 && value.every((key) => typeof key === "string");
1128
+ function spokenKeys(keys, platform, keyLabels) {
1129
+ return keys.map((key) => keyLabels?.[key] ?? KEYS[key]?.[platform]?.[1] ?? key).join("+");
1130
+ }
1137
1131
  function KbdCombo({ keys, platform, keyLabels, className, ...props }) {
1138
1132
  const active = useKbdPlatform(platform);
1139
- const parts = keys.map((key) => {
1140
- const known = KEYS[key]?.[active];
1141
- return { glyph: known?.[0] ?? key, spoken: keyLabels?.[key] ?? known?.[1] ?? key };
1142
- });
1143
1133
  return /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("span", { className: cx("uix-kbd-combo", className), ...props, children: [
1144
- /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("span", { className: "uix-visually-hidden", children: parts.map((p) => p.spoken).join("+") }),
1145
- /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("span", { className: "uix-kbd-combo__keys", "aria-hidden": "true", children: parts.map((p, i) => /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("kbd", { className: "uix-kbd", children: p.glyph }, `${keys[i]}-${i}`)) })
1134
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("span", { className: "uix-visually-hidden", children: spokenKeys(keys, active, keyLabels) }),
1135
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("span", { className: "uix-kbd-combo__keys", "aria-hidden": "true", children: keys.map((key, i) => /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("kbd", { className: "uix-kbd", children: KEYS[key]?.[active]?.[0] ?? key }, `${key}-${i}`)) })
1146
1136
  ] });
1147
1137
  }
1148
1138
 
1149
1139
  // src/components/Tooltip.tsx
1150
1140
  var import_jsx_runtime6 = require("react/jsx-runtime");
1141
+ var useIsomorphicLayoutEffect4 = typeof document !== "undefined" ? import_react6.useLayoutEffect : import_react6.useEffect;
1151
1142
  function Tooltip({ label, content, placement = "top", shortcut, className, children, ...props }) {
1152
1143
  const triggerRef = (0, import_react6.useRef)(null);
1153
1144
  const bubbleRef = (0, import_react6.useRef)(null);
@@ -1158,7 +1149,6 @@ function Tooltip({ label, content, placement = "top", shortcut, className, child
1158
1149
  const childShortcut = (0, import_react6.isValidElement)(children) ? children.props.shortcut : void 0;
1159
1150
  const own = shortcut ?? (isKeyShortcut(childShortcut) ? childShortcut : void 0);
1160
1151
  const keys = own && own.length > 0 ? own : null;
1161
- const keyshortcuts = keys ? ariaKeyShortcuts(keys, platform) : void 0;
1162
1152
  useAnchoredPosition(triggerRef, bubbleRef, { open, placement, offset: 6 });
1163
1153
  const cancelClose = () => {
1164
1154
  if (closeTimer.current != null) {
@@ -1192,13 +1182,16 @@ function Tooltip({ label, content, placement = "top", shortcut, className, child
1192
1182
  cancelClose();
1193
1183
  closeTimer.current = window.setTimeout(hide, 100);
1194
1184
  };
1195
- (0, import_react6.useEffect)(() => {
1196
- if (!open) return;
1185
+ useIsomorphicLayoutEffect4(() => {
1186
+ if (!open) return void 0;
1197
1187
  const onKey = (e) => {
1198
- if (e.key === "Escape") hide();
1188
+ if (e.key !== "Escape") return;
1189
+ e.preventDefault();
1190
+ e.stopPropagation();
1191
+ hide();
1199
1192
  };
1200
- document.addEventListener("keydown", onKey);
1201
- return () => document.removeEventListener("keydown", onKey);
1193
+ document.addEventListener("keydown", onKey, true);
1194
+ return () => document.removeEventListener("keydown", onKey, true);
1202
1195
  }, [open]);
1203
1196
  (0, import_react6.useEffect)(() => () => {
1204
1197
  if (closeTimer.current != null) clearTimeout(closeTimer.current);
@@ -1211,30 +1204,27 @@ function Tooltip({ label, content, placement = "top", shortcut, className, child
1211
1204
  }
1212
1205
  }, []);
1213
1206
  const bubbleAttr = { popover: "manual" };
1207
+ const text = content ?? label;
1208
+ const description = keys ? [text != null ? `${id}-text` : null, `${id}-keys`].filter(Boolean).join(" ") : id;
1214
1209
  const single = (0, import_react6.isValidElement)(children);
1215
1210
  const child = single ? (0, import_react6.cloneElement)(children, {
1216
- "aria-describedby": [children.props["aria-describedby"], id].filter(Boolean).join(" "),
1217
- ...keyshortcuts ? { "aria-keyshortcuts": keyshortcuts } : {}
1211
+ "aria-describedby": [children.props["aria-describedby"], description].filter(Boolean).join(" ")
1218
1212
  }) : children;
1219
1213
  return /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(
1220
1214
  "span",
1221
1215
  {
1222
1216
  ref: triggerRef,
1223
1217
  className: cx("uix-tooltip", className),
1224
- "aria-describedby": single ? void 0 : id,
1225
- "aria-keyshortcuts": single ? void 0 : keyshortcuts,
1218
+ "aria-describedby": single ? void 0 : description,
1226
1219
  tabIndex: single ? void 0 : 0,
1227
1220
  onMouseEnter: show,
1228
1221
  onMouseLeave: scheduleHide,
1229
1222
  onFocus: show,
1230
1223
  onBlur: hide,
1231
- onKeyDown: (e) => {
1232
- if (e.key === "Escape") hide();
1233
- },
1234
1224
  ...props,
1235
1225
  children: [
1236
1226
  child,
1237
- /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(
1227
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
1238
1228
  "span",
1239
1229
  {
1240
1230
  ref: bubbleRef,
@@ -1244,10 +1234,13 @@ function Tooltip({ label, content, placement = "top", shortcut, className, child
1244
1234
  onMouseEnter: cancelClose,
1245
1235
  onMouseLeave: scheduleHide,
1246
1236
  ...bubbleAttr,
1247
- children: [
1248
- content ?? label,
1249
- keys && /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("span", { className: "uix-tooltip__shortcut", children: /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(KbdCombo, { keys: [...keys], platform }) })
1250
- ]
1237
+ children: keys ? /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(import_jsx_runtime6.Fragment, { children: [
1238
+ text != null && /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("span", { id: `${id}-text`, children: text }),
1239
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("span", { className: "uix-tooltip__shortcut", children: [
1240
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("span", { id: `${id}-keys`, className: "uix-visually-hidden", children: spokenKeys(keys, platform) }),
1241
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(KbdCombo, { keys: [...keys], platform, "aria-hidden": "true" })
1242
+ ] })
1243
+ ] }) : text
1251
1244
  }
1252
1245
  )
1253
1246
  ]
@@ -9,6 +9,12 @@ interface FilterChoice {
9
9
  }
10
10
  type TextFilterOperator = 'contains' | 'not-contains' | 'is' | 'is-not' | 'starts-with';
11
11
  type NumberFilterOperator = 'eq' | 'neq' | 'lt' | 'lte' | 'gt' | 'gte' | 'between';
12
+ /**
13
+ * A date condition (HAR-1832). `on`, `before` and `after` take one day in `from`; `between` takes
14
+ * `from` and `to`, both inclusive. "Before" excludes the day and "after" starts the next day:
15
+ * `dateFilterBounds` turns each into the half-open range a query needs.
16
+ */
17
+ type DateFilterOperator = 'on' | 'before' | 'after' | 'between';
12
18
  type FilterValue = {
13
19
  kind: 'enum';
14
20
  values: string[];
@@ -25,12 +31,24 @@ type FilterValue = {
25
31
  kind: 'date-range';
26
32
  from?: string;
27
33
  to?: string;
34
+ }
35
+ /** A date with a condition (HAR-1832); `YYYY-MM-DD`. A saved `between` stays `between`, even of one day. */
36
+ | {
37
+ kind: 'date';
38
+ operator: DateFilterOperator;
39
+ from?: string;
40
+ to?: string;
28
41
  } | {
29
42
  kind: 'boolean';
30
43
  value?: boolean;
31
44
  } | {
32
45
  kind: 'reference';
33
46
  values: FilterChoice[];
47
+ }
48
+ /** Free-text values typed in as tags (HAR-1832), e.g. several hostnames; matched by the product. */
49
+ | {
50
+ kind: 'tags';
51
+ values: string[];
34
52
  };
35
53
  type FilterValueKind = FilterValue['kind'];
36
54
  /**
@@ -52,8 +70,15 @@ interface FilterField {
52
70
  options?: readonly FilterChoice[];
53
71
  /** `reference`: finds records to pick; rejected promises show the error state. */
54
72
  onSearch?: (query: string) => Promise<readonly FilterChoice[]>;
73
+ /**
74
+ * `reference`: labels for chosen ids that arrived without one (a saved view restores ids).
75
+ * The editor asks once for every id whose label is missing or equal to the id.
76
+ */
77
+ resolveLabels?: (ids: readonly string[]) => Promise<readonly FilterChoice[]>;
55
78
  /** `text` / `number`: the operators offered, in order. Default: all of the kind. */
56
79
  operators?: readonly (TextFilterOperator | NumberFilterOperator)[];
80
+ /** `date`: the conditions offered, in order. Default: on, before, after, between. */
81
+ dateOperators?: readonly DateFilterOperator[];
57
82
  /** `number`: a unit shown after the value, e.g. "days". */
58
83
  unit?: string;
59
84
  /** `boolean`: words for true and false, e.g. Escalated / Not escalated. Default from labels. */
@@ -80,14 +105,45 @@ interface FilterSummaryLabels {
80
105
  more: string;
81
106
  /** An `enum` value equal to a preset: `{label}` (the preset), `{field}`. Default `{label}`. */
82
107
  preset?: string;
108
+ /** `date` conditions (HAR-1832), `{date}`. Default "{date}", "before {date}", "after {date}". */
109
+ on?: string;
110
+ before?: string;
111
+ after?: string;
83
112
  }
84
113
  declare const DEFAULT_FILTER_SUMMARY_LABELS: FilterSummaryLabels;
85
114
  declare const TEXT_FILTER_OPERATORS: readonly TextFilterOperator[];
86
115
  declare const NUMBER_FILTER_OPERATORS: readonly NumberFilterOperator[];
116
+ declare const DATE_FILTER_OPERATORS: readonly DateFilterOperator[];
87
117
  /** The empty value for a field kind (what Clear resets to). */
88
118
  declare function emptyFilterValue(kind: FilterValueKind): FilterValue;
89
119
  /** True when the value filters nothing (so the chip goes away). */
90
120
  declare function isFilterEmpty(value: FilterValue | undefined): boolean;
121
+ /**
122
+ * A range whose ends were typed the wrong way round (from 10 Oct to 1 Oct, 7 to 2) put in order,
123
+ * so it means what the person meant instead of matching nothing (PR #168 review). Number
124
+ * `between`, date `between` and `date-range`; every other value comes back as it is. Internal:
125
+ * FilterEditor applies through it, `dateFilterBounds` and `summarizeFilter` read through it.
126
+ */
127
+ declare function orderFilterRange<V extends FilterValue | undefined>(value: V): V;
128
+ /**
129
+ * The half-open range a `date` filter means, for a query (HAR-1832): `{ from }` is the first day
130
+ * included, `{ until }` the first day NOT included, either may be missing. "On 5 Oct" is
131
+ * `[5 Oct, 6 Oct)`, "before 5 Oct" is `[…, 5 Oct)`, "after 5 Oct" is `[6 Oct, …)`, "between 1 and
132
+ * 5 Oct" is `[1 Oct, 6 Oct)`. A `date-range` value reads as `between` with optional ends. Empty
133
+ * values give `{}`.
134
+ */
135
+ declare function dateFilterBounds(input: FilterValue | undefined): {
136
+ from?: string;
137
+ until?: string;
138
+ };
139
+ /** True for a chosen reference whose label is missing or only repeats its id (restored from an id). */
140
+ declare const isUnresolvedChoice: (choice: FilterChoice) => boolean;
141
+ /**
142
+ * Fills in the labels of a `reference` value restored from ids (HAR-1832), with
143
+ * `field.resolveLabels`. Other values, and values whose labels are all known, come back as they
144
+ * are. Ids the resolver does not return keep the label they had.
145
+ */
146
+ declare function resolveFilterLabels(field: FilterField, value: FilterValue | undefined): Promise<FilterValue | undefined>;
91
147
  /**
92
148
  * The first of `field.presets` whose values equal the value's as a set, or undefined. Only an
93
149
  * `enum` value can match a preset.
@@ -104,9 +160,9 @@ interface SummarizeFilterOptions {
104
160
  }
105
161
  /**
106
162
  * One line for the filter chip, e.g. "State: Open, Pending", "Priority ≥ 2", "Created:
107
- * 01.10.2026 – 05.10.2026", "Escalated: Yes". An enum value equal to one of `field.presets`
163
+ * 01.10.2026 – 05.10.2026", "Created: before 05.10.2026", "Host: db-01, db-02 +1", "Escalated: Yes". An enum value equal to one of `field.presets`
108
164
  * gives that preset's label (the `preset` label). Empty values give an empty string.
109
165
  */
110
- declare function summarizeFilter(field: FilterField, value: FilterValue | undefined, options?: SummarizeFilterOptions): string;
166
+ declare function summarizeFilter(field: FilterField, input: FilterValue | undefined, options?: SummarizeFilterOptions): string;
111
167
 
112
- export { DEFAULT_FILTER_SUMMARY_LABELS, type FilterChoice, type FilterField, type FilterPreset, type FilterSummaryLabels, type FilterValue, type FilterValueKind, NUMBER_FILTER_OPERATORS, type NumberFilterOperator, type SummarizeFilterOptions, TEXT_FILTER_OPERATORS, type TextFilterOperator, emptyFilterValue, isFilterEmpty, matchFilterPreset, summarizeFilter };
168
+ export { DATE_FILTER_OPERATORS, DEFAULT_FILTER_SUMMARY_LABELS, type DateFilterOperator, type FilterChoice, type FilterField, type FilterPreset, type FilterSummaryLabels, type FilterValue, type FilterValueKind, NUMBER_FILTER_OPERATORS, type NumberFilterOperator, type SummarizeFilterOptions, TEXT_FILTER_OPERATORS, type TextFilterOperator, dateFilterBounds, emptyFilterValue, isFilterEmpty, isUnresolvedChoice, matchFilterPreset, orderFilterRange, resolveFilterLabels, summarizeFilter };
@@ -19,10 +19,14 @@ const DEFAULT_FILTER_SUMMARY_LABELS = {
19
19
  from: "from {from}",
20
20
  until: "until {to}",
21
21
  more: "{first} +{count}",
22
- preset: "{label}"
22
+ preset: "{label}",
23
+ on: "{date}",
24
+ before: "before {date}",
25
+ after: "after {date}"
23
26
  };
24
27
  const TEXT_FILTER_OPERATORS = ["contains", "not-contains", "is", "is-not", "starts-with"];
25
28
  const NUMBER_FILTER_OPERATORS = ["eq", "neq", "lt", "lte", "gt", "gte", "between"];
29
+ const DATE_FILTER_OPERATORS = ["on", "before", "after", "between"];
26
30
  function emptyFilterValue(kind) {
27
31
  switch (kind) {
28
32
  case "enum":
@@ -33,10 +37,14 @@ function emptyFilterValue(kind) {
33
37
  return { kind, operator: "eq" };
34
38
  case "date-range":
35
39
  return { kind };
40
+ case "date":
41
+ return { kind, operator: "on" };
36
42
  case "boolean":
37
43
  return { kind };
38
44
  case "reference":
39
45
  return { kind, values: [] };
46
+ case "tags":
47
+ return { kind, values: [] };
40
48
  }
41
49
  }
42
50
  function isFilterEmpty(value) {
@@ -44,6 +52,7 @@ function isFilterEmpty(value) {
44
52
  switch (value.kind) {
45
53
  case "enum":
46
54
  case "reference":
55
+ case "tags":
47
56
  return value.values.length === 0;
48
57
  case "text":
49
58
  return value.text.trim() === "";
@@ -51,10 +60,53 @@ function isFilterEmpty(value) {
51
60
  return value.value === void 0 || value.operator === "between" && value.to === void 0;
52
61
  case "date-range":
53
62
  return !value.from && !value.to;
63
+ case "date":
64
+ return !value.from || value.operator === "between" && !value.to;
54
65
  case "boolean":
55
66
  return value.value === void 0;
56
67
  }
57
68
  }
69
+ function addDays(date, days) {
70
+ const [y, m, d] = date.split("-").map(Number);
71
+ const next = new Date(Date.UTC(y, m - 1, d + days));
72
+ return next.toISOString().slice(0, 10);
73
+ }
74
+ function orderFilterRange(value) {
75
+ if (!value) return value;
76
+ if (value.kind === "number" && value.operator === "between" && value.value !== void 0 && value.to !== void 0 && value.value > value.to) {
77
+ return { ...value, value: value.to, to: value.value };
78
+ }
79
+ if ((value.kind === "date-range" || value.kind === "date" && value.operator === "between") && value.from && value.to && value.from > value.to) {
80
+ return { ...value, from: value.to, to: value.from };
81
+ }
82
+ return value;
83
+ }
84
+ function dateFilterBounds(input) {
85
+ const value = orderFilterRange(input);
86
+ if (value?.kind === "date-range") {
87
+ return { ...value.from ? { from: value.from } : {}, ...value.to ? { until: addDays(value.to, 1) } : {} };
88
+ }
89
+ if (value?.kind !== "date" || isFilterEmpty(value)) return {};
90
+ const day = value.from;
91
+ switch (value.operator) {
92
+ case "on":
93
+ return { from: day, until: addDays(day, 1) };
94
+ case "before":
95
+ return { until: day };
96
+ case "after":
97
+ return { from: addDays(day, 1) };
98
+ case "between":
99
+ return { from: day, until: addDays(value.to, 1) };
100
+ }
101
+ }
102
+ const isUnresolvedChoice = (choice) => !choice.label || choice.label === choice.value;
103
+ async function resolveFilterLabels(field, value) {
104
+ if (value?.kind !== "reference" || !field.resolveLabels) return value;
105
+ const missing = value.values.filter(isUnresolvedChoice).map((v) => v.value);
106
+ if (missing.length === 0) return value;
107
+ const resolved = new Map((await field.resolveLabels(missing)).map((c) => [c.value, c.label]));
108
+ return { kind: "reference", values: value.values.map((v) => resolved.has(v.value) ? { value: v.value, label: resolved.get(v.value) } : v) };
109
+ }
58
110
  function matchFilterPreset(field, value) {
59
111
  if (value?.kind !== "enum" || !field.presets?.length) return void 0;
60
112
  const chosen = new Set(value.values);
@@ -64,7 +116,8 @@ function matchFilterPreset(field, value) {
64
116
  });
65
117
  }
66
118
  const fill = (template, values) => template.replace(/\{(\w+)\}/g, (match, key) => key in values ? String(values[key]) : match);
67
- function summarizeFilter(field, value, options = {}) {
119
+ function summarizeFilter(field, input, options = {}) {
120
+ const value = orderFilterRange(input);
68
121
  if (!value || isFilterEmpty(value)) return "";
69
122
  const labels = { ...DEFAULT_FILTER_SUMMARY_LABELS, ...options.labels, operators: { ...DEFAULT_FILTER_SUMMARY_LABELS.operators, ...options.labels?.operators } };
70
123
  const date = options.formatDate ?? ((d) => d);
@@ -82,22 +135,34 @@ function summarizeFilter(field, value, options = {}) {
82
135
  }
83
136
  case "reference":
84
137
  return `${field.label}: ${list(value.values.map((v) => v.label))}`;
138
+ case "tags":
139
+ return `${field.label}: ${list(value.values)}`;
85
140
  case "text":
86
141
  return `${field.label} ${labels.operators[value.operator]} \u201C${value.text.trim()}\u201D`;
87
142
  case "number":
88
143
  return value.operator === "between" ? `${field.label}: ${fill(labels.range, { from: num(value.value), to: num(value.to) })}` : `${field.label} ${labels.operators[value.operator]} ${num(value.value)}`;
89
144
  case "date-range":
90
145
  return `${field.label}: ${value.from && value.to ? fill(labels.range, { from: date(value.from), to: date(value.to) }) : value.from ? fill(labels.from, { from: date(value.from) }) : fill(labels.until, { to: date(value.to) })}`;
146
+ case "date": {
147
+ if (value.operator === "between") return `${field.label}: ${fill(labels.range, { from: date(value.from), to: date(value.to) })}`;
148
+ const template = labels[value.operator] ?? DEFAULT_FILTER_SUMMARY_LABELS[value.operator];
149
+ return `${field.label}: ${fill(template, { date: date(value.from) })}`;
150
+ }
91
151
  case "boolean":
92
152
  return `${field.label}: ${value.value ? field.trueLabel ?? labels.yes : field.falseLabel ?? labels.no}`;
93
153
  }
94
154
  }
95
155
  export {
156
+ DATE_FILTER_OPERATORS,
96
157
  DEFAULT_FILTER_SUMMARY_LABELS,
97
158
  NUMBER_FILTER_OPERATORS,
98
159
  TEXT_FILTER_OPERATORS,
160
+ dateFilterBounds,
99
161
  emptyFilterValue,
100
162
  isFilterEmpty,
163
+ isUnresolvedChoice,
101
164
  matchFilterPreset,
165
+ orderFilterRange,
166
+ resolveFilterLabels,
102
167
  summarizeFilter
103
168
  };
@@ -0,0 +1,33 @@
1
+ import { RefObject } from 'react';
2
+
3
+ interface ChangeHighlightOptions {
4
+ /**
5
+ * Said through the page's polite live region when the value changes, e.g. "Status changed to
6
+ * Resolved by A. Novak". The highlight is the signal and its place is the location; this is
7
+ * the description for people who cannot see either (motion standard §2.5).
8
+ */
9
+ announce?: string;
10
+ /** Turn the highlight off, e.g. while the person is editing the value themselves. */
11
+ disabled?: boolean;
12
+ }
13
+ /**
14
+ * Flash `.uix-changed` on an element when `value` changes after mount — never on the first
15
+ * render (HAR-2585). A second change restarts the highlight. Compare by `Object.is`: pass a
16
+ * primitive (a status, a count, an `updatedAt`), not a fresh object.
17
+ *
18
+ * ```tsx
19
+ * const ref = useChangeHighlight<HTMLTableCellElement>(row.status, { announce: `Status: ${row.status}` });
20
+ * <Td ref={ref}>{row.status}</Td>
21
+ * ```
22
+ *
23
+ * The hook sets `data-uix-changed` on the element (React never renders that attribute, so a
24
+ * re-render cannot clear it) and removes it when the animation ends.
25
+ */
26
+ declare function useChangeHighlight<T extends HTMLElement = HTMLElement>(value: unknown, { announce, disabled }?: ChangeHighlightOptions): RefObject<T | null>;
27
+ /**
28
+ * Flash `.uix-changed` on `el` now (restarting a highlight already running). For changes that
29
+ * do not flow through a React value, e.g. a row a live update replaced.
30
+ */
31
+ declare function flashChanged(el: HTMLElement): void;
32
+
33
+ export { type ChangeHighlightOptions, flashChanged, useChangeHighlight };
@@ -0,0 +1,44 @@
1
+ "use client";
2
+ import { useEffect, useRef } from "react";
3
+ import { announcePolite, ensureAnnouncer } from "../announce.js";
4
+ function useChangeHighlight(value, { announce, disabled } = {}) {
5
+ const ref = useRef(null);
6
+ const previous = useRef(value);
7
+ const announces = announce !== void 0;
8
+ useEffect(() => {
9
+ if (announces) ensureAnnouncer(ref.current);
10
+ }, [announces]);
11
+ useEffect(() => {
12
+ if (Object.is(previous.current, value)) return;
13
+ previous.current = value;
14
+ const el = ref.current;
15
+ if (disabled || !el) return;
16
+ flashChanged(el);
17
+ if (announce) announcePolite(announce, el);
18
+ }, [value, announce, disabled]);
19
+ return ref;
20
+ }
21
+ const handlers = /* @__PURE__ */ new WeakMap();
22
+ function flashChanged(el) {
23
+ if (el.hasAttribute("data-uix-changed")) {
24
+ el.removeAttribute("data-uix-changed");
25
+ void el.offsetWidth;
26
+ }
27
+ el.setAttribute("data-uix-changed", "");
28
+ const previous = handlers.get(el);
29
+ if (previous) el.removeEventListener("animationend", previous);
30
+ const done = (event) => {
31
+ const target = event.target;
32
+ if (!event.animationName.startsWith("uix-changed")) return;
33
+ if (target !== el && target?.parentNode !== el) return;
34
+ el.removeAttribute("data-uix-changed");
35
+ el.removeEventListener("animationend", done);
36
+ handlers.delete(el);
37
+ };
38
+ handlers.set(el, done);
39
+ el.addEventListener("animationend", done);
40
+ }
41
+ export {
42
+ flashChanged,
43
+ useChangeHighlight
44
+ };
@@ -2,6 +2,11 @@
2
2
  * `keepOpen` is asked when the browser closed the dialog on its own (Escape or a close request
3
3
  * nobody cancelled, a `method="dialog"` form). Returning `true` reopens it instead of calling
4
4
  * `onClose`: a form dialog with unsaved input or a save in flight (HAR-2029).
5
+ *
6
+ * The dialog opens and closes in a layout effect (HAR-1831). A dialog its caller unmounts while
7
+ * open (`{open && <ConfirmDialog …/>}`, or a page that navigates away) is closed in the commit,
8
+ * while it is still in the document, so the browser returns focus to the control that opened it;
9
+ * a passive cleanup ran after the element was removed, and focus fell to `<body>`.
5
10
  */
6
11
  declare function useDialog(open: boolean, onClose?: () => void, keepOpen?: () => boolean): React.RefObject<HTMLDialogElement>;
7
12
 
@@ -1,5 +1,6 @@
1
1
  "use client";
2
- import { useRef, useEffect } from "react";
2
+ import { useRef, useEffect, useLayoutEffect } from "react";
3
+ const useIsomorphicLayoutEffect = typeof document !== "undefined" ? useLayoutEffect : useEffect;
3
4
  let lockCount = 0;
4
5
  let saved = null;
5
6
  function lockBodyScroll() {
@@ -36,7 +37,7 @@ function useDialog(open, onClose, keepOpen) {
36
37
  onCloseRef.current = onClose;
37
38
  const keepOpenRef = useRef(keepOpen);
38
39
  keepOpenRef.current = keepOpen;
39
- useEffect(() => {
40
+ useIsomorphicLayoutEffect(() => {
40
41
  const el = ref.current;
41
42
  if (!el || !open) {
42
43
  if (el && el.open) el.close();