@tensor_1/react 2.30.0 → 2.32.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 (122) hide show
  1. package/README.md +430 -0
  2. package/dist/attachment-labels.d.ts +16 -0
  3. package/dist/attachment-labels.js +9 -0
  4. package/dist/calendar-model.d.ts +141 -1
  5. package/dist/calendar-model.js +200 -11
  6. package/dist/chart-preset.cjs +127 -7
  7. package/dist/chart-preset.d.cts +2 -2
  8. package/dist/chart-preset.d.ts +1 -0
  9. package/dist/chart-preset.js +6 -1
  10. package/dist/chart-theme-Cl0IrkKQ.d.cts +112 -0
  11. package/dist/chart-theme.d.ts +51 -0
  12. package/dist/chart-theme.js +99 -0
  13. package/dist/chart.cjs +127 -7
  14. package/dist/chart.d.cts +2 -2
  15. package/dist/chart.d.ts +1 -0
  16. package/dist/chart.js +6 -2
  17. package/dist/components/Alert.d.ts +17 -1
  18. package/dist/components/Alert.js +9 -4
  19. package/dist/components/AlertDismiss.d.ts +14 -0
  20. package/dist/components/AlertDismiss.js +11 -0
  21. package/dist/components/Attachment.d.ts +58 -0
  22. package/dist/components/Attachment.js +69 -0
  23. package/dist/components/AttachmentParts.d.ts +24 -0
  24. package/dist/components/AttachmentParts.js +20 -0
  25. package/dist/components/Chart.d.ts +1 -0
  26. package/dist/components/Chart.js +6 -1
  27. package/dist/components/ChartCore.d.ts +6 -1
  28. package/dist/components/ChartCore.js +23 -5
  29. package/dist/components/Chip.d.ts +17 -2
  30. package/dist/components/Chip.js +35 -11
  31. package/dist/components/ChipRemove.d.ts +17 -0
  32. package/dist/components/ChipRemove.js +13 -0
  33. package/dist/components/CollapsibleParts.d.ts +29 -0
  34. package/dist/components/CollapsibleParts.js +20 -0
  35. package/dist/components/CollapsibleSection.d.ts +30 -3
  36. package/dist/components/CollapsibleSection.js +7 -11
  37. package/dist/components/CollapsibleSectionState.d.ts +1 -1
  38. package/dist/components/CollapsibleSectionState.js +54 -27
  39. package/dist/components/DashboardGrid.d.ts +60 -0
  40. package/dist/components/DashboardGrid.js +40 -0
  41. package/dist/components/DateRangePicker.d.ts +7 -1
  42. package/dist/components/DateRangePicker.js +5 -1
  43. package/dist/components/DescriptionList.d.ts +12 -1
  44. package/dist/components/DescriptionList.js +5 -5
  45. package/dist/components/Dialogs.d.ts +24 -3
  46. package/dist/components/Dialogs.js +74 -16
  47. package/dist/components/EntityPicker.d.ts +56 -0
  48. package/dist/components/EntityPicker.js +168 -0
  49. package/dist/components/FileIcon.d.ts +9 -0
  50. package/dist/components/FileIcon.js +17 -0
  51. package/dist/components/FileUpload.d.ts +95 -0
  52. package/dist/components/FileUpload.js +145 -0
  53. package/dist/components/FilterEditor.d.ts +55 -0
  54. package/dist/components/FilterEditor.js +186 -0
  55. package/dist/components/Heartbeat.d.ts +31 -0
  56. package/dist/components/Heartbeat.js +28 -0
  57. package/dist/components/Lightbox.d.ts +45 -0
  58. package/dist/components/Lightbox.js +102 -0
  59. package/dist/components/Menu.d.ts +53 -5
  60. package/dist/components/Menu.js +104 -4
  61. package/dist/components/Meter.d.ts +7 -3
  62. package/dist/components/Meter.js +2 -1
  63. package/dist/components/Popover.d.ts +21 -2
  64. package/dist/components/Popover.js +94 -17
  65. package/dist/components/Prose.d.ts +6 -2
  66. package/dist/components/Prose.js +1 -1
  67. package/dist/components/RuleBuilder.d.ts +52 -4
  68. package/dist/components/RuleBuilder.js +109 -24
  69. package/dist/components/SchedulingCalendar.d.ts +142 -11
  70. package/dist/components/SchedulingCalendar.js +234 -57
  71. package/dist/components/SchedulingTimeline.d.ts +1 -0
  72. package/dist/components/SearchSuggest.d.ts +6 -2
  73. package/dist/components/SearchSuggest.js +4 -0
  74. package/dist/components/Segmented.d.ts +17 -3
  75. package/dist/components/Segmented.js +59 -5
  76. package/dist/components/Select.d.ts +66 -2
  77. package/dist/components/Select.js +800 -10
  78. package/dist/components/Spinner.d.ts +2 -1
  79. package/dist/components/Spinner.js +1 -1
  80. package/dist/components/StatusPill.d.ts +8 -2
  81. package/dist/components/StatusPill.js +2 -2
  82. package/dist/components/Steps.d.ts +28 -4
  83. package/dist/components/Steps.js +46 -16
  84. package/dist/components/TableControls.js +4 -3
  85. package/dist/components/Tabs.d.ts +7 -2
  86. package/dist/components/Tabs.js +14 -1
  87. package/dist/components/TextDiff.d.ts +49 -0
  88. package/dist/components/TextDiff.js +124 -0
  89. package/dist/{emoji-image-6mPZ15Zy.d.cts → emoji-image-DjCKpdwE.d.cts} +1 -1
  90. package/dist/emoji.cjs +169 -27
  91. package/dist/emoji.d.cts +3 -3
  92. package/dist/file-model.d.ts +56 -0
  93. package/dist/file-model.js +52 -0
  94. package/dist/filter-model.d.ts +112 -0
  95. package/dist/filter-model.js +103 -0
  96. package/dist/hooks/useAnchoredPosition.d.ts +24 -1
  97. package/dist/hooks/useAnchoredPosition.js +58 -8
  98. package/dist/hooks/useVirtualRows.d.ts +17 -2
  99. package/dist/hooks/useVirtualRows.js +6 -5
  100. package/dist/icons/Icon.d.ts +14 -14
  101. package/dist/index.cjs +4938 -1631
  102. package/dist/index.d.cts +1622 -178
  103. package/dist/index.d.ts +20 -6
  104. package/dist/index.js +88 -4
  105. package/dist/labels-context.d.ts +28 -0
  106. package/dist/{overlay-position-Cjv5E7mn.d.cts → overlay-position-2Afke51l.d.cts} +20 -0
  107. package/dist/overlay-position.d.ts +20 -0
  108. package/dist/overlay-position.js +17 -2
  109. package/dist/rich-text.cjs +169 -27
  110. package/dist/rich-text.d.cts +3 -3
  111. package/dist/rule-builder-model.d.ts +34 -2
  112. package/dist/rule-builder-model.js +48 -12
  113. package/dist/scheduling-calendar-model.d.ts +66 -0
  114. package/dist/scheduling-calendar-model.js +82 -0
  115. package/dist/select-model.d.ts +80 -0
  116. package/dist/select-model.js +109 -0
  117. package/dist/table-engine.d.ts +7 -0
  118. package/dist/table-engine.js +2 -1
  119. package/dist/text-diff-model.d.ts +71 -0
  120. package/dist/text-diff-model.js +261 -0
  121. package/package.json +1 -1
  122. package/dist/ChartCore-DzWe10bN.d.cts +0 -59
package/dist/emoji.cjs CHANGED
@@ -83,7 +83,7 @@ function fitsOn(side, a, f, vp, offset, padding) {
83
83
  return spaceOn(side, a, vp, offset) >= need + padding;
84
84
  }
85
85
  function computePosition(anchor, floating, viewport, options = {}) {
86
- const { placement = "bottom-start", offset = 6, padding = 8, flip = true, shift = true, stickySide } = options;
86
+ const { placement = "bottom-start", offset = 6, padding = 8, flip = true, shift = true, shiftMainAxis = true, stickySide } = options;
87
87
  const { side: preferred, align } = parsePlacement(placement);
88
88
  let side = preferred;
89
89
  const sticky = stickySide === preferred || stickySide === OPPOSITE[preferred] ? stickySide : void 0;
@@ -116,12 +116,27 @@ function computePosition(anchor, floating, viewport, options = {}) {
116
116
  y = align === "start" ? start : align === "end" ? end : center;
117
117
  if (shift) y = clamp(y, padding, Math.max(padding, viewport.height - floating.height - padding));
118
118
  }
119
+ const mainAxisNatural = horizontal ? y : x;
120
+ const anchorInView = horizontal ? anchor.y + anchor.height > 0 && anchor.y < viewport.height : anchor.x + anchor.width > 0 && anchor.x < viewport.width;
121
+ let mainAxisRange = null;
122
+ if (shiftMainAxis && anchorInView) {
123
+ const extent = horizontal ? viewport.height - floating.height : viewport.width - floating.width;
124
+ mainAxisRange = [padding, Math.max(padding, extent - padding)];
125
+ if (horizontal) y = clamp(y, mainAxisRange[0], mainAxisRange[1]);
126
+ else x = clamp(x, mainAxisRange[0], mainAxisRange[1]);
127
+ }
119
128
  return {
120
129
  x,
121
130
  y,
122
131
  placement: align === "center" ? side : `${side}-${align}`,
123
132
  side,
124
- align
133
+ align,
134
+ mainAxisNatural,
135
+ mainAxisRange,
136
+ available: {
137
+ width: Math.max(0, viewport.width - 2 * padding),
138
+ height: Math.max(0, viewport.height - 2 * padding)
139
+ }
125
140
  };
126
141
  }
127
142
 
@@ -140,6 +155,7 @@ function supportsAnchoring() {
140
155
  }
141
156
  return anchoringSupported;
142
157
  }
158
+ var ANCHOR_BAND = 64;
143
159
  var anchorCount = 0;
144
160
  var namesOf = (value) => value.split(",").map((n) => n.trim()).filter((n) => n.startsWith("--"));
145
161
  function addAnchorName(el, name) {
@@ -164,9 +180,12 @@ function translationOf(el) {
164
180
  }
165
181
  var signed = (n) => `${n < 0 ? "-" : "+"} ${Math.round(Math.abs(n) * 100) / 100}px`;
166
182
  var prepared = /* @__PURE__ */ new WeakSet();
167
- function useAnchoredPosition(anchor, floatingRef, { open, placement = "bottom-start", offset = 6, padding = 8 }) {
168
- const opts = (0, import_react.useRef)({ placement, offset, padding });
169
- opts.current = { placement, offset, padding };
183
+ function useAnchoredPosition(anchor, floatingRef, { open, placement = "bottom-start", offset = 6, padding = 8, shiftMainAxis = true, capHeight = false, onAnchorHidden }) {
184
+ const opts = (0, import_react.useRef)({ placement, offset, padding, shiftMainAxis, capHeight });
185
+ opts.current = { placement, offset, padding, shiftMainAxis, capHeight };
186
+ const onAnchorHiddenRef = (0, import_react.useRef)(onAnchorHidden);
187
+ onAnchorHiddenRef.current = onAnchorHidden;
188
+ const heightCap = (0, import_react.useRef)(null);
170
189
  const session = (0, import_react.useRef)(false);
171
190
  const side = (0, import_react.useRef)(null);
172
191
  const written = (0, import_react.useRef)("");
@@ -190,6 +209,15 @@ function useAnchoredPosition(anchor, floatingRef, { open, placement = "bottom-st
190
209
  }
191
210
  removeAnchorName(current.anchor, current.name);
192
211
  }, [floatingRef]);
212
+ const uncap = (0, import_react.useCallback)(() => {
213
+ const cap = heightCap.current;
214
+ if (!cap) return;
215
+ heightCap.current = null;
216
+ const floating = floatingRef.current;
217
+ if (!floating) return;
218
+ floating.style.maxHeight = cap.inlineMaxHeight;
219
+ if (cap.setOverflow) floating.style.overflowY = cap.inlineOverflowY;
220
+ }, [floatingRef]);
193
221
  const reposition = (0, import_react.useCallback)(() => {
194
222
  const anchorEl = resolveAnchor(anchor);
195
223
  const floating = floatingRef.current;
@@ -202,14 +230,36 @@ function useAnchoredPosition(anchor, floatingRef, { open, placement = "bottom-st
202
230
  prepared.add(floating);
203
231
  written.current = "";
204
232
  }
233
+ const { placement: p, offset: o, padding: pad, shiftMainAxis: keepInView, capHeight: cap } = opts.current;
234
+ if (cap) {
235
+ if (!heightCap.current) {
236
+ const own = getComputedStyle(floating);
237
+ const setOverflow = !own.overflowY || own.overflowY === "visible";
238
+ heightCap.current = {
239
+ inlineMaxHeight: s.maxHeight,
240
+ inlineOverflowY: s.overflowY,
241
+ ownMaxHeight: own.maxHeight && own.maxHeight !== "none" ? own.maxHeight : "",
242
+ setOverflow,
243
+ written: ""
244
+ };
245
+ if (setOverflow) s.overflowY = "auto";
246
+ }
247
+ const room = `${Math.max(0, window.innerHeight - 2 * pad)}px`;
248
+ const max = heightCap.current.ownMaxHeight ? `min(${room}, ${heightCap.current.ownMaxHeight})` : room;
249
+ if (max !== heightCap.current.written) {
250
+ s.maxHeight = max;
251
+ heightCap.current.written = max;
252
+ }
253
+ } else if (heightCap.current) {
254
+ uncap();
255
+ }
205
256
  const a = anchorEl.getBoundingClientRect();
206
257
  const size = { width: floating.offsetWidth, height: floating.offsetHeight };
207
- const { placement: p, offset: o, padding: pad } = opts.current;
208
258
  const pos = computePosition(
209
259
  { x: a.x, y: a.y, width: a.width, height: a.height },
210
260
  size,
211
261
  { width: window.innerWidth, height: window.innerHeight },
212
- { placement: p, offset: o, padding: pad, stickySide: side.current ?? void 0 }
262
+ { placement: p, offset: o, padding: pad, shiftMainAxis: keepInView, stickySide: side.current ?? void 0 }
213
263
  );
214
264
  const measured = size.width > 0 || size.height > 0;
215
265
  if (measured && session.current) side.current = pos.side;
@@ -223,7 +273,10 @@ function useAnchoredPosition(anchor, floatingRef, { open, placement = "bottom-st
223
273
  s.top = top2;
224
274
  written.current = key2;
225
275
  };
226
- if (anchoringFailed.current || !supportsAnchoring()) {
276
+ const limit = pos.mainAxisRange;
277
+ const nearLimit = limit !== null && (pos.mainAxisNatural < limit[0] + ANCHOR_BAND || pos.mainAxisNatural > limit[1] - ANCHOR_BAND);
278
+ if (anchoringFailed.current || nearLimit || !supportsAnchoring()) {
279
+ if (anchoring.current) release(false);
227
280
  writeFixed(pos);
228
281
  return;
229
282
  }
@@ -260,7 +313,7 @@ function useAnchoredPosition(anchor, floatingRef, { open, placement = "bottom-st
260
313
  writeFixed(pos);
261
314
  }
262
315
  }
263
- }, [anchor, floatingRef, release]);
316
+ }, [anchor, floatingRef, release, uncap]);
264
317
  useIsomorphicLayoutEffect(() => {
265
318
  if (!open) return;
266
319
  session.current = true;
@@ -289,8 +342,20 @@ function useAnchoredPosition(anchor, floatingRef, { open, placement = "bottom-st
289
342
  side.current = null;
290
343
  anchoringFailed.current = false;
291
344
  release(true);
345
+ uncap();
292
346
  };
293
- }, [open, reposition, anchor, floatingRef, release]);
347
+ }, [open, reposition, anchor, floatingRef, release, uncap]);
348
+ const watchAnchor = open && !!onAnchorHidden;
349
+ (0, import_react.useEffect)(() => {
350
+ const anchorEl = resolveAnchor(anchor);
351
+ if (!watchAnchor || !anchorEl || typeof IntersectionObserver !== "function") return;
352
+ const observer = new IntersectionObserver((entries) => {
353
+ const latest = entries[entries.length - 1];
354
+ if (latest && !latest.isIntersecting) onAnchorHiddenRef.current?.();
355
+ });
356
+ observer.observe(anchorEl);
357
+ return () => observer.disconnect();
358
+ }, [watchAnchor, anchor]);
294
359
  (0, import_react.useEffect)(() => () => release(false), [release]);
295
360
  return reposition;
296
361
  }
@@ -312,16 +377,22 @@ var settle = (el) => {
312
377
  el.removeAttribute(PLACING);
313
378
  };
314
379
  function Popover({
315
- popover = "auto",
380
+ popover: popoverProp,
316
381
  anchor,
317
382
  placement = "bottom-start",
318
383
  offset = 6,
319
384
  closeWhenAnchorHidden = false,
320
385
  onAnchorHidden,
386
+ capHeight = false,
387
+ openOnHover = false,
321
388
  className,
322
389
  children,
323
390
  ...props
324
391
  }) {
392
+ const hover = !!openOnHover && !!anchor;
393
+ const popover = popoverProp ?? (hover ? "manual" : "auto");
394
+ const openDelay = typeof openOnHover === "object" ? openOnHover.openDelay ?? 300 : 300;
395
+ const closeDelay = typeof openOnHover === "object" ? openOnHover.closeDelay ?? 150 : 150;
325
396
  const ref = (0, import_react2.useRef)(null);
326
397
  const [open, setOpen] = (0, import_react2.useState)(false);
327
398
  const openRef = (0, import_react2.useRef)(false);
@@ -331,7 +402,16 @@ function Popover({
331
402
  const reposition = useAnchoredPosition(anchor ?? null, ref, {
332
403
  open: open && !!anchor,
333
404
  placement,
334
- offset
405
+ offset,
406
+ capHeight,
407
+ onAnchorHidden: closeWhenAnchorHidden ? () => {
408
+ const el = ref.current;
409
+ if (!el || !isPopoverOpen(el)) return;
410
+ onAnchorHiddenRef.current?.();
411
+ const active = el.ownerDocument.activeElement;
412
+ if (active && el.contains(active)) active.blur();
413
+ el.hidePopover();
414
+ } : void 0
335
415
  });
336
416
  (0, import_react2.useEffect)(() => {
337
417
  const el = ref.current;
@@ -366,25 +446,87 @@ function Popover({
366
446
  el.removeAttribute(PLACING);
367
447
  };
368
448
  }, [anchor, reposition]);
449
+ (0, import_react2.useEffect)(() => {
450
+ const el = ref.current;
451
+ const anchorEl = resolveAnchor(anchor);
452
+ if (!hover || !el || !anchorEl) return;
453
+ let timer;
454
+ const inside = (node) => !!node && (anchorEl.contains(node) || el.contains(node));
455
+ const stillWanted = () => {
456
+ try {
457
+ return anchorEl.matches(":hover") || el.matches(":hover") || inside(el.ownerDocument.activeElement);
458
+ } catch {
459
+ return false;
460
+ }
461
+ };
462
+ const show = (delay) => {
463
+ window.clearTimeout(timer);
464
+ timer = window.setTimeout(() => {
465
+ if (!isPopoverOpen(el)) {
466
+ try {
467
+ el.showPopover?.();
468
+ } catch {
469
+ }
470
+ }
471
+ }, delay);
472
+ };
473
+ const hide = (delay) => {
474
+ window.clearTimeout(timer);
475
+ timer = window.setTimeout(() => {
476
+ if (isPopoverOpen(el) && !stillWanted()) {
477
+ try {
478
+ el.hidePopover?.();
479
+ } catch {
480
+ }
481
+ }
482
+ }, delay);
483
+ };
484
+ const onEnter = (event) => {
485
+ if (event.pointerType !== "touch") show(openDelay);
486
+ };
487
+ const onLeave = (event) => {
488
+ if (event.pointerType !== "touch") hide(closeDelay);
489
+ };
490
+ const onFocusIn = () => show(0);
491
+ const onFocusOut = (event) => {
492
+ if (!inside(event.relatedTarget)) hide(closeDelay);
493
+ };
494
+ const onKeyDown = (event) => {
495
+ if (event.key !== "Escape" || !isPopoverOpen(el)) return;
496
+ window.clearTimeout(timer);
497
+ const hadFocus = el.contains(el.ownerDocument.activeElement);
498
+ try {
499
+ el.hidePopover?.();
500
+ } catch {
501
+ }
502
+ if (hadFocus) {
503
+ anchorEl.removeEventListener("focusin", onFocusIn);
504
+ anchorEl.focus();
505
+ anchorEl.addEventListener("focusin", onFocusIn);
506
+ }
507
+ };
508
+ for (const node of [anchorEl, el]) {
509
+ node.addEventListener("pointerenter", onEnter);
510
+ node.addEventListener("pointerleave", onLeave);
511
+ node.addEventListener("focusout", onFocusOut);
512
+ node.addEventListener("keydown", onKeyDown);
513
+ }
514
+ anchorEl.addEventListener("focusin", onFocusIn);
515
+ return () => {
516
+ window.clearTimeout(timer);
517
+ for (const node of [anchorEl, el]) {
518
+ node.removeEventListener("pointerenter", onEnter);
519
+ node.removeEventListener("pointerleave", onLeave);
520
+ node.removeEventListener("focusout", onFocusOut);
521
+ node.removeEventListener("keydown", onKeyDown);
522
+ }
523
+ anchorEl.removeEventListener("focusin", onFocusIn);
524
+ };
525
+ }, [hover, anchor, openDelay, closeDelay]);
369
526
  useIsomorphicLayoutEffect2(() => {
370
527
  openRef.current = open;
371
528
  if (open && ref.current) settle(ref.current);
372
529
  }, [open]);
373
- (0, import_react2.useEffect)(() => {
374
- const el = ref.current;
375
- const anchorEl = resolveAnchor(anchor);
376
- if (!closeWhenAnchorHidden || !open || !el || !anchorEl || typeof IntersectionObserver !== "function") return;
377
- const observer = new IntersectionObserver((entries) => {
378
- const latest = entries[entries.length - 1];
379
- if (!latest || latest.isIntersecting || !isPopoverOpen(el)) return;
380
- onAnchorHiddenRef.current?.();
381
- const active = el.ownerDocument.activeElement;
382
- if (active && el.contains(active)) active.blur();
383
- el.hidePopover();
384
- });
385
- observer.observe(anchorEl);
386
- return () => observer.disconnect();
387
- }, [closeWhenAnchorHidden, open, anchor]);
388
530
  return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { ref, className: cx("uix-popover", className), ...popoverAttr, ...props, children });
389
531
  }
390
532
 
package/dist/emoji.d.cts CHANGED
@@ -1,6 +1,6 @@
1
- import { E as EmojiPickerLabels, a as EmojiLocale, b as EmojiDataLoader } from './emoji-image-6mPZ15Zy.cjs';
2
- export { C as CompactEmoji, D as DEFAULT_EMOJI_PICKER_LABELS, c as EMOJI_GRID_COLUMNS, d as EmojiData, e as EmojiEntry, f as EmojiGroup, g as EmojiMessages, h as EmojiPicker, i as EmojiPickerProps, j as buildEmojiData, k as canRenderEmoji, l as emojiImageFileName, m as loadEmojiData, n as normalizeEmojiImageBaseUrl, s as searchEmoji } from './emoji-image-6mPZ15Zy.cjs';
3
- export { A as Align, P as Placement, S as Side } from './overlay-position-Cjv5E7mn.cjs';
1
+ import { E as EmojiPickerLabels, a as EmojiLocale, b as EmojiDataLoader } from './emoji-image-DjCKpdwE.cjs';
2
+ export { C as CompactEmoji, D as DEFAULT_EMOJI_PICKER_LABELS, c as EMOJI_GRID_COLUMNS, d as EmojiData, e as EmojiEntry, f as EmojiGroup, g as EmojiMessages, h as EmojiPicker, i as EmojiPickerProps, j as buildEmojiData, k as canRenderEmoji, l as emojiImageFileName, m as loadEmojiData, n as normalizeEmojiImageBaseUrl, s as searchEmoji } from './emoji-image-DjCKpdwE.cjs';
3
+ export { A as Align, P as Placement, S as Side } from './overlay-position-2Afke51l.cjs';
4
4
  import * as react from 'react';
5
5
 
6
6
  interface ReactionSummary {
@@ -0,0 +1,56 @@
1
+ /** File helpers shared by `FileUpload` and `Attachment` (HAR-984/985). Pure. */
2
+ interface FileSizeOptions {
3
+ /**
4
+ * `1000` (default): decimal, as file managers on macOS and most of the web show sizes
5
+ * (1 MB = 1,000,000 bytes). `1024`: binary, as Windows and many servers count them
6
+ * (1 MB = 1,048,576 bytes) — use it when the rest of the product does, so one file never
7
+ * shows two sizes (HAR-1630).
8
+ */
9
+ base?: 1000 | 1024;
10
+ /**
11
+ * The five unit labels, bytes first. Default `['B', 'KB', 'MB', 'GB', 'TB']` for both bases;
12
+ * pass `['B', 'KiB', 'MiB', 'GiB', 'TiB']` to mark base 1024 the IEC way, or localised ones.
13
+ */
14
+ units?: readonly [string, string, string, string, string];
15
+ }
16
+ /** A product's own size formatter for `FileUpload` and `Attachment` (`formatSize`). */
17
+ type FileSizeFormatter = (bytes: number, locale?: string) => string;
18
+ /**
19
+ * "1.2 MB", "340 KB", "12 B" in the locale's number format. Base 1000 unless `options.base`
20
+ * is 1024.
21
+ */
22
+ declare function formatFileSize(bytes: number, locale?: string, options?: FileSizeOptions): string;
23
+ /** Whether a file matches an `accept` list (".pdf,image/*,application/zip"). An empty list accepts all. */
24
+ declare function fileMatchesAccept(file: {
25
+ name: string;
26
+ type?: string;
27
+ }, accept?: string): boolean;
28
+ type FileRejectionReason = 'type' | 'size' | 'count';
29
+ interface FileRejection<F = File> {
30
+ file: F;
31
+ reason: FileRejectionReason;
32
+ }
33
+ /**
34
+ * Splits picked files into accepted and rejected (wrong type, over `maxSize` bytes, or beyond
35
+ * `maxFiles` counting the `existing` ones). Order is kept.
36
+ */
37
+ declare function partitionFiles<F extends {
38
+ name: string;
39
+ type?: string;
40
+ size: number;
41
+ }>(files: readonly F[], { accept, maxSize, maxFiles, existing }: {
42
+ accept?: string;
43
+ maxSize?: number;
44
+ maxFiles?: number;
45
+ existing?: number;
46
+ }): {
47
+ accepted: F[];
48
+ rejected: FileRejection<F>[];
49
+ };
50
+ /** A coarse kind for a file icon. */
51
+ declare function fileKind(file: {
52
+ name: string;
53
+ type?: string;
54
+ }): 'image' | 'video' | 'pdf' | 'sheet' | 'archive' | 'text' | 'other';
55
+
56
+ export { type FileRejection, type FileRejectionReason, type FileSizeFormatter, type FileSizeOptions, fileKind, fileMatchesAccept, formatFileSize, partitionFiles };
@@ -0,0 +1,52 @@
1
+ const UNITS = ["B", "KB", "MB", "GB", "TB"];
2
+ function formatFileSize(bytes, locale, options = {}) {
3
+ if (!Number.isFinite(bytes) || bytes < 0) return "";
4
+ const base = options.base === 1024 ? 1024 : 1e3;
5
+ const units = options.units ?? UNITS;
6
+ let value = bytes;
7
+ let unit = 0;
8
+ while (value >= base && unit < units.length - 1) {
9
+ value /= base;
10
+ unit += 1;
11
+ }
12
+ const digits = unit === 0 || value >= 100 ? 0 : 1;
13
+ return `${new Intl.NumberFormat(locale, { maximumFractionDigits: digits, minimumFractionDigits: 0 }).format(value)} ${units[unit]}`;
14
+ }
15
+ function fileMatchesAccept(file, accept) {
16
+ if (!accept || !accept.trim()) return true;
17
+ const name = file.name.toLowerCase();
18
+ const type = (file.type ?? "").toLowerCase();
19
+ return accept.split(",").map((t) => t.trim().toLowerCase()).filter(Boolean).some((token) => {
20
+ if (token.startsWith(".")) return name.endsWith(token);
21
+ if (token.endsWith("/*")) return type.startsWith(token.slice(0, -1));
22
+ return type === token;
23
+ });
24
+ }
25
+ function partitionFiles(files, { accept, maxSize, maxFiles, existing = 0 }) {
26
+ const accepted = [];
27
+ const rejected = [];
28
+ for (const file of files) {
29
+ if (!fileMatchesAccept(file, accept)) rejected.push({ file, reason: "type" });
30
+ else if (maxSize !== void 0 && file.size > maxSize) rejected.push({ file, reason: "size" });
31
+ else if (maxFiles !== void 0 && existing + accepted.length >= maxFiles) rejected.push({ file, reason: "count" });
32
+ else accepted.push(file);
33
+ }
34
+ return { accepted, rejected };
35
+ }
36
+ function fileKind(file) {
37
+ const type = (file.type ?? "").toLowerCase();
38
+ const ext = file.name.toLowerCase().split(".").pop() ?? "";
39
+ if (type.startsWith("image/") || ["png", "jpg", "jpeg", "gif", "webp", "svg", "heic"].includes(ext)) return "image";
40
+ if (type.startsWith("video/") || ["mp4", "mov", "webm"].includes(ext)) return "video";
41
+ if (type === "application/pdf" || ext === "pdf") return "pdf";
42
+ if (["xls", "xlsx", "csv", "ods"].includes(ext) || type.includes("spreadsheet") || type === "text/csv") return "sheet";
43
+ if (["zip", "7z", "gz", "tar", "rar"].includes(ext) || type.includes("zip")) return "archive";
44
+ if (type.startsWith("text/") || ["txt", "md", "log", "json", "xml", "doc", "docx", "odt", "rtf"].includes(ext)) return "text";
45
+ return "other";
46
+ }
47
+ export {
48
+ fileKind,
49
+ fileMatchesAccept,
50
+ formatFileSize,
51
+ partitionFiles
52
+ };
@@ -0,0 +1,112 @@
1
+ /**
2
+ * Typed column filters for `FilterEditor` (HAR-1365; TENSOR C6 `table-filter-editor.tsx`,
3
+ * MOTUS C-1 "+ filter"). The value is data the product serializes into its own query; the
4
+ * summary feeds a `Chip`. Pure, so a server can summarize a saved view too.
5
+ */
6
+ interface FilterChoice {
7
+ value: string;
8
+ label: string;
9
+ }
10
+ type TextFilterOperator = 'contains' | 'not-contains' | 'is' | 'is-not' | 'starts-with';
11
+ type NumberFilterOperator = 'eq' | 'neq' | 'lt' | 'lte' | 'gt' | 'gte' | 'between';
12
+ type FilterValue = {
13
+ kind: 'enum';
14
+ values: string[];
15
+ } | {
16
+ kind: 'text';
17
+ operator: TextFilterOperator;
18
+ text: string;
19
+ } | {
20
+ kind: 'number';
21
+ operator: NumberFilterOperator;
22
+ value?: number;
23
+ to?: number;
24
+ } | {
25
+ kind: 'date-range';
26
+ from?: string;
27
+ to?: string;
28
+ } | {
29
+ kind: 'boolean';
30
+ value?: boolean;
31
+ } | {
32
+ kind: 'reference';
33
+ values: FilterChoice[];
34
+ };
35
+ type FilterValueKind = FilterValue['kind'];
36
+ /**
37
+ * A named selection for an `enum` field (HAR-1505), e.g. "Open work" = new, open and pending.
38
+ * The id, label and values come from the product.
39
+ */
40
+ interface FilterPreset {
41
+ id: string;
42
+ label: string;
43
+ /** The option values the preset selects. Compared as a set: order and repeats do not matter. */
44
+ values: readonly string[];
45
+ }
46
+ interface FilterField {
47
+ id: string;
48
+ /** Column name, also the start of the chip summary. */
49
+ label: string;
50
+ kind: FilterValueKind;
51
+ /** `enum`: the choices. */
52
+ options?: readonly FilterChoice[];
53
+ /** `reference`: finds records to pick; rejected promises show the error state. */
54
+ onSearch?: (query: string) => Promise<readonly FilterChoice[]>;
55
+ /** `text` / `number`: the operators offered, in order. Default: all of the kind. */
56
+ operators?: readonly (TextFilterOperator | NumberFilterOperator)[];
57
+ /** `number`: a unit shown after the value, e.g. "days". */
58
+ unit?: string;
59
+ /** `boolean`: words for true and false, e.g. Escalated / Not escalated. Default from labels. */
60
+ trueLabel?: string;
61
+ falseLabel?: string;
62
+ /**
63
+ * `enum`: named selections shown as toggle chips above the options. Activating one replaces
64
+ * the selection with its values; the one equal to the selection is pressed, and the chip
65
+ * summary names it.
66
+ */
67
+ presets?: readonly FilterPreset[];
68
+ }
69
+ interface FilterSummaryLabels {
70
+ operators: Record<TextFilterOperator | NumberFilterOperator, string>;
71
+ yes: string;
72
+ no: string;
73
+ /** `{from}`, `{to}`; also used for a number range. */
74
+ range: string;
75
+ /** Only a start date. `{from}`. */
76
+ from: string;
77
+ /** Only an end date. `{to}`. */
78
+ until: string;
79
+ /** More than `maxListed` values: `{first}`, `{count}`. */
80
+ more: string;
81
+ /** An `enum` value equal to a preset: `{label}` (the preset), `{field}`. Default `{label}`. */
82
+ preset?: string;
83
+ }
84
+ declare const DEFAULT_FILTER_SUMMARY_LABELS: FilterSummaryLabels;
85
+ declare const TEXT_FILTER_OPERATORS: readonly TextFilterOperator[];
86
+ declare const NUMBER_FILTER_OPERATORS: readonly NumberFilterOperator[];
87
+ /** The empty value for a field kind (what Clear resets to). */
88
+ declare function emptyFilterValue(kind: FilterValueKind): FilterValue;
89
+ /** True when the value filters nothing (so the chip goes away). */
90
+ declare function isFilterEmpty(value: FilterValue | undefined): boolean;
91
+ /**
92
+ * The first of `field.presets` whose values equal the value's as a set, or undefined. Only an
93
+ * `enum` value can match a preset.
94
+ */
95
+ declare function matchFilterPreset(field: FilterField, value: FilterValue | undefined): FilterPreset | undefined;
96
+ interface SummarizeFilterOptions {
97
+ labels?: Partial<FilterSummaryLabels>;
98
+ /** Formats a `YYYY-MM-DD` date (a product format such as DD.MM.YYYY). Default: as given. */
99
+ formatDate?: (date: string) => string;
100
+ /** Formats a number. Default: `String`. */
101
+ formatNumber?: (value: number) => string;
102
+ /** List this many values before "+N". Default 2. */
103
+ maxListed?: number;
104
+ }
105
+ /**
106
+ * 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`
108
+ * gives that preset's label (the `preset` label). Empty values give an empty string.
109
+ */
110
+ declare function summarizeFilter(field: FilterField, value: FilterValue | undefined, options?: SummarizeFilterOptions): string;
111
+
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 };
@@ -0,0 +1,103 @@
1
+ const DEFAULT_FILTER_SUMMARY_LABELS = {
2
+ operators: {
3
+ contains: "contains",
4
+ "not-contains": "does not contain",
5
+ is: "is",
6
+ "is-not": "is not",
7
+ "starts-with": "starts with",
8
+ eq: "=",
9
+ neq: "\u2260",
10
+ lt: "<",
11
+ lte: "\u2264",
12
+ gt: ">",
13
+ gte: "\u2265",
14
+ between: "between"
15
+ },
16
+ yes: "Yes",
17
+ no: "No",
18
+ range: "{from} \u2013 {to}",
19
+ from: "from {from}",
20
+ until: "until {to}",
21
+ more: "{first} +{count}",
22
+ preset: "{label}"
23
+ };
24
+ const TEXT_FILTER_OPERATORS = ["contains", "not-contains", "is", "is-not", "starts-with"];
25
+ const NUMBER_FILTER_OPERATORS = ["eq", "neq", "lt", "lte", "gt", "gte", "between"];
26
+ function emptyFilterValue(kind) {
27
+ switch (kind) {
28
+ case "enum":
29
+ return { kind, values: [] };
30
+ case "text":
31
+ return { kind, operator: "contains", text: "" };
32
+ case "number":
33
+ return { kind, operator: "eq" };
34
+ case "date-range":
35
+ return { kind };
36
+ case "boolean":
37
+ return { kind };
38
+ case "reference":
39
+ return { kind, values: [] };
40
+ }
41
+ }
42
+ function isFilterEmpty(value) {
43
+ if (!value) return true;
44
+ switch (value.kind) {
45
+ case "enum":
46
+ case "reference":
47
+ return value.values.length === 0;
48
+ case "text":
49
+ return value.text.trim() === "";
50
+ case "number":
51
+ return value.value === void 0 || value.operator === "between" && value.to === void 0;
52
+ case "date-range":
53
+ return !value.from && !value.to;
54
+ case "boolean":
55
+ return value.value === void 0;
56
+ }
57
+ }
58
+ function matchFilterPreset(field, value) {
59
+ if (value?.kind !== "enum" || !field.presets?.length) return void 0;
60
+ const chosen = new Set(value.values);
61
+ return field.presets.find((preset) => {
62
+ const wanted = new Set(preset.values);
63
+ return wanted.size === chosen.size && [...wanted].every((v) => chosen.has(v));
64
+ });
65
+ }
66
+ const fill = (template, values) => template.replace(/\{(\w+)\}/g, (match, key) => key in values ? String(values[key]) : match);
67
+ function summarizeFilter(field, value, options = {}) {
68
+ if (!value || isFilterEmpty(value)) return "";
69
+ const labels = { ...DEFAULT_FILTER_SUMMARY_LABELS, ...options.labels, operators: { ...DEFAULT_FILTER_SUMMARY_LABELS.operators, ...options.labels?.operators } };
70
+ const date = options.formatDate ?? ((d) => d);
71
+ const num = (n) => `${options.formatNumber?.(n) ?? String(n)}${field.unit ? ` ${field.unit}` : ""}`;
72
+ const list = (items) => {
73
+ const max = Math.max(1, options.maxListed ?? 2);
74
+ return items.length > max ? fill(labels.more, { first: items.slice(0, max).join(", "), count: items.length - max }) : items.join(", ");
75
+ };
76
+ switch (value.kind) {
77
+ case "enum": {
78
+ const preset = matchFilterPreset(field, value);
79
+ if (preset) return fill(labels.preset ?? DEFAULT_FILTER_SUMMARY_LABELS.preset, { label: preset.label, field: field.label });
80
+ const byValue = new Map((field.options ?? []).map((o) => [o.value, o.label]));
81
+ return `${field.label}: ${list(value.values.map((v) => byValue.get(v) ?? v))}`;
82
+ }
83
+ case "reference":
84
+ return `${field.label}: ${list(value.values.map((v) => v.label))}`;
85
+ case "text":
86
+ return `${field.label} ${labels.operators[value.operator]} \u201C${value.text.trim()}\u201D`;
87
+ case "number":
88
+ 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
+ case "date-range":
90
+ 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) })}`;
91
+ case "boolean":
92
+ return `${field.label}: ${value.value ? field.trueLabel ?? labels.yes : field.falseLabel ?? labels.no}`;
93
+ }
94
+ }
95
+ export {
96
+ DEFAULT_FILTER_SUMMARY_LABELS,
97
+ NUMBER_FILTER_OPERATORS,
98
+ TEXT_FILTER_OPERATORS,
99
+ emptyFilterValue,
100
+ isFilterEmpty,
101
+ matchFilterPreset,
102
+ summarizeFilter
103
+ };
@@ -7,6 +7,25 @@ interface UseAnchoredPositionOptions {
7
7
  placement?: Placement;
8
8
  offset?: number;
9
9
  padding?: number;
10
+ /**
11
+ * Keep the element inside the viewport on the side it hangs off as well: when it fits neither
12
+ * above nor below (or left nor right) it slides over the anchor instead of leaving the
13
+ * viewport. Once the anchor itself has left the viewport the element follows it out; pair
14
+ * that with `onAnchorHidden` to close it. Default true.
15
+ */
16
+ shiftMainAxis?: boolean;
17
+ /**
18
+ * Cap the element's height to the viewport minus the padding on both edges, and let it scroll
19
+ * (`overflow-y: auto`, unless its own CSS already sets an overflow). A `max-height` from the
20
+ * element's own CSS still applies when it is smaller. Default false.
21
+ */
22
+ capHeight?: boolean;
23
+ /**
24
+ * Called when the anchor has scrolled completely out of view (outside the viewport or a
25
+ * clipping ancestor) while `open`. Close the overlay here; `Popover closeWhenAnchorHidden`
26
+ * is built on it.
27
+ */
28
+ onAnchorHidden?: () => void;
10
29
  }
11
30
  type AnchorArg = RefObject<HTMLElement | null> | HTMLElement | null | undefined;
12
31
  /** @internal The element behind an anchor ref or element. */
@@ -22,9 +41,13 @@ declare const resolveAnchor: (a: AnchorArg) => HTMLElement | null;
22
41
  * `position: fixed` + left/top and follows scroll/resize from JS. While `open`, the side
23
42
  * chosen at open is kept until it no longer fits and the other side does, scrolls inside
24
43
  * the overlay itself are ignored, and a size change of either element re-places it.
44
+ * An element that fits on neither side is kept inside the viewport (it covers the anchor).
45
+ * While it is held there, or within `ANCHOR_BAND` of the limit, it is written as fixed
46
+ * coordinates even where anchor positioning exists: those cannot drift while the page scrolls,
47
+ * so it is inside on every frame, not one frame later (HAR-1613).
25
48
  * Returns a `reposition` fn for callers that want to nudge it manually (e.g. right when a
26
49
  * native popover's `toggle` fires).
27
50
  */
28
- declare function useAnchoredPosition(anchor: RefObject<HTMLElement | null> | HTMLElement | null | undefined, floatingRef: RefObject<HTMLElement | null>, { open, placement, offset, padding }: UseAnchoredPositionOptions): () => void;
51
+ declare function useAnchoredPosition(anchor: RefObject<HTMLElement | null> | HTMLElement | null | undefined, floatingRef: RefObject<HTMLElement | null>, { open, placement, offset, padding, shiftMainAxis, capHeight, onAnchorHidden }: UseAnchoredPositionOptions): () => void;
29
52
 
30
53
  export { type UseAnchoredPositionOptions, resolveAnchor, useAnchoredPosition };