@tensor_1/react 2.31.0 → 2.33.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 (117) hide show
  1. package/README.md +367 -0
  2. package/dist/SchedulingCalendar-C4XVbqZr.d.ts +339 -0
  3. package/dist/attachment-labels.d.ts +16 -0
  4. package/dist/attachment-labels.js +9 -0
  5. package/dist/calendar-model.d.ts +141 -1
  6. package/dist/calendar-model.js +200 -11
  7. package/dist/chart-preset.cjs +127 -7
  8. package/dist/chart-preset.d.cts +2 -2
  9. package/dist/chart-preset.d.ts +1 -0
  10. package/dist/chart-preset.js +6 -1
  11. package/dist/chart-theme-Cl0IrkKQ.d.cts +112 -0
  12. package/dist/chart-theme.d.ts +51 -0
  13. package/dist/chart-theme.js +99 -0
  14. package/dist/chart.cjs +127 -7
  15. package/dist/chart.d.cts +2 -2
  16. package/dist/chart.d.ts +1 -0
  17. package/dist/chart.js +6 -2
  18. package/dist/components/Alert.d.ts +17 -1
  19. package/dist/components/Alert.js +9 -4
  20. package/dist/components/AlertDismiss.d.ts +14 -0
  21. package/dist/components/AlertDismiss.js +11 -0
  22. package/dist/components/Attachment.d.ts +15 -12
  23. package/dist/components/Attachment.js +17 -17
  24. package/dist/components/AttachmentParts.d.ts +24 -0
  25. package/dist/components/AttachmentParts.js +20 -0
  26. package/dist/components/Chart.d.ts +1 -0
  27. package/dist/components/Chart.js +6 -1
  28. package/dist/components/ChartCore.d.ts +6 -1
  29. package/dist/components/ChartCore.js +23 -5
  30. package/dist/components/Chip.d.ts +17 -2
  31. package/dist/components/Chip.js +35 -11
  32. package/dist/components/ChipRemove.d.ts +17 -0
  33. package/dist/components/ChipRemove.js +13 -0
  34. package/dist/components/CollapsibleParts.d.ts +29 -0
  35. package/dist/components/CollapsibleParts.js +20 -0
  36. package/dist/components/CollapsibleSection.d.ts +30 -3
  37. package/dist/components/CollapsibleSection.js +7 -11
  38. package/dist/components/CollapsibleSectionState.d.ts +1 -1
  39. package/dist/components/CollapsibleSectionState.js +54 -27
  40. package/dist/components/DashboardGrid.d.ts +60 -0
  41. package/dist/components/DashboardGrid.js +40 -0
  42. package/dist/components/DateRangePicker.d.ts +7 -1
  43. package/dist/components/DateRangePicker.js +5 -1
  44. package/dist/components/DescriptionList.d.ts +12 -1
  45. package/dist/components/DescriptionList.js +5 -5
  46. package/dist/components/Dialogs.d.ts +24 -3
  47. package/dist/components/Dialogs.js +74 -16
  48. package/dist/components/FileUpload.d.ts +10 -2
  49. package/dist/components/FileUpload.js +10 -4
  50. package/dist/components/FilterEditor.d.ts +4 -0
  51. package/dist/components/FilterEditor.js +13 -2
  52. package/dist/components/Heartbeat.d.ts +31 -0
  53. package/dist/components/Heartbeat.js +28 -0
  54. package/dist/components/Menu.d.ts +53 -5
  55. package/dist/components/Menu.js +104 -4
  56. package/dist/components/Meter.d.ts +7 -3
  57. package/dist/components/Meter.js +2 -1
  58. package/dist/components/Popover.d.ts +21 -2
  59. package/dist/components/Popover.js +94 -17
  60. package/dist/components/Prose.d.ts +6 -2
  61. package/dist/components/Prose.js +1 -1
  62. package/dist/components/RuleBuilder.d.ts +52 -4
  63. package/dist/components/RuleBuilder.js +109 -24
  64. package/dist/components/SchedulingCalendar.d.ts +4 -110
  65. package/dist/components/SchedulingCalendar.js +299 -63
  66. package/dist/components/SchedulingTimeGrid.d.ts +4 -0
  67. package/dist/components/SchedulingTimeGrid.js +521 -0
  68. package/dist/components/SchedulingTimeline.d.ts +2 -1
  69. package/dist/components/Segmented.d.ts +17 -3
  70. package/dist/components/Segmented.js +59 -5
  71. package/dist/components/Select.d.ts +66 -2
  72. package/dist/components/Select.js +800 -10
  73. package/dist/components/Spinner.d.ts +2 -1
  74. package/dist/components/Spinner.js +1 -1
  75. package/dist/components/StatusPill.d.ts +8 -2
  76. package/dist/components/StatusPill.js +2 -2
  77. package/dist/components/Steps.d.ts +28 -4
  78. package/dist/components/Steps.js +46 -16
  79. package/dist/components/TableControls.js +4 -3
  80. package/dist/components/Tabs.d.ts +7 -2
  81. package/dist/components/Tabs.js +14 -1
  82. package/dist/components/TextDiff.d.ts +49 -0
  83. package/dist/components/TextDiff.js +124 -0
  84. package/dist/{emoji-image-6mPZ15Zy.d.cts → emoji-image-DjCKpdwE.d.cts} +1 -1
  85. package/dist/emoji.cjs +169 -27
  86. package/dist/emoji.d.cts +3 -3
  87. package/dist/file-model.d.ts +22 -3
  88. package/dist/file-model.js +6 -4
  89. package/dist/filter-model.d.ts +26 -2
  90. package/dist/filter-model.js +13 -1
  91. package/dist/hooks/useAnchoredPosition.d.ts +24 -1
  92. package/dist/hooks/useAnchoredPosition.js +58 -8
  93. package/dist/hooks/useVirtualRows.d.ts +17 -2
  94. package/dist/hooks/useVirtualRows.js +6 -5
  95. package/dist/icons/Icon.d.ts +14 -14
  96. package/dist/index.cjs +4888 -1714
  97. package/dist/index.d.cts +1871 -658
  98. package/dist/index.d.ts +16 -9
  99. package/dist/index.js +67 -4
  100. package/dist/labels-context.d.ts +29 -1
  101. package/dist/{overlay-position-Cjv5E7mn.d.cts → overlay-position-2Afke51l.d.cts} +20 -0
  102. package/dist/overlay-position.d.ts +20 -0
  103. package/dist/overlay-position.js +17 -2
  104. package/dist/rich-text.cjs +169 -27
  105. package/dist/rich-text.d.cts +3 -3
  106. package/dist/rule-builder-model.d.ts +34 -2
  107. package/dist/rule-builder-model.js +48 -12
  108. package/dist/scheduling-calendar-model.d.ts +169 -0
  109. package/dist/scheduling-calendar-model.js +176 -0
  110. package/dist/select-model.d.ts +80 -0
  111. package/dist/select-model.js +109 -0
  112. package/dist/table-engine.d.ts +7 -0
  113. package/dist/table-engine.js +2 -1
  114. package/dist/text-diff-model.d.ts +71 -0
  115. package/dist/text-diff-model.js +261 -0
  116. package/package.json +1 -1
  117. package/dist/ChartCore-DzWe10bN.d.cts +0 -59
@@ -79,7 +79,7 @@ function fitsOn(side, a, f, vp, offset, padding) {
79
79
  return spaceOn(side, a, vp, offset) >= need + padding;
80
80
  }
81
81
  function computePosition(anchor, floating, viewport, options = {}) {
82
- const { placement = "bottom-start", offset = 6, padding = 8, flip = true, shift = true, stickySide } = options;
82
+ const { placement = "bottom-start", offset = 6, padding = 8, flip = true, shift = true, shiftMainAxis = true, stickySide } = options;
83
83
  const { side: preferred, align } = parsePlacement(placement);
84
84
  let side = preferred;
85
85
  const sticky = stickySide === preferred || stickySide === OPPOSITE[preferred] ? stickySide : void 0;
@@ -112,12 +112,27 @@ function computePosition(anchor, floating, viewport, options = {}) {
112
112
  y = align === "start" ? start : align === "end" ? end : center;
113
113
  if (shift) y = clamp(y, padding, Math.max(padding, viewport.height - floating.height - padding));
114
114
  }
115
+ const mainAxisNatural = horizontal ? y : x;
116
+ const anchorInView = horizontal ? anchor.y + anchor.height > 0 && anchor.y < viewport.height : anchor.x + anchor.width > 0 && anchor.x < viewport.width;
117
+ let mainAxisRange = null;
118
+ if (shiftMainAxis && anchorInView) {
119
+ const extent = horizontal ? viewport.height - floating.height : viewport.width - floating.width;
120
+ mainAxisRange = [padding, Math.max(padding, extent - padding)];
121
+ if (horizontal) y = clamp(y, mainAxisRange[0], mainAxisRange[1]);
122
+ else x = clamp(x, mainAxisRange[0], mainAxisRange[1]);
123
+ }
115
124
  return {
116
125
  x,
117
126
  y,
118
127
  placement: align === "center" ? side : `${side}-${align}`,
119
128
  side,
120
- align
129
+ align,
130
+ mainAxisNatural,
131
+ mainAxisRange,
132
+ available: {
133
+ width: Math.max(0, viewport.width - 2 * padding),
134
+ height: Math.max(0, viewport.height - 2 * padding)
135
+ }
121
136
  };
122
137
  }
123
138
 
@@ -780,6 +795,7 @@ function supportsAnchoring() {
780
795
  }
781
796
  return anchoringSupported;
782
797
  }
798
+ var ANCHOR_BAND = 64;
783
799
  var anchorCount = 0;
784
800
  var namesOf = (value) => value.split(",").map((n) => n.trim()).filter((n) => n.startsWith("--"));
785
801
  function addAnchorName(el, name) {
@@ -804,9 +820,12 @@ function translationOf(el) {
804
820
  }
805
821
  var signed = (n) => `${n < 0 ? "-" : "+"} ${Math.round(Math.abs(n) * 100) / 100}px`;
806
822
  var prepared = /* @__PURE__ */ new WeakSet();
807
- function useAnchoredPosition(anchor, floatingRef, { open, placement = "bottom-start", offset = 6, padding = 8 }) {
808
- const opts = (0, import_react3.useRef)({ placement, offset, padding });
809
- opts.current = { placement, offset, padding };
823
+ function useAnchoredPosition(anchor, floatingRef, { open, placement = "bottom-start", offset = 6, padding = 8, shiftMainAxis = true, capHeight = false, onAnchorHidden }) {
824
+ const opts = (0, import_react3.useRef)({ placement, offset, padding, shiftMainAxis, capHeight });
825
+ opts.current = { placement, offset, padding, shiftMainAxis, capHeight };
826
+ const onAnchorHiddenRef = (0, import_react3.useRef)(onAnchorHidden);
827
+ onAnchorHiddenRef.current = onAnchorHidden;
828
+ const heightCap = (0, import_react3.useRef)(null);
810
829
  const session = (0, import_react3.useRef)(false);
811
830
  const side = (0, import_react3.useRef)(null);
812
831
  const written = (0, import_react3.useRef)("");
@@ -830,6 +849,15 @@ function useAnchoredPosition(anchor, floatingRef, { open, placement = "bottom-st
830
849
  }
831
850
  removeAnchorName(current.anchor, current.name);
832
851
  }, [floatingRef]);
852
+ const uncap = (0, import_react3.useCallback)(() => {
853
+ const cap = heightCap.current;
854
+ if (!cap) return;
855
+ heightCap.current = null;
856
+ const floating = floatingRef.current;
857
+ if (!floating) return;
858
+ floating.style.maxHeight = cap.inlineMaxHeight;
859
+ if (cap.setOverflow) floating.style.overflowY = cap.inlineOverflowY;
860
+ }, [floatingRef]);
833
861
  const reposition = (0, import_react3.useCallback)(() => {
834
862
  const anchorEl = resolveAnchor(anchor);
835
863
  const floating = floatingRef.current;
@@ -842,14 +870,36 @@ function useAnchoredPosition(anchor, floatingRef, { open, placement = "bottom-st
842
870
  prepared.add(floating);
843
871
  written.current = "";
844
872
  }
873
+ const { placement: p, offset: o, padding: pad, shiftMainAxis: keepInView, capHeight: cap } = opts.current;
874
+ if (cap) {
875
+ if (!heightCap.current) {
876
+ const own = getComputedStyle(floating);
877
+ const setOverflow = !own.overflowY || own.overflowY === "visible";
878
+ heightCap.current = {
879
+ inlineMaxHeight: s.maxHeight,
880
+ inlineOverflowY: s.overflowY,
881
+ ownMaxHeight: own.maxHeight && own.maxHeight !== "none" ? own.maxHeight : "",
882
+ setOverflow,
883
+ written: ""
884
+ };
885
+ if (setOverflow) s.overflowY = "auto";
886
+ }
887
+ const room = `${Math.max(0, window.innerHeight - 2 * pad)}px`;
888
+ const max = heightCap.current.ownMaxHeight ? `min(${room}, ${heightCap.current.ownMaxHeight})` : room;
889
+ if (max !== heightCap.current.written) {
890
+ s.maxHeight = max;
891
+ heightCap.current.written = max;
892
+ }
893
+ } else if (heightCap.current) {
894
+ uncap();
895
+ }
845
896
  const a = anchorEl.getBoundingClientRect();
846
897
  const size = { width: floating.offsetWidth, height: floating.offsetHeight };
847
- const { placement: p, offset: o, padding: pad } = opts.current;
848
898
  const pos = computePosition(
849
899
  { x: a.x, y: a.y, width: a.width, height: a.height },
850
900
  size,
851
901
  { width: window.innerWidth, height: window.innerHeight },
852
- { placement: p, offset: o, padding: pad, stickySide: side.current ?? void 0 }
902
+ { placement: p, offset: o, padding: pad, shiftMainAxis: keepInView, stickySide: side.current ?? void 0 }
853
903
  );
854
904
  const measured = size.width > 0 || size.height > 0;
855
905
  if (measured && session.current) side.current = pos.side;
@@ -863,7 +913,10 @@ function useAnchoredPosition(anchor, floatingRef, { open, placement = "bottom-st
863
913
  s.top = top2;
864
914
  written.current = key2;
865
915
  };
866
- if (anchoringFailed.current || !supportsAnchoring()) {
916
+ const limit = pos.mainAxisRange;
917
+ const nearLimit = limit !== null && (pos.mainAxisNatural < limit[0] + ANCHOR_BAND || pos.mainAxisNatural > limit[1] - ANCHOR_BAND);
918
+ if (anchoringFailed.current || nearLimit || !supportsAnchoring()) {
919
+ if (anchoring.current) release(false);
867
920
  writeFixed(pos);
868
921
  return;
869
922
  }
@@ -900,7 +953,7 @@ function useAnchoredPosition(anchor, floatingRef, { open, placement = "bottom-st
900
953
  writeFixed(pos);
901
954
  }
902
955
  }
903
- }, [anchor, floatingRef, release]);
956
+ }, [anchor, floatingRef, release, uncap]);
904
957
  useIsomorphicLayoutEffect(() => {
905
958
  if (!open) return;
906
959
  session.current = true;
@@ -929,8 +982,20 @@ function useAnchoredPosition(anchor, floatingRef, { open, placement = "bottom-st
929
982
  side.current = null;
930
983
  anchoringFailed.current = false;
931
984
  release(true);
985
+ uncap();
932
986
  };
933
- }, [open, reposition, anchor, floatingRef, release]);
987
+ }, [open, reposition, anchor, floatingRef, release, uncap]);
988
+ const watchAnchor = open && !!onAnchorHidden;
989
+ (0, import_react3.useEffect)(() => {
990
+ const anchorEl = resolveAnchor(anchor);
991
+ if (!watchAnchor || !anchorEl || typeof IntersectionObserver !== "function") return;
992
+ const observer = new IntersectionObserver((entries) => {
993
+ const latest = entries[entries.length - 1];
994
+ if (latest && !latest.isIntersecting) onAnchorHiddenRef.current?.();
995
+ });
996
+ observer.observe(anchorEl);
997
+ return () => observer.disconnect();
998
+ }, [watchAnchor, anchor]);
934
999
  (0, import_react3.useEffect)(() => () => release(false), [release]);
935
1000
  return reposition;
936
1001
  }
@@ -952,16 +1017,22 @@ var settle = (el) => {
952
1017
  el.removeAttribute(PLACING);
953
1018
  };
954
1019
  function Popover({
955
- popover = "auto",
1020
+ popover: popoverProp,
956
1021
  anchor,
957
1022
  placement = "bottom-start",
958
1023
  offset = 6,
959
1024
  closeWhenAnchorHidden = false,
960
1025
  onAnchorHidden,
1026
+ capHeight = false,
1027
+ openOnHover = false,
961
1028
  className,
962
1029
  children,
963
1030
  ...props
964
1031
  }) {
1032
+ const hover = !!openOnHover && !!anchor;
1033
+ const popover = popoverProp ?? (hover ? "manual" : "auto");
1034
+ const openDelay = typeof openOnHover === "object" ? openOnHover.openDelay ?? 300 : 300;
1035
+ const closeDelay = typeof openOnHover === "object" ? openOnHover.closeDelay ?? 150 : 150;
965
1036
  const ref = (0, import_react4.useRef)(null);
966
1037
  const [open, setOpen] = (0, import_react4.useState)(false);
967
1038
  const openRef = (0, import_react4.useRef)(false);
@@ -971,7 +1042,16 @@ function Popover({
971
1042
  const reposition = useAnchoredPosition(anchor ?? null, ref, {
972
1043
  open: open && !!anchor,
973
1044
  placement,
974
- offset
1045
+ offset,
1046
+ capHeight,
1047
+ onAnchorHidden: closeWhenAnchorHidden ? () => {
1048
+ const el = ref.current;
1049
+ if (!el || !isPopoverOpen(el)) return;
1050
+ onAnchorHiddenRef.current?.();
1051
+ const active = el.ownerDocument.activeElement;
1052
+ if (active && el.contains(active)) active.blur();
1053
+ el.hidePopover();
1054
+ } : void 0
975
1055
  });
976
1056
  (0, import_react4.useEffect)(() => {
977
1057
  const el = ref.current;
@@ -1006,25 +1086,87 @@ function Popover({
1006
1086
  el.removeAttribute(PLACING);
1007
1087
  };
1008
1088
  }, [anchor, reposition]);
1089
+ (0, import_react4.useEffect)(() => {
1090
+ const el = ref.current;
1091
+ const anchorEl = resolveAnchor(anchor);
1092
+ if (!hover || !el || !anchorEl) return;
1093
+ let timer;
1094
+ const inside = (node) => !!node && (anchorEl.contains(node) || el.contains(node));
1095
+ const stillWanted = () => {
1096
+ try {
1097
+ return anchorEl.matches(":hover") || el.matches(":hover") || inside(el.ownerDocument.activeElement);
1098
+ } catch {
1099
+ return false;
1100
+ }
1101
+ };
1102
+ const show = (delay) => {
1103
+ window.clearTimeout(timer);
1104
+ timer = window.setTimeout(() => {
1105
+ if (!isPopoverOpen(el)) {
1106
+ try {
1107
+ el.showPopover?.();
1108
+ } catch {
1109
+ }
1110
+ }
1111
+ }, delay);
1112
+ };
1113
+ const hide = (delay) => {
1114
+ window.clearTimeout(timer);
1115
+ timer = window.setTimeout(() => {
1116
+ if (isPopoverOpen(el) && !stillWanted()) {
1117
+ try {
1118
+ el.hidePopover?.();
1119
+ } catch {
1120
+ }
1121
+ }
1122
+ }, delay);
1123
+ };
1124
+ const onEnter = (event) => {
1125
+ if (event.pointerType !== "touch") show(openDelay);
1126
+ };
1127
+ const onLeave = (event) => {
1128
+ if (event.pointerType !== "touch") hide(closeDelay);
1129
+ };
1130
+ const onFocusIn = () => show(0);
1131
+ const onFocusOut = (event) => {
1132
+ if (!inside(event.relatedTarget)) hide(closeDelay);
1133
+ };
1134
+ const onKeyDown = (event) => {
1135
+ if (event.key !== "Escape" || !isPopoverOpen(el)) return;
1136
+ window.clearTimeout(timer);
1137
+ const hadFocus = el.contains(el.ownerDocument.activeElement);
1138
+ try {
1139
+ el.hidePopover?.();
1140
+ } catch {
1141
+ }
1142
+ if (hadFocus) {
1143
+ anchorEl.removeEventListener("focusin", onFocusIn);
1144
+ anchorEl.focus();
1145
+ anchorEl.addEventListener("focusin", onFocusIn);
1146
+ }
1147
+ };
1148
+ for (const node of [anchorEl, el]) {
1149
+ node.addEventListener("pointerenter", onEnter);
1150
+ node.addEventListener("pointerleave", onLeave);
1151
+ node.addEventListener("focusout", onFocusOut);
1152
+ node.addEventListener("keydown", onKeyDown);
1153
+ }
1154
+ anchorEl.addEventListener("focusin", onFocusIn);
1155
+ return () => {
1156
+ window.clearTimeout(timer);
1157
+ for (const node of [anchorEl, el]) {
1158
+ node.removeEventListener("pointerenter", onEnter);
1159
+ node.removeEventListener("pointerleave", onLeave);
1160
+ node.removeEventListener("focusout", onFocusOut);
1161
+ node.removeEventListener("keydown", onKeyDown);
1162
+ }
1163
+ anchorEl.removeEventListener("focusin", onFocusIn);
1164
+ };
1165
+ }, [hover, anchor, openDelay, closeDelay]);
1009
1166
  useIsomorphicLayoutEffect2(() => {
1010
1167
  openRef.current = open;
1011
1168
  if (open && ref.current) settle(ref.current);
1012
1169
  }, [open]);
1013
- (0, import_react4.useEffect)(() => {
1014
- const el = ref.current;
1015
- const anchorEl = resolveAnchor(anchor);
1016
- if (!closeWhenAnchorHidden || !open || !el || !anchorEl || typeof IntersectionObserver !== "function") return;
1017
- const observer = new IntersectionObserver((entries) => {
1018
- const latest = entries[entries.length - 1];
1019
- if (!latest || latest.isIntersecting || !isPopoverOpen(el)) return;
1020
- onAnchorHiddenRef.current?.();
1021
- const active = el.ownerDocument.activeElement;
1022
- if (active && el.contains(active)) active.blur();
1023
- el.hidePopover();
1024
- });
1025
- observer.observe(anchorEl);
1026
- return () => observer.disconnect();
1027
- }, [closeWhenAnchorHidden, open, anchor]);
1028
1170
  return /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { ref, className: cx("uix-popover", className), ...popoverAttr, ...props, children });
1029
1171
  }
1030
1172
 
@@ -1,8 +1,8 @@
1
1
  import * as react from 'react';
2
2
  import { ReactNode } from 'react';
3
- import { E as EmojiPickerLabels, a as EmojiLocale } from './emoji-image-6mPZ15Zy.cjs';
4
- export { l as emojiImageFileName } from './emoji-image-6mPZ15Zy.cjs';
5
- import './overlay-position-Cjv5E7mn.cjs';
3
+ import { E as EmojiPickerLabels, a as EmojiLocale } from './emoji-image-DjCKpdwE.cjs';
4
+ export { l as emojiImageFileName } from './emoji-image-DjCKpdwE.cjs';
5
+ import './overlay-position-2Afke51l.cjs';
6
6
 
7
7
  type RichTextFeatures = 'full' | 'comment' | 'template';
8
8
  type RichTextHeadingLevel = 1 | 2 | 3;
@@ -20,9 +20,24 @@ interface RuleDefinition {
20
20
  when: RuleGroup;
21
21
  then: RuleAction[];
22
22
  }
23
+ /** The built-in checks of `validateRuleDefinition`; each can be reworded or turned off (HAR-1618). */
24
+ type RuleCheck = 'duplicateId' | 'maxDepth' | 'emptyGroup' | 'missingField' | 'missingOperator' | 'noActions' | 'missingActionType';
23
25
  interface RuleValidationIssue {
24
26
  path: string;
25
27
  message: string;
28
+ /** Which built-in check raised it. Absent on issues from the consumer's own `validate`. */
29
+ check?: RuleCheck;
30
+ }
31
+ /** The built-in messages. `{id}` is the duplicated id, `{max}` the depth limit. */
32
+ declare const DEFAULT_RULE_VALIDATION_MESSAGES: Record<RuleCheck, string>;
33
+ interface RuleValidationOptions {
34
+ /** Replace individual messages (a translation). */
35
+ messages?: Partial<Record<RuleCheck, string>>;
36
+ /**
37
+ * `false` turns a check off: `{ emptyGroup: false }` for a rule that may have no condition
38
+ * (a catch-all), `{ noActions: false, missingActionType: false }` for conditions without actions.
39
+ */
40
+ checks?: Partial<Record<RuleCheck, boolean>>;
26
41
  }
27
42
  declare function isRuleGroup(node: RuleCondition | RuleGroup): node is RuleGroup;
28
43
  declare function ruleDepth(group: RuleGroup): number;
@@ -31,11 +46,28 @@ declare function mapRuleGroup(group: RuleGroup, id: string, update: (node: RuleC
31
46
  declare function appendRuleNode(group: RuleGroup, parentId: string, node: RuleCondition | RuleGroup, maxDepth?: number): RuleGroup;
32
47
  declare function removeRuleNode(group: RuleGroup, id: string): RuleGroup;
33
48
  declare function moveRuleNode(group: RuleGroup, id: string, direction: -1 | 1): RuleGroup;
34
- declare function validateRuleDefinition(value: RuleDefinition, maxDepth?: number): RuleValidationIssue[];
49
+ /** Set `combinator` on a group and on every group nested in it. */
50
+ declare function setRuleCombinator(group: RuleGroup, combinator: 'and' | 'or'): RuleGroup;
51
+ declare function validateRuleDefinition(value: RuleDefinition, maxDepth?: number, options?: RuleValidationOptions): RuleValidationIssue[];
52
+ /** The words of the `summarizeRule` sentence. `{conditions}` and `{actions}` are filled in. */
53
+ interface RuleSummaryWords {
54
+ sentence: string;
55
+ /** For a rule with no actions to name (a conditions-only rule). */
56
+ sentenceConditionsOnly: string;
57
+ noConditions: string;
58
+ noActions: string;
59
+ and: string;
60
+ or: string;
61
+ }
62
+ declare const DEFAULT_RULE_SUMMARY_WORDS: RuleSummaryWords;
35
63
  declare function summarizeRule(value: RuleDefinition, labels?: {
36
64
  fields?: Record<string, string>;
37
65
  operators?: Record<string, string>;
38
66
  actions?: Record<string, string>;
67
+ /** Translate the sentence itself. Default: English. */
68
+ words?: Partial<RuleSummaryWords>;
69
+ /** Leave the actions out of the sentence. */
70
+ conditionsOnly?: boolean;
39
71
  }): string;
40
72
 
41
- export { type RuleAction, type RuleCondition, type RuleDefinition, type RuleGroup, type RuleValidationIssue, appendRuleNode, findRuleNodeDepth, isRuleGroup, mapRuleGroup, moveRuleNode, removeRuleNode, ruleDepth, summarizeRule, validateRuleDefinition };
73
+ export { DEFAULT_RULE_SUMMARY_WORDS, DEFAULT_RULE_VALIDATION_MESSAGES, type RuleAction, type RuleCheck, type RuleCondition, type RuleDefinition, type RuleGroup, type RuleSummaryWords, type RuleValidationIssue, type RuleValidationOptions, appendRuleNode, findRuleNodeDepth, isRuleGroup, mapRuleGroup, moveRuleNode, removeRuleNode, ruleDepth, setRuleCombinator, summarizeRule, validateRuleDefinition };
@@ -1,3 +1,13 @@
1
+ const fillLabel = (template, values) => template.replace(/\{(\w+)\}/g, (match, key) => Object.prototype.hasOwnProperty.call(values, key) ? String(values[key]) : match);
2
+ const DEFAULT_RULE_VALIDATION_MESSAGES = {
3
+ duplicateId: "Duplicate id \u201C{id}\u201D.",
4
+ maxDepth: "Nesting exceeds {max} levels.",
5
+ emptyGroup: "Add at least one condition.",
6
+ missingField: "Choose a field.",
7
+ missingOperator: "Choose an operator.",
8
+ noActions: "Add at least one action.",
9
+ missingActionType: "Choose an action."
10
+ };
1
11
  function isRuleGroup(node) {
2
12
  return "conditions" in node;
3
13
  }
@@ -52,43 +62,68 @@ function moveRuleNode(group, id, direction) {
52
62
  conditions: group.conditions.map((node) => isRuleGroup(node) ? moveRuleNode(node, id, direction) : node)
53
63
  };
54
64
  }
55
- function validateRuleDefinition(value, maxDepth = 3) {
65
+ function setRuleCombinator(group, combinator) {
66
+ return {
67
+ ...group,
68
+ combinator,
69
+ conditions: group.conditions.map((node) => isRuleGroup(node) ? setRuleCombinator(node, combinator) : node)
70
+ };
71
+ }
72
+ function validateRuleDefinition(value, maxDepth = 3, options = {}) {
56
73
  const issues = [];
74
+ const messages = { ...DEFAULT_RULE_VALIDATION_MESSAGES, ...options.messages };
75
+ const report = (check, path, values = {}) => {
76
+ if (options.checks?.[check] === false) return;
77
+ issues.push({ path, message: fillLabel(messages[check], values), check });
78
+ };
57
79
  const ids = /* @__PURE__ */ new Set();
58
80
  const visit = (group, path, depth) => {
59
- if (ids.has(group.id)) issues.push({ path, message: `Duplicate id \u201C${group.id}\u201D.` });
81
+ if (ids.has(group.id)) report("duplicateId", path, { id: group.id });
60
82
  ids.add(group.id);
61
- if (depth > maxDepth) issues.push({ path, message: `Nesting exceeds ${maxDepth} levels.` });
62
- if (group.conditions.length === 0) issues.push({ path, message: "Add at least one condition." });
83
+ if (depth > maxDepth) report("maxDepth", path, { max: maxDepth });
84
+ if (group.conditions.length === 0) report("emptyGroup", path);
63
85
  group.conditions.forEach((node, index) => {
64
86
  const nodePath = `${path}.conditions[${index}]`;
65
- if (ids.has(node.id)) issues.push({ path: nodePath, message: `Duplicate id \u201C${node.id}\u201D.` });
87
+ if (ids.has(node.id)) report("duplicateId", nodePath, { id: node.id });
66
88
  if (isRuleGroup(node)) visit(node, nodePath, depth + 1);
67
89
  else {
68
90
  ids.add(node.id);
69
- if (!node.field) issues.push({ path: nodePath, message: "Choose a field." });
70
- if (!node.operator) issues.push({ path: nodePath, message: "Choose an operator." });
91
+ if (!node.field) report("missingField", nodePath);
92
+ if (!node.operator) report("missingOperator", nodePath);
71
93
  }
72
94
  });
73
95
  };
74
96
  visit(value.when, "when", 1);
75
- if (value.then.length === 0) issues.push({ path: "then", message: "Add at least one action." });
97
+ if (value.then.length === 0) report("noActions", "then");
76
98
  value.then.forEach((action, index) => {
77
- if (ids.has(action.id)) issues.push({ path: `then[${index}]`, message: `Duplicate id \u201C${action.id}\u201D.` });
99
+ if (ids.has(action.id)) report("duplicateId", `then[${index}]`, { id: action.id });
78
100
  ids.add(action.id);
79
- if (!action.type) issues.push({ path: `then[${index}]`, message: "Choose an action." });
101
+ if (!action.type) report("missingActionType", `then[${index}]`);
80
102
  });
81
103
  return issues;
82
104
  }
105
+ const DEFAULT_RULE_SUMMARY_WORDS = {
106
+ sentence: "When {conditions}, then {actions}.",
107
+ sentenceConditionsOnly: "When {conditions}.",
108
+ noConditions: "no conditions",
109
+ noActions: "no actions",
110
+ and: "AND",
111
+ or: "OR"
112
+ };
83
113
  function summarizeRule(value, labels = {}) {
114
+ const words = { ...DEFAULT_RULE_SUMMARY_WORDS, ...labels.words };
84
115
  const formatValue = (input) => input === void 0 ? "" : ` ${JSON.stringify(input)}`;
85
116
  const formatGroup = (group) => group.conditions.map(
86
117
  (node) => isRuleGroup(node) ? `(${formatGroup(node)})` : `${labels.fields?.[node.field] ?? node.field} ${labels.operators?.[node.operator] ?? node.operator}${formatValue(node.value)}`
87
- ).join(` ${group.combinator.toUpperCase()} `);
118
+ ).join(` ${group.combinator === "or" ? words.or : words.and} `);
119
+ const conditions = formatGroup(value.when) || words.noConditions;
120
+ if (labels.conditionsOnly) return fillLabel(words.sentenceConditionsOnly, { conditions });
88
121
  const actions = value.then.map((action) => labels.actions?.[action.type] ?? action.type).join(", ");
89
- return `When ${formatGroup(value.when) || "no conditions"}, then ${actions || "no actions"}.`;
122
+ return fillLabel(words.sentence, { conditions, actions: actions || words.noActions });
90
123
  }
91
124
  export {
125
+ DEFAULT_RULE_SUMMARY_WORDS,
126
+ DEFAULT_RULE_VALIDATION_MESSAGES,
92
127
  appendRuleNode,
93
128
  findRuleNodeDepth,
94
129
  isRuleGroup,
@@ -96,6 +131,7 @@ export {
96
131
  moveRuleNode,
97
132
  removeRuleNode,
98
133
  ruleDepth,
134
+ setRuleCombinator,
99
135
  summarizeRule,
100
136
  validateRuleDefinition
101
137
  };
@@ -0,0 +1,169 @@
1
+ import { CalendarWeekday, ZonedDaySpan } from './calendar-model.js';
2
+
3
+ /** Which lane group a span is drawn in: windows sit above multi-day items, and neither takes a chip slot. */
4
+ type MonthSpanGroup = 'window' | 'item';
5
+ interface MonthSpanInput {
6
+ id: string;
7
+ /** Half-open `[start, end)` instants (ISO 8601, epoch ms or Date). */
8
+ start: string | number | Date;
9
+ end: string | number | Date;
10
+ /** Defaults to `'item'`. */
11
+ group?: MonthSpanGroup;
12
+ }
13
+ interface PlacedMonthSpan {
14
+ id: string;
15
+ group: MonthSpanGroup;
16
+ /** 0-based week row of the grid `days` the layout was computed for. */
17
+ weekRow: number;
18
+ /** 0-based lane inside its group. */
19
+ lane: number;
20
+ /** First and last column (0–6, inclusive) the span covers in this row. */
21
+ startCol: number;
22
+ endCol: number;
23
+ /** The span started in an earlier row (or before the grid). */
24
+ continuesBefore: boolean;
25
+ /** The span goes on in a later row (or past the grid). */
26
+ continuesAfter: boolean;
27
+ }
28
+ interface MonthSpanLayout {
29
+ /** One entry per span per week row it got a lane in. Render exactly these; never re-pack. */
30
+ placed: PlacedMonthSpan[];
31
+ /** Per week row, the ids that got no lane, in input order (windows first). An id may name a window and an item. */
32
+ hiddenByRow: Record<number, string[]>;
33
+ /** Per week row with a hidden span, the first day (`YYYY-MM-DD`) a hidden span covers. */
34
+ firstHiddenDayByRow: Record<number, string>;
35
+ }
36
+ interface LayoutMonthSpansOptions {
37
+ /** The display zone that decides which days a span covers. */
38
+ timeZone: string;
39
+ /** When set, `days[0]` must fall on this weekday (a guard against a grid built for another week start). */
40
+ weekStartsOn?: CalendarWeekday;
41
+ /** Lanes per group (default 2). A number applies to both groups. */
42
+ laneCap?: number | Partial<Record<MonthSpanGroup, number>>;
43
+ }
44
+ /**
45
+ * The calendar days an item covers in `timeZone`, end-exclusive (`end` is the first day not
46
+ * covered): a 22:00–00:00 item covers only its start day, a 23:00 → 02:00 item two days. A
47
+ * zero-length item covers the day of its start. Throws a `RangeError` when `end` is before
48
+ * `start`; it never swaps them.
49
+ */
50
+ declare function itemDaySpan(start: string | number | Date, end: string | number | Date, timeZone: string): ZonedDaySpan;
51
+ /**
52
+ * The date keys a `SchedulingCalendar` grid shows, in order: six week rows for `'month'`
53
+ * (from the week holding the 1st), one for `'week'` (the week holding `anchorDate`). Pass the
54
+ * result to `layoutMonthSpans` so the layout and the rendered grid agree.
55
+ */
56
+ declare function schedulingGridDays(anchorDate: string, view: 'month' | 'week', weekStartsOn?: CalendarWeekday): string[];
57
+ /**
58
+ * Places multi-day spans as one continuous bar per week row. Spans are taken **in the order
59
+ * given** (the consumer's salience order): in each row and group, the first spans to ask get
60
+ * the lanes, the rest are listed in `hiddenByRow`. Windows and items pack in separate groups,
61
+ * each capped by `laneCap`. Day membership is the end-exclusive zoned span in `timeZone`
62
+ * (never a UTC date). `days` are the grid's date keys, a whole number of weeks.
63
+ */
64
+ declare function layoutMonthSpans(spans: readonly MonthSpanInput[], days: readonly string[], options: LayoutMonthSpansOptions): MonthSpanLayout;
65
+ /** How far past its first local midnight an entry may run and still be drawn in the grid (operator decision E12). */
66
+ declare const TOP_LANE_CROSS_MIDNIGHT_MINUTES = 360;
67
+ interface TimeGridEntryInput {
68
+ id: string;
69
+ /** Half-open `[start, end)` instants (ISO 8601, epoch ms or Date). */
70
+ start: string | number | Date;
71
+ end: string | number | Date;
72
+ /** An entry with no time of day: always drawn in the top lane. */
73
+ allDay?: boolean;
74
+ }
75
+ /**
76
+ * Whether an entry is drawn in the top lane of the time grid instead of in the day columns:
77
+ * it is all-day, it is longer than one day (it ends after the same wall-clock time on the
78
+ * next day), or it runs more than `crossMidnightMinutes` past its first local midnight. A
79
+ * shorter midnight-crosser stays in the grid as a start segment and a continuation segment.
80
+ */
81
+ declare function placesInTopLane(entry: Pick<TimeGridEntryInput, 'start' | 'end' | 'allDay'>, timeZone: string, options?: {
82
+ crossMidnightMinutes?: number;
83
+ }): boolean;
84
+ /** Which part of an entry a grid segment is: all of it, its first day, or the rest on the next day. */
85
+ type TimeGridPart = 'whole' | 'start' | 'continuation';
86
+ interface TimeGridSegment {
87
+ id: string;
88
+ part: TimeGridPart;
89
+ /** The segment, clipped to the day (epoch ms). */
90
+ start: number;
91
+ end: number;
92
+ /** Real hours from the start of the day to the segment. Multiply by the hour height. */
93
+ offsetHours: number;
94
+ /** The length of the segment in real hours. */
95
+ lengthHours: number;
96
+ /** 0-based lane inside its overlap cluster. */
97
+ lane: number;
98
+ /** Lanes its cluster uses: the segment is this fraction of the column wide. */
99
+ laneCount: number;
100
+ }
101
+ interface TimeGridDayLayout {
102
+ date: string;
103
+ /** The first instant of the day (epoch ms) and its length in real hours. */
104
+ dayStart: number;
105
+ dayHours: number;
106
+ /** One per entry part that got a lane, by start. */
107
+ segments: TimeGridSegment[];
108
+ /** Entries on this day that got no lane, by start. */
109
+ hidden: string[];
110
+ }
111
+ /**
112
+ * Lays one day column out: every entry that is not in the top lane and touches the day, clipped
113
+ * to it and packed into at most `maxLanes` lanes with `packLanes`. An entry that crosses
114
+ * midnight gives a `'start'` segment on its first day and a `'continuation'` on the next.
115
+ */
116
+ declare function layoutTimeGridDay(entries: readonly TimeGridEntryInput[], date: string, timeZone: string, options?: {
117
+ maxLanes?: number;
118
+ crossMidnightMinutes?: number;
119
+ }): TimeGridDayLayout;
120
+ interface PlacedDaySpan {
121
+ id: string;
122
+ lane: number;
123
+ /** First and last column (inclusive) of `days` the span covers. */
124
+ startCol: number;
125
+ endCol: number;
126
+ continuesBefore: boolean;
127
+ continuesAfter: boolean;
128
+ }
129
+ interface DaySpanLayout {
130
+ placed: PlacedDaySpan[];
131
+ /** Ids that got no lane, in input order. */
132
+ hidden: string[];
133
+ /** The first day a hidden span covers, when there is one. */
134
+ firstHiddenDay?: string;
135
+ }
136
+ /**
137
+ * Places spans over one row of consecutive `days` (a week, or the single day of the Day
138
+ * view), in the order given, in at most `laneCap` lanes. Day membership is the end-exclusive
139
+ * zoned span. The time grid uses it for its top lane and its window strip.
140
+ */
141
+ declare function layoutDaySpans(spans: readonly MonthSpanInput[], days: readonly string[], options: {
142
+ timeZone: string;
143
+ laneCap?: number;
144
+ }): DaySpanLayout;
145
+ /** A move the user asks for. The component never applies it: the consumer decides. */
146
+ interface MoveProposal {
147
+ /** ISO 8601 instants of the proposed window. */
148
+ start: string;
149
+ end: string;
150
+ /** `'gap_forward'` when the wall-clock time asked for does not exist that day and moved past the gap. */
151
+ adjusted: 'gap_forward' | null;
152
+ }
153
+ /**
154
+ * The window `[start, end)` moved by `minutes` of real time and then by `days` calendar days.
155
+ * A day move keeps the wall-clock start and end (`addZonedDays`), whatever the length of the
156
+ * days in between. Compute it from the original window every time, so that a time falling in
157
+ * a clock-change gap moves forward once, never twice. When a clock change on the target day
158
+ * would put the wall-clock end at or before the start (a window that sits on the skipped or
159
+ * repeated hour), the end keeps the real length of the window instead.
160
+ */
161
+ declare function proposeMove(span: {
162
+ start: string | number | Date;
163
+ end: string | number | Date;
164
+ }, delta: {
165
+ days?: number;
166
+ minutes?: number;
167
+ }, timeZone: string): MoveProposal;
168
+
169
+ export { type DaySpanLayout, type LayoutMonthSpansOptions, type MonthSpanGroup, type MonthSpanInput, type MonthSpanLayout, type MoveProposal, type PlacedDaySpan, type PlacedMonthSpan, TOP_LANE_CROSS_MIDNIGHT_MINUTES, type TimeGridDayLayout, type TimeGridEntryInput, type TimeGridPart, type TimeGridSegment, itemDaySpan, layoutDaySpans, layoutMonthSpans, layoutTimeGridDay, placesInTopLane, proposeMove, schedulingGridDays };