@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
@@ -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,66 @@
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
+
66
+ export { type LayoutMonthSpansOptions, type MonthSpanGroup, type MonthSpanInput, type MonthSpanLayout, type PlacedMonthSpan, itemDaySpan, layoutMonthSpans, schedulingGridDays };
@@ -0,0 +1,82 @@
1
+ import { addCalendarDays, buildMonthGrid, packLanes, startOfMonth, zonedDateKey, zonedDaySpan } from "./calendar-model.js";
2
+ const DEFAULT_LANE_CAP = 2;
3
+ function itemDaySpan(start, end, timeZone) {
4
+ const from = new Date(start);
5
+ const to = new Date(end);
6
+ if (from.getTime() === to.getTime()) {
7
+ const day = zonedDateKey(from, timeZone);
8
+ return { start: day, end: addCalendarDays(day, 1) };
9
+ }
10
+ return zonedDaySpan(from, to, timeZone);
11
+ }
12
+ function schedulingGridDays(anchorDate, view, weekStartsOn = 1) {
13
+ if (view === "month") return buildMonthGrid(startOfMonth(anchorDate), weekStartsOn).map((day) => day.date);
14
+ const weekday = (/* @__PURE__ */ new Date(`${anchorDate}T00:00:00Z`)).getUTCDay();
15
+ const first = addCalendarDays(anchorDate, -((weekday - weekStartsOn + 7) % 7));
16
+ return Array.from({ length: 7 }, (_, index) => addCalendarDays(first, index));
17
+ }
18
+ function layoutMonthSpans(spans, days, options) {
19
+ if (days.length === 0 || days.length % 7 !== 0) throw new RangeError(`layoutMonthSpans: days must be whole weeks, got ${days.length} day(s)`);
20
+ for (let index = 1; index < days.length; index++) {
21
+ if (days[index] !== addCalendarDays(days[index - 1], 1)) throw new RangeError(`layoutMonthSpans: days must be consecutive (${days[index - 1]} \u2192 ${days[index]})`);
22
+ }
23
+ if (options.weekStartsOn !== void 0 && (/* @__PURE__ */ new Date(`${days[0]}T00:00:00Z`)).getUTCDay() !== options.weekStartsOn) {
24
+ throw new RangeError(`layoutMonthSpans: days[0] (${days[0]}) does not fall on weekday ${options.weekStartsOn}`);
25
+ }
26
+ const capFor = (group) => {
27
+ const cap = typeof options.laneCap === "number" ? options.laneCap : options.laneCap?.[group] ?? DEFAULT_LANE_CAP;
28
+ if (!(cap === Infinity || Number.isInteger(cap) && cap >= 0)) throw new RangeError(`layoutMonthSpans: laneCap for ${group} must be a non-negative integer, got ${cap}`);
29
+ return cap;
30
+ };
31
+ const caps = { window: capFor("window"), item: capFor("item") };
32
+ const rows = days.length / 7;
33
+ const gridEnd = addCalendarDays(days[days.length - 1], 1);
34
+ const seen = /* @__PURE__ */ new Set();
35
+ const covered = spans.map((span) => {
36
+ const group = span.group ?? "item";
37
+ if (seen.has(`${group} ${span.id}`)) throw new TypeError(`layoutMonthSpans: duplicate ${group} id ${span.id}`);
38
+ seen.add(`${group} ${span.id}`);
39
+ return { id: span.id, group, days: itemDaySpan(span.start, span.end, options.timeZone) };
40
+ });
41
+ const placed = [];
42
+ const hiddenByRow = {};
43
+ const firstHiddenDayByRow = {};
44
+ for (let row = 0; row < rows; row++) {
45
+ const rowStart = days[row * 7];
46
+ const rowEnd = row + 1 < rows ? days[(row + 1) * 7] : gridEnd;
47
+ for (const group of ["window", "item"]) {
48
+ const inRow = covered.filter((span) => span.group === group && span.days.start < rowEnd && span.days.end > rowStart).map((span) => {
49
+ const from = span.days.start > rowStart ? span.days.start : rowStart;
50
+ const to = span.days.end < rowEnd ? span.days.end : rowEnd;
51
+ const startCol = days.indexOf(from, row * 7) - row * 7;
52
+ const endCol = days.indexOf(addCalendarDays(to, -1), row * 7) - row * 7;
53
+ return { span, from, startCol, endCol };
54
+ });
55
+ const packed = packLanes(inRow.map(({ span, startCol, endCol }) => ({ id: span.id, start: startCol, end: endCol + 1 })), caps[group], { order: "given" });
56
+ for (const { span, from, startCol, endCol } of inRow) {
57
+ const lane = packed.lanes[span.id].lane;
58
+ if (lane === null) {
59
+ (hiddenByRow[row] ?? (hiddenByRow[row] = [])).push(span.id);
60
+ if (firstHiddenDayByRow[row] === void 0 || from < firstHiddenDayByRow[row]) firstHiddenDayByRow[row] = from;
61
+ continue;
62
+ }
63
+ placed.push({
64
+ id: span.id,
65
+ group,
66
+ weekRow: row,
67
+ lane,
68
+ startCol,
69
+ endCol,
70
+ continuesBefore: span.days.start < rowStart,
71
+ continuesAfter: span.days.end > rowEnd
72
+ });
73
+ }
74
+ }
75
+ }
76
+ return { placed, hiddenByRow, firstHiddenDayByRow };
77
+ }
78
+ export {
79
+ itemDaySpan,
80
+ layoutMonthSpans,
81
+ schedulingGridDays
82
+ };