@sustaina/shared-ui 1.18.0 → 1.20.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.
package/dist/index.js CHANGED
@@ -21,11 +21,17 @@ var SheetPrimitive = require('@radix-ui/react-dialog');
21
21
  var i18n = require('i18next');
22
22
  var reactI18next = require('react-i18next');
23
23
  var zustand = require('zustand');
24
+ var StarterKit = require('@tiptap/starter-kit');
25
+ var react = require('@tiptap/react');
26
+ var ReactDOM = require('react-dom/client');
27
+ var core = require('@tiptap/core');
28
+ var Suggestion = require('@tiptap/suggestion');
29
+ var prosemirrorState = require('prosemirror-state');
24
30
  var zod$1 = require('@hookform/resolvers/zod');
25
31
  var sortable = require('@dnd-kit/sortable');
26
32
  var utilities = require('@dnd-kit/utilities');
27
33
  var TooltipPrimitive = require('@radix-ui/react-tooltip');
28
- var core = require('@dnd-kit/core');
34
+ var core$1 = require('@dnd-kit/core');
29
35
  var modifiers = require('@dnd-kit/modifiers');
30
36
  var zod = require('zod');
31
37
  var RadioGroupPrimitive = require('@radix-ui/react-radio-group');
@@ -83,6 +89,9 @@ var CheckboxPrimitive__namespace = /*#__PURE__*/_interopNamespace(CheckboxPrimit
83
89
  var CollapsiblePrimitive__namespace = /*#__PURE__*/_interopNamespace(CollapsiblePrimitive);
84
90
  var SheetPrimitive__namespace = /*#__PURE__*/_interopNamespace(SheetPrimitive);
85
91
  var i18n__default = /*#__PURE__*/_interopDefault(i18n);
92
+ var StarterKit__default = /*#__PURE__*/_interopDefault(StarterKit);
93
+ var ReactDOM__default = /*#__PURE__*/_interopDefault(ReactDOM);
94
+ var Suggestion__default = /*#__PURE__*/_interopDefault(Suggestion);
86
95
  var TooltipPrimitive__namespace = /*#__PURE__*/_interopNamespace(TooltipPrimitive);
87
96
  var RadioGroupPrimitive__namespace = /*#__PURE__*/_interopNamespace(RadioGroupPrimitive);
88
97
  var SeparatorPrimitive__namespace = /*#__PURE__*/_interopNamespace(SeparatorPrimitive);
@@ -5382,6 +5391,956 @@ var getDialogAlertControls = () => ({
5382
5391
  closeDialogAlert,
5383
5392
  openErrorDialogAlert
5384
5393
  });
5394
+
5395
+ // src/components/formulaEditor/constants/formulaOperation.ts
5396
+ var defaultOperators = [
5397
+ { value: "(", label: "(" },
5398
+ { value: ")", label: ")" },
5399
+ { value: "%", label: "%" },
5400
+ { value: "&", label: "&" },
5401
+ { value: "+", label: "+" },
5402
+ { value: "-", label: "-" },
5403
+ { value: "*", label: "\xD7" },
5404
+ { value: "/", label: "\xF7" },
5405
+ { value: "=", label: "=" },
5406
+ { value: "\u2260", label: "\u2260" },
5407
+ { value: "#NULL", label: "#NULL" }
5408
+ ];
5409
+ var defaultOperatorShortcuts = {
5410
+ "(": "(",
5411
+ ")": ")",
5412
+ "%": "%",
5413
+ "&": "&",
5414
+ "+": "+",
5415
+ "-": "-",
5416
+ "*": "*",
5417
+ "/": "/",
5418
+ "=": "=",
5419
+ "\u2260": "\u2260",
5420
+ "#NULL": "#NULL"
5421
+ };
5422
+ var DEFAULT_DEBOUNCE = 200;
5423
+ function useKeyboardNavigation(itemsLength, onSelect, isLocked) {
5424
+ const [selectedIndex, setSelectedIndex] = React4.useState(0);
5425
+ React4.useEffect(() => {
5426
+ const handler = (event) => {
5427
+ if (event.key === "ArrowDown") {
5428
+ event.preventDefault();
5429
+ setSelectedIndex((prev) => (prev + 1) % Math.max(itemsLength, 1));
5430
+ return;
5431
+ }
5432
+ if (event.key === "ArrowUp") {
5433
+ event.preventDefault();
5434
+ setSelectedIndex((prev) => (prev - 1 + Math.max(itemsLength, 1)) % Math.max(itemsLength, 1));
5435
+ return;
5436
+ }
5437
+ if (event.key === "Enter" || event.key === "Tab") {
5438
+ event.preventDefault();
5439
+ if (!isLocked) onSelect(selectedIndex);
5440
+ }
5441
+ };
5442
+ window.addEventListener("keydown", handler);
5443
+ return () => window.removeEventListener("keydown", handler);
5444
+ }, [itemsLength, onSelect, selectedIndex, isLocked]);
5445
+ return [selectedIndex, setSelectedIndex];
5446
+ }
5447
+ function useDropdownPosition(clientRect, itemsCount) {
5448
+ const [rect, setRect] = React4.useState(null);
5449
+ const [style, setStyle] = React4.useState({});
5450
+ const ref = React4.useRef(null);
5451
+ React4.useEffect(() => {
5452
+ if (!clientRect) return;
5453
+ const update = () => {
5454
+ const nextRect = clientRect();
5455
+ if (nextRect) setRect(nextRect);
5456
+ };
5457
+ update();
5458
+ window.addEventListener("scroll", update, true);
5459
+ window.addEventListener("resize", update);
5460
+ const resizeObserver = new ResizeObserver(update);
5461
+ resizeObserver.observe(document.body);
5462
+ return () => {
5463
+ window.removeEventListener("scroll", update, true);
5464
+ window.removeEventListener("resize", update);
5465
+ resizeObserver.disconnect();
5466
+ };
5467
+ }, [clientRect]);
5468
+ React4.useLayoutEffect(() => {
5469
+ if (!rect || !ref.current) return;
5470
+ const dropdown = ref.current;
5471
+ const dropdownRect = dropdown.getBoundingClientRect();
5472
+ const viewportHeight = window.innerHeight;
5473
+ const viewportWidth = window.innerWidth;
5474
+ let top = rect.bottom + window.scrollY + 6;
5475
+ let left = rect.left + window.scrollX;
5476
+ if (top + dropdownRect.height > viewportHeight + window.scrollY) {
5477
+ top = rect.top + window.scrollY - dropdownRect.height - 6;
5478
+ }
5479
+ if (left + dropdownRect.width > viewportWidth + window.scrollX) {
5480
+ left = Math.max(viewportWidth + window.scrollX - dropdownRect.width - 8, 0);
5481
+ }
5482
+ setStyle({ position: "absolute", zIndex: 50, top, left });
5483
+ }, [rect, itemsCount]);
5484
+ return { ref, style, rect };
5485
+ }
5486
+ var SuggestionList = ({
5487
+ clientRect,
5488
+ command,
5489
+ fetchItems,
5490
+ mapItem,
5491
+ normalizeToken,
5492
+ debounceMs = DEFAULT_DEBOUNCE,
5493
+ query
5494
+ }) => {
5495
+ const [items, setItems] = React4.useState([]);
5496
+ const [isLoading, setIsLoading] = React4.useState(false);
5497
+ const fetchId = React4.useRef(0);
5498
+ const debounceHandle = React4.useRef(null);
5499
+ const itemRefs = React4.useRef([]);
5500
+ const { ref, style, rect } = useDropdownPosition(clientRect, items.length);
5501
+ const normalizedMap = React4.useMemo(() => mapItem, [mapItem]);
5502
+ const normalizeItem = React4.useMemo(() => normalizeToken, [normalizeToken]);
5503
+ React4.useEffect(() => {
5504
+ const runFetch = (input) => {
5505
+ fetchId.current += 1;
5506
+ const currentId = fetchId.current;
5507
+ if (debounceHandle.current) clearTimeout(debounceHandle.current);
5508
+ debounceHandle.current = window.setTimeout(async () => {
5509
+ setIsLoading(true);
5510
+ try {
5511
+ const results = await fetchItems(input);
5512
+ if (fetchId.current !== currentId) return;
5513
+ const mapped = Array.isArray(results) ? results.map((item) => normalizeItem(normalizedMap(item))) : [];
5514
+ setItems(mapped);
5515
+ } catch {
5516
+ if (fetchId.current === currentId) {
5517
+ setItems([]);
5518
+ }
5519
+ } finally {
5520
+ if (fetchId.current === currentId) {
5521
+ setIsLoading(false);
5522
+ }
5523
+ }
5524
+ }, debounceMs);
5525
+ };
5526
+ runFetch(query ?? "");
5527
+ return () => {
5528
+ if (debounceHandle.current) {
5529
+ clearTimeout(debounceHandle.current);
5530
+ }
5531
+ fetchId.current += 1;
5532
+ };
5533
+ }, [query, fetchItems, normalizedMap, normalizeItem, debounceMs]);
5534
+ const handleSelect = React4.useMemo(
5535
+ () => (item) => {
5536
+ if (isLoading) return;
5537
+ command(item);
5538
+ },
5539
+ [command, isLoading]
5540
+ );
5541
+ const [selectedIndex, setSelectedIndex] = useKeyboardNavigation(
5542
+ items.length,
5543
+ (index) => {
5544
+ if (!items.length) return;
5545
+ handleSelect(items[index]);
5546
+ },
5547
+ isLoading
5548
+ );
5549
+ React4.useEffect(() => {
5550
+ setSelectedIndex(0);
5551
+ }, [items, setSelectedIndex]);
5552
+ React4.useEffect(() => {
5553
+ const element = itemRefs.current[selectedIndex];
5554
+ if (element) element.scrollIntoView({ block: "nearest" });
5555
+ }, [selectedIndex]);
5556
+ if (!rect) return null;
5557
+ const showEmptyState = !isLoading && items.length === 0;
5558
+ return /* @__PURE__ */ jsxRuntime.jsxs(
5559
+ "div",
5560
+ {
5561
+ ref,
5562
+ style,
5563
+ className: "w-80 max-h-60 overflow-y-auto rounded-lg border bg-white p-1 shadow-lg",
5564
+ children: [
5565
+ isLoading && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "px-3 py-2 text-sm text-muted-foreground", children: "Loading suggestions\u2026" }),
5566
+ showEmptyState && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "px-3 py-2 text-sm text-muted-foreground", children: "No matches" }),
5567
+ items.map((item, idx) => /* @__PURE__ */ jsxRuntime.jsxs(
5568
+ "button",
5569
+ {
5570
+ ref: (el) => {
5571
+ itemRefs.current[idx] = el;
5572
+ },
5573
+ type: "button",
5574
+ onClick: () => handleSelect(item),
5575
+ onMouseMove: () => setSelectedIndex(idx),
5576
+ className: cn(
5577
+ "flex w-full items-center justify-between rounded-md px-3 py-2 text-left text-sm",
5578
+ selectedIndex === idx ? "bg-muted" : "hover:bg-muted",
5579
+ isLoading && "pointer-events-none opacity-60"
5580
+ ),
5581
+ disabled: isLoading,
5582
+ children: [
5583
+ /* @__PURE__ */ jsxRuntime.jsx("span", { children: item.label ?? item.code }),
5584
+ /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "text-xs text-muted-foreground", children: [
5585
+ item.prefix ?? "",
5586
+ item.code ?? ""
5587
+ ] })
5588
+ ]
5589
+ },
5590
+ item.id
5591
+ ))
5592
+ ]
5593
+ }
5594
+ );
5595
+ };
5596
+ var DISALLOWED_MARKS = ["bold", "italic", "link"];
5597
+ var SUGGESTION_DEBOUNCE = 200;
5598
+ var DEFAULT_CHIP_CLASS = "outline-1 outline-muted bg-muted/40 text-foreground";
5599
+ var TokenView = ({ node, editor, getPos }) => {
5600
+ const [isFocused, setIsFocused] = React4__namespace.default.useState(false);
5601
+ React4__namespace.default.useEffect(() => {
5602
+ const handler = () => {
5603
+ const { from, to } = editor.state.selection;
5604
+ const position = getPos();
5605
+ if (position >= from && position + node.nodeSize <= to) {
5606
+ setIsFocused(true);
5607
+ } else {
5608
+ setIsFocused(false);
5609
+ }
5610
+ };
5611
+ editor.on("selectionUpdate", handler);
5612
+ return () => {
5613
+ editor.off("selectionUpdate", handler);
5614
+ };
5615
+ }, [editor, getPos, node.nodeSize]);
5616
+ const remove = () => {
5617
+ const pos = getPos();
5618
+ editor.chain().focus().deleteRange({ from: pos, to: pos + node.nodeSize }).run();
5619
+ };
5620
+ return /* @__PURE__ */ jsxRuntime.jsxs(
5621
+ react.NodeViewWrapper,
5622
+ {
5623
+ as: "span",
5624
+ className: cn(
5625
+ "inline-flex items-center gap-1 rounded-sm px-2 py-0.5 mx-2 text-sm font-medium",
5626
+ DEFAULT_CHIP_CLASS,
5627
+ node.attrs.chipClassName,
5628
+ isFocused && "ring-2 ring-offset-1 ring-ring"
5629
+ ),
5630
+ children: [
5631
+ /* @__PURE__ */ jsxRuntime.jsx("span", { children: node.attrs.label ?? node.attrs.code ?? node.attrs.rawValue }),
5632
+ (node.attrs.code || node.attrs.prefix) && /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "text-xs font-normal opacity-80", children: [
5633
+ "(",
5634
+ node.attrs.prefix,
5635
+ node.attrs.code ?? "",
5636
+ ")"
5637
+ ] }),
5638
+ /* @__PURE__ */ jsxRuntime.jsx(
5639
+ "button",
5640
+ {
5641
+ type: "button",
5642
+ onClick: remove,
5643
+ className: "ml-1 cursor-pointer rounded-sm px-1 text-xs leading-none hover:bg-sus-primary-1/30",
5644
+ "aria-label": `Remove ${node.attrs.label ?? node.attrs.code ?? "token"}`,
5645
+ children: "\xD7"
5646
+ }
5647
+ )
5648
+ ]
5649
+ }
5650
+ );
5651
+ };
5652
+ function normalizeTokenAttrs(attrs, config) {
5653
+ const idSource = attrs.id ?? attrs.code ?? attrs.label ?? Math.random().toString(36).slice(2);
5654
+ const prefix = attrs.prefix ?? config.prefix;
5655
+ const suffix = attrs.code ?? attrs.label ?? "";
5656
+ const outputType = attrs.outputType ?? config.outputType ?? config.type;
5657
+ return {
5658
+ ...attrs,
5659
+ id: String(idSource),
5660
+ type: config.type,
5661
+ prefix,
5662
+ chipClassName: attrs.chipClassName ?? config.chipClassName,
5663
+ rawValue: attrs.rawValue ?? (suffix ? `${prefix}${suffix}` : prefix),
5664
+ outputType
5665
+ };
5666
+ }
5667
+ var isPlainTextContext = (state, range) => {
5668
+ const { doc, schema } = state;
5669
+ const $from = doc.resolve(range.from);
5670
+ if ($from.parent.type.name !== "paragraph") return false;
5671
+ const marks = $from.marks();
5672
+ if (marks.some((mark) => DISALLOWED_MARKS.includes(mark.type.name))) return false;
5673
+ const linkMark = schema.marks.link;
5674
+ if (linkMark && linkMark.isInSet(marks)) return false;
5675
+ const nodeBefore = $from.nodeBefore;
5676
+ if (nodeBefore && nodeBefore.type.name === "token") return false;
5677
+ const nodeAfter = $from.nodeAfter;
5678
+ if (nodeAfter && nodeAfter.type.name === "token") return false;
5679
+ return true;
5680
+ };
5681
+ function suggestionPlugin({ config, onSelect, nodeName, editor, register }) {
5682
+ const pluginKey = new prosemirrorState.PluginKey(`formula-token-${config.type}-${config.prefix}`);
5683
+ const fetchItems = config.fetchItems ?? (async () => []);
5684
+ const mapItem = config.mapItem ?? ((item) => item);
5685
+ const normalizeForConfig = (attrs) => normalizeTokenAttrs(attrs, config);
5686
+ register({ key: pluginKey, config });
5687
+ return Suggestion__default.default({
5688
+ editor,
5689
+ char: config.prefix,
5690
+ pluginKey,
5691
+ allowSpaces: true,
5692
+ startOfLine: false,
5693
+ items: () => [],
5694
+ allow: ({ state, range }) => {
5695
+ if (editor.view.composing) return false;
5696
+ return isPlainTextContext(state, range);
5697
+ },
5698
+ render: () => {
5699
+ let reactRoot = null;
5700
+ let container = null;
5701
+ return {
5702
+ onStart: (props) => {
5703
+ container = document.createElement("div");
5704
+ document.body.appendChild(container);
5705
+ reactRoot = ReactDOM__default.default.createRoot(container);
5706
+ reactRoot.render(
5707
+ /* @__PURE__ */ jsxRuntime.jsx(
5708
+ SuggestionList,
5709
+ {
5710
+ ...props,
5711
+ editor,
5712
+ fetchItems,
5713
+ mapItem,
5714
+ normalizeToken: normalizeForConfig,
5715
+ debounceMs: SUGGESTION_DEBOUNCE
5716
+ }
5717
+ )
5718
+ );
5719
+ },
5720
+ onUpdate: (props) => {
5721
+ reactRoot?.render(
5722
+ /* @__PURE__ */ jsxRuntime.jsx(
5723
+ SuggestionList,
5724
+ {
5725
+ ...props,
5726
+ editor,
5727
+ fetchItems,
5728
+ mapItem,
5729
+ normalizeToken: normalizeForConfig,
5730
+ debounceMs: SUGGESTION_DEBOUNCE
5731
+ }
5732
+ )
5733
+ );
5734
+ },
5735
+ onKeyDown: ({ event }) => {
5736
+ if (event.key === "Escape" || event.key === "Enter" || event.key === "Tab") {
5737
+ event.preventDefault();
5738
+ return true;
5739
+ }
5740
+ return false;
5741
+ },
5742
+ onExit: () => {
5743
+ reactRoot?.unmount();
5744
+ reactRoot = null;
5745
+ container?.remove();
5746
+ container = null;
5747
+ }
5748
+ };
5749
+ },
5750
+ command: ({ range, props, editor: editor2 }) => {
5751
+ const attrs = normalizeTokenAttrs(props, config);
5752
+ const nextChar = editor2.state.doc.textBetween(range.to, range.to + 1, "\n", "\n");
5753
+ const shouldInsertSpace = !nextChar || !/\s/.test(nextChar);
5754
+ const content = shouldInsertSpace ? [
5755
+ { type: nodeName, attrs },
5756
+ { type: "text", text: " " }
5757
+ ] : { type: nodeName, attrs };
5758
+ editor2.chain().focus().insertContentAt(range, content).run();
5759
+ onSelect?.(attrs, config);
5760
+ }
5761
+ });
5762
+ }
5763
+ var createSuggestionMonitorPlugin = (registrations) => new prosemirrorState.Plugin({
5764
+ key: new prosemirrorState.PluginKey("formula-token-suggestion-guard"),
5765
+ view(editorView) {
5766
+ const ensureValid = () => {
5767
+ registrations.forEach(({ key, config }) => {
5768
+ const state = key.getState(editorView.state);
5769
+ if (!state?.active) return;
5770
+ const currentText = editorView.state.doc.textBetween(
5771
+ state.range.from,
5772
+ state.range.to,
5773
+ "\n",
5774
+ "\n"
5775
+ );
5776
+ if (!currentText || !currentText.startsWith(config.prefix)) {
5777
+ const tr = editorView.state.tr.setMeta(key, { exit: true });
5778
+ editorView.dispatch(tr);
5779
+ }
5780
+ });
5781
+ };
5782
+ ensureValid();
5783
+ return {
5784
+ update: () => ensureValid()
5785
+ };
5786
+ }
5787
+ });
5788
+ var createTokenSpacingPlugin = (nodeName) => new prosemirrorState.Plugin({
5789
+ key: new prosemirrorState.PluginKey(`formula-token-spacing-${nodeName}`),
5790
+ appendTransaction(transactions, oldState, newState) {
5791
+ if (!transactions.some((tr2) => tr2.docChanged)) return null;
5792
+ const insertions = [];
5793
+ newState.doc.descendants((node, pos) => {
5794
+ if (node.type.name !== nodeName) return;
5795
+ const beforePos = pos - 1;
5796
+ if (beforePos >= 0) {
5797
+ const beforeChar = newState.doc.textBetween(beforePos, pos, "\n", "\n");
5798
+ if (beforeChar && !/\s/.test(beforeChar)) {
5799
+ insertions.push({ pos, text: " " });
5800
+ }
5801
+ }
5802
+ const afterPos = pos + node.nodeSize;
5803
+ if (afterPos < newState.doc.content.size) {
5804
+ const afterChar = newState.doc.textBetween(afterPos, afterPos + 1, "\n", "\n");
5805
+ if (afterChar && !/\s/.test(afterChar)) {
5806
+ insertions.push({ pos: afterPos, text: " " });
5807
+ }
5808
+ }
5809
+ });
5810
+ if (!insertions.length) return null;
5811
+ const tr = newState.tr;
5812
+ insertions.sort((a, b) => b.pos - a.pos).forEach(({ pos, text }) => {
5813
+ tr.insertText(text, pos);
5814
+ });
5815
+ return tr;
5816
+ }
5817
+ });
5818
+ var Token = core.Node.create({
5819
+ name: "token",
5820
+ inline: true,
5821
+ group: "inline",
5822
+ atom: true,
5823
+ selectable: false,
5824
+ draggable: false,
5825
+ addAttributes() {
5826
+ return {
5827
+ type: { default: "" },
5828
+ id: { default: "" },
5829
+ code: { default: "" },
5830
+ label: { default: "" },
5831
+ prefix: { default: "" },
5832
+ rawValue: { default: "" },
5833
+ chipClassName: { default: "" },
5834
+ outputType: { default: "" }
5835
+ };
5836
+ },
5837
+ parseHTML() {
5838
+ return [{ tag: "token-chip" }];
5839
+ },
5840
+ renderHTML({ HTMLAttributes }) {
5841
+ return ["token-chip", core.mergeAttributes(HTMLAttributes), HTMLAttributes.label || ""];
5842
+ },
5843
+ addNodeView() {
5844
+ return react.ReactNodeViewRenderer(TokenView);
5845
+ },
5846
+ addProseMirrorPlugins() {
5847
+ const configs = this.options.configs ?? [];
5848
+ const registrations = [];
5849
+ const suggestionPlugins = configs.map(
5850
+ (config) => suggestionPlugin({
5851
+ config,
5852
+ onSelect: this.options.onSelect,
5853
+ nodeName: this.name,
5854
+ editor: this.editor,
5855
+ register: (entry) => registrations.push(entry)
5856
+ })
5857
+ );
5858
+ return [
5859
+ ...suggestionPlugins,
5860
+ createTokenSpacingPlugin(this.name),
5861
+ createSuggestionMonitorPlugin(registrations)
5862
+ ];
5863
+ }
5864
+ });
5865
+ var Operator = core.Node.create({
5866
+ name: "operator",
5867
+ inline: true,
5868
+ group: "inline",
5869
+ atom: true,
5870
+ selectable: false,
5871
+ addAttributes() {
5872
+ return {
5873
+ value: { default: null }
5874
+ };
5875
+ },
5876
+ parseHTML() {
5877
+ return [{ tag: "span[operator]" }];
5878
+ },
5879
+ renderHTML({ HTMLAttributes }) {
5880
+ return ["span", core.mergeAttributes({ operator: HTMLAttributes.value }), HTMLAttributes.value];
5881
+ },
5882
+ addKeyboardShortcuts() {
5883
+ const shortcuts = this.options.shortcuts ?? {};
5884
+ return Object.fromEntries(
5885
+ Object.entries(shortcuts).map(([key, value]) => [
5886
+ key,
5887
+ ({ editor }) => {
5888
+ editor.chain().insertContent({ type: "operator", attrs: { value } }).insertContent(" ").run();
5889
+ return true;
5890
+ }
5891
+ ])
5892
+ );
5893
+ }
5894
+ });
5895
+
5896
+ // src/components/formulaEditor/utils/parseFormulaToken.ts
5897
+ var DEFAULT_PARENTHESIS_MAP = {
5898
+ "(": ")",
5899
+ "[": "]",
5900
+ "{": "}"
5901
+ };
5902
+ var createSortedPrefixes = (prefixMap) => Object.keys(prefixMap).sort((a, b) => b.length - a.length);
5903
+ var findPrefixAt = (value, start, prefixes) => prefixes.find((prefix) => value.startsWith(prefix, start));
5904
+ var pushVariableAndInner = (tokens, value, prefixMap, prefixes) => {
5905
+ if (!value) return;
5906
+ tokens.push({ type: "variable", value });
5907
+ for (let cursor = 0; cursor < value.length; ) {
5908
+ const prefix = findPrefixAt(value, cursor, prefixes);
5909
+ if (!prefix) {
5910
+ cursor += 1;
5911
+ continue;
5912
+ }
5913
+ cursor += prefix.length;
5914
+ const rest = value.slice(cursor);
5915
+ const codeMatch = rest.match(/^\w+/);
5916
+ if (codeMatch) {
5917
+ tokens.push({ type: prefixMap[prefix], code: codeMatch[0] });
5918
+ cursor += codeMatch[0].length;
5919
+ }
5920
+ }
5921
+ };
5922
+ var buildPrefixMap = (configs) => {
5923
+ return configs.reduce((acc, config) => {
5924
+ const outputType = config.outputType ?? config.type;
5925
+ if (!acc[config.prefix]) acc[config.prefix] = outputType;
5926
+ return acc;
5927
+ }, {});
5928
+ };
5929
+ var tokenizeFormulaString = (raw, prefixMap) => {
5930
+ const segments = [];
5931
+ if (!raw) return segments;
5932
+ const prefixes = createSortedPrefixes(prefixMap);
5933
+ let buffer = "";
5934
+ const flushBuffer = () => {
5935
+ if (buffer) {
5936
+ segments.push({ kind: "text", value: buffer });
5937
+ buffer = "";
5938
+ }
5939
+ };
5940
+ for (let i = 0; i < raw.length; ) {
5941
+ const prefix = findPrefixAt(raw, i, prefixes);
5942
+ if (prefix) {
5943
+ const rest = raw.slice(i + prefix.length);
5944
+ const codeMatch = rest.match(/^\w+/);
5945
+ if (codeMatch) {
5946
+ flushBuffer();
5947
+ segments.push({ kind: "token", prefix, code: codeMatch[0] });
5948
+ i += prefix.length + codeMatch[0].length;
5949
+ continue;
5950
+ }
5951
+ }
5952
+ buffer += raw[i];
5953
+ i += 1;
5954
+ }
5955
+ flushBuffer();
5956
+ return segments;
5957
+ };
5958
+ var parseFormulaToToken = (text, prefixMap) => {
5959
+ const tokens = [];
5960
+ if (!text) return tokens;
5961
+ const prefixes = createSortedPrefixes(prefixMap);
5962
+ const leadingPrefix = findPrefixAt(text, 0, prefixes);
5963
+ if (!leadingPrefix) {
5964
+ pushVariableAndInner(tokens, text, prefixMap, prefixes);
5965
+ return tokens;
5966
+ }
5967
+ const afterPrefix = text.slice(leadingPrefix.length);
5968
+ const codeMatch = afterPrefix.match(/^\w+/);
5969
+ if (!codeMatch) {
5970
+ pushVariableAndInner(tokens, text, prefixMap, prefixes);
5971
+ return tokens;
5972
+ }
5973
+ tokens.push({ type: prefixMap[leadingPrefix], code: codeMatch[0] });
5974
+ const remaining = afterPrefix.slice(codeMatch[0].length);
5975
+ if (remaining) {
5976
+ let variablePart = remaining;
5977
+ if (remaining.startsWith(":") && remaining.includes("(")) {
5978
+ let depth = 0;
5979
+ let endIndex = -1;
5980
+ for (let i = 0; i < remaining.length; i++) {
5981
+ if (remaining[i] === "(") depth += 1;
5982
+ else if (remaining[i] === ")") depth -= 1;
5983
+ if (depth === 0 && remaining[i] === ")") {
5984
+ endIndex = i;
5985
+ break;
5986
+ }
5987
+ }
5988
+ if (endIndex !== -1) {
5989
+ variablePart = remaining.slice(0, endIndex + 1);
5990
+ }
5991
+ }
5992
+ pushVariableAndInner(tokens, variablePart, prefixMap, prefixes);
5993
+ }
5994
+ return tokens;
5995
+ };
5996
+ var splitOperators = (value, allowedOperators) => {
5997
+ const result = [];
5998
+ if (!value) return result;
5999
+ const sortedOperators = [...allowedOperators].sort((a, b) => b.length - a.length);
6000
+ let buffer = "";
6001
+ for (let i = 0; i < value.length; i += 1) {
6002
+ let matched = false;
6003
+ for (const operator of sortedOperators) {
6004
+ if (operator && value.startsWith(operator, i)) {
6005
+ if (buffer) {
6006
+ result.push({ type: "variable", value: buffer });
6007
+ buffer = "";
6008
+ }
6009
+ result.push({ type: "operator", value: operator });
6010
+ i += operator.length - 1;
6011
+ matched = true;
6012
+ break;
6013
+ }
6014
+ }
6015
+ if (!matched) buffer += value[i];
6016
+ }
6017
+ if (buffer) result.push({ type: "variable", value: buffer });
6018
+ return result;
6019
+ };
6020
+ var parseFormula = (editorJson, prefixMap, allowedOperators) => {
6021
+ const rawParts = [];
6022
+ const tokens = [];
6023
+ const traverse = (nodes2 = []) => {
6024
+ nodes2.forEach((node) => {
6025
+ if (!node) return;
6026
+ switch (node.type) {
6027
+ case "text": {
6028
+ const text = node.text ?? "";
6029
+ if (!text) return;
6030
+ rawParts.push(text);
6031
+ if (text.trim()) {
6032
+ tokens.push({ type: "variable", value: text });
6033
+ }
6034
+ break;
6035
+ }
6036
+ case "operator": {
6037
+ rawParts.push(node.attrs?.value ?? "");
6038
+ tokens.push({ type: "operator", value: node.attrs?.value ?? "" });
6039
+ break;
6040
+ }
6041
+ case "op-library": {
6042
+ rawParts.push(node.attrs?.value ?? "");
6043
+ tokens.push(...splitOperators(node.attrs?.value ?? "", allowedOperators));
6044
+ break;
6045
+ }
6046
+ case "token": {
6047
+ const attrs = node.attrs ?? {};
6048
+ const rawValue = attrs.rawValue ?? `${attrs.prefix ?? ""}${attrs.code ?? ""}`;
6049
+ rawParts.push(rawValue);
6050
+ const outputType = attrs.outputType ?? attrs.type ?? prefixMap[attrs.prefix ?? ""];
6051
+ tokens.push({ ...attrs, type: outputType });
6052
+ break;
6053
+ }
6054
+ default:
6055
+ if (node.content) traverse(node.content);
6056
+ }
6057
+ });
6058
+ };
6059
+ traverse(editorJson?.content ?? []);
6060
+ return { raw: rawParts.join(""), token: tokens };
6061
+ };
6062
+ var validateTokenPrefixes = (rawFormula, prefixMap) => {
6063
+ const prefixes = Object.keys(prefixMap);
6064
+ if (prefixes.length === 0) return { isValid: true };
6065
+ const missingTypes = /* @__PURE__ */ new Set();
6066
+ for (let i = 0; i < rawFormula.length; i += 1) {
6067
+ const prefix = prefixes.find((key) => rawFormula.startsWith(key, i));
6068
+ if (prefix) {
6069
+ const nextChar = rawFormula[i + prefix.length];
6070
+ if (!nextChar || !/\w/.test(nextChar)) {
6071
+ missingTypes.add(prefixMap[prefix]);
6072
+ }
6073
+ i += prefix.length;
6074
+ }
6075
+ }
6076
+ if (missingTypes.size > 0) {
6077
+ return {
6078
+ isValid: false,
6079
+ message: `Invalid token for ${Array.from(missingTypes).join(" and ")}`
6080
+ };
6081
+ }
6082
+ return { isValid: true };
6083
+ };
6084
+ var isValidParentheses = (input) => {
6085
+ const stack = [];
6086
+ const openers = Object.keys(DEFAULT_PARENTHESIS_MAP);
6087
+ const closers = Object.values(DEFAULT_PARENTHESIS_MAP);
6088
+ for (const char of input) {
6089
+ if (openers.includes(char)) {
6090
+ stack.push(char);
6091
+ } else if (closers.includes(char)) {
6092
+ const last = stack.pop();
6093
+ if (!last || DEFAULT_PARENTHESIS_MAP[last] !== char) {
6094
+ return { valid: false };
6095
+ }
6096
+ }
6097
+ }
6098
+ if (stack.length > 0) {
6099
+ return { valid: false };
6100
+ }
6101
+ return { valid: true };
6102
+ };
6103
+ var mapTokensToOutput = (tokens, configs) => {
6104
+ const prefixLookup = /* @__PURE__ */ new Map();
6105
+ const typeLookup = /* @__PURE__ */ new Map();
6106
+ configs.forEach((config) => {
6107
+ prefixLookup.set(config.prefix, config);
6108
+ typeLookup.set(config.outputType ?? config.type, config);
6109
+ });
6110
+ return tokens.map((token) => {
6111
+ if (!token || typeof token !== "object") return token;
6112
+ const tokenPrefix = "prefix" in token ? token.prefix : void 0;
6113
+ const tokenType = "type" in token ? token.type : void 0;
6114
+ const config = (tokenPrefix ? prefixLookup.get(tokenPrefix) : void 0) ?? (tokenType ? typeLookup.get(String(tokenType)) : void 0);
6115
+ if (config?.mapOutput) {
6116
+ return config.mapOutput(token);
6117
+ }
6118
+ return token;
6119
+ });
6120
+ };
6121
+ var DEFAULT_TOKEN_CONFIGS = [
6122
+ {
6123
+ type: "position",
6124
+ prefix: "$",
6125
+ chipClassName: "text-black border border-sus-primary-1 outline-sus-primary-1",
6126
+ fetchItems: async () => []
6127
+ },
6128
+ {
6129
+ type: "impact",
6130
+ prefix: "#",
6131
+ chipClassName: "text-black border border-sus-primary-1 outline-sus-primary-1",
6132
+ fetchItems: async () => []
6133
+ }
6134
+ ];
6135
+ var defaultMapItem = (item) => {
6136
+ const id = item?.id ?? item?.code ?? item?.label ?? Math.random().toString(36).slice(2);
6137
+ return {
6138
+ id: String(id),
6139
+ label: item?.label ?? item?.name ?? String(item?.code ?? item?.id ?? ""),
6140
+ code: item?.code ?? String(item?.id ?? "")
6141
+ };
6142
+ };
6143
+ var looksLikeHTML = (value) => /<\/?[a-z][\s\S]*>/i.test(value.trim());
6144
+ var tryParseJSON = (value) => {
6145
+ try {
6146
+ const parsed = JSON.parse(value);
6147
+ if (parsed && typeof parsed === "object") {
6148
+ return parsed;
6149
+ }
6150
+ } catch {
6151
+ }
6152
+ return null;
6153
+ };
6154
+ var buildDocFromRaw = (raw, prefixMap, configLookup) => {
6155
+ const lines = raw.split(/\r?\n/);
6156
+ let tokenCounter = 0;
6157
+ const paragraphs = lines.map((line) => {
6158
+ const segments = tokenizeFormulaString(line, prefixMap);
6159
+ const content = segments.map((segment) => {
6160
+ if (segment.kind === "text") {
6161
+ return segment.value ? { type: "text", text: segment.value } : null;
6162
+ }
6163
+ const config = configLookup.get(segment.prefix);
6164
+ const fallbackType = prefixMap[segment.prefix];
6165
+ const displayType = config?.type ?? fallbackType;
6166
+ const outputType = config?.outputType ?? fallbackType;
6167
+ if (!displayType && !outputType) {
6168
+ return { type: "text", text: `${segment.prefix}${segment.code}` };
6169
+ }
6170
+ const attrs = {
6171
+ id: `token-${tokenCounter++}`,
6172
+ type: displayType ?? outputType ?? "token",
6173
+ code: segment.code,
6174
+ prefix: segment.prefix,
6175
+ rawValue: `${segment.prefix}${segment.code}`,
6176
+ chipClassName: config?.chipClassName,
6177
+ label: segment.code,
6178
+ outputType: outputType ?? displayType
6179
+ };
6180
+ return { type: "token", attrs };
6181
+ }).filter((node) => Boolean(node));
6182
+ return {
6183
+ type: "paragraph",
6184
+ content: content.length > 0 ? content : [{ type: "text", text: "" }]
6185
+ };
6186
+ });
6187
+ return { type: "doc", content: paragraphs };
6188
+ };
6189
+ var FormulaEditor = ({
6190
+ value,
6191
+ disabled,
6192
+ className,
6193
+ editorClassName,
6194
+ errorMessage,
6195
+ tokenConfigs,
6196
+ operators = defaultOperators,
6197
+ operatorShortcuts = defaultOperatorShortcuts,
6198
+ onChange,
6199
+ onSelectSuggestion,
6200
+ field,
6201
+ fieldState
6202
+ }) => {
6203
+ const [isExpanded, setIsExpanded] = React4.useState(false);
6204
+ const lastEmittedValueRef = React4.useRef(null);
6205
+ const ignorePropValueRef = React4.useRef(false);
6206
+ const normalizedConfigs = React4.useMemo(() => {
6207
+ const configsToUse = tokenConfigs?.length ? tokenConfigs : DEFAULT_TOKEN_CONFIGS;
6208
+ return configsToUse.map((config) => ({
6209
+ ...config,
6210
+ fetchItems: config.fetchItems ?? (async () => []),
6211
+ mapItem: config.mapItem ?? ((item) => ({ ...item, ...defaultMapItem(item) })),
6212
+ outputType: config.outputType ?? config.type
6213
+ }));
6214
+ }, [tokenConfigs]);
6215
+ const prefixMap = React4.useMemo(() => buildPrefixMap(normalizedConfigs), [normalizedConfigs]);
6216
+ const configLookup = React4.useMemo(() => {
6217
+ const lookup = /* @__PURE__ */ new Map();
6218
+ normalizedConfigs.forEach((config) => {
6219
+ lookup.set(config.prefix, config);
6220
+ });
6221
+ return lookup;
6222
+ }, [normalizedConfigs]);
6223
+ const allowedOperators = React4.useMemo(() => operators.map((operator) => operator.value), [operators]);
6224
+ const displayError = errorMessage ?? fieldState?.error?.message;
6225
+ const hasError = Boolean(displayError);
6226
+ const convertValueToContent = React4.useCallback(
6227
+ (input) => {
6228
+ if (!input) return "";
6229
+ const trimmed = input.trim();
6230
+ if (!trimmed) return "";
6231
+ const parsedJSON = tryParseJSON(trimmed);
6232
+ if (parsedJSON && parsedJSON.type === "doc") {
6233
+ return parsedJSON;
6234
+ }
6235
+ if (looksLikeHTML(trimmed)) {
6236
+ return input;
6237
+ }
6238
+ return buildDocFromRaw(input, prefixMap, configLookup);
6239
+ },
6240
+ [configLookup, prefixMap]
6241
+ );
6242
+ const resolvedContent = React4.useMemo(() => convertValueToContent(value), [convertValueToContent, value]);
6243
+ const extensions = React4.useMemo(
6244
+ () => [
6245
+ StarterKit__default.default.configure({ bold: false, italic: false }),
6246
+ Token.configure({ configs: normalizedConfigs, onSelect: onSelectSuggestion }),
6247
+ Operator.configure({ shortcuts: operatorShortcuts })
6248
+ ],
6249
+ [normalizedConfigs, onSelectSuggestion, operatorShortcuts]
6250
+ );
6251
+ const editor = react.useEditor({
6252
+ extensions,
6253
+ content: resolvedContent ?? "",
6254
+ onUpdate: ({ editor: nextEditor }) => {
6255
+ const { raw, token } = parseFormula(nextEditor.getJSON(), prefixMap, allowedOperators);
6256
+ const mappedTokens = mapTokensToOutput(token, normalizedConfigs);
6257
+ lastEmittedValueRef.current = raw;
6258
+ ignorePropValueRef.current = true;
6259
+ onChange?.(raw, mappedTokens);
6260
+ },
6261
+ editorProps: {
6262
+ attributes: {
6263
+ class: cn(
6264
+ isExpanded ? "min-h-[300px] max-h-[300px]" : "min-h-[150px] max-h-[150px]",
6265
+ hasError ? "border border-destructive" : "border focus-visible:border-ring",
6266
+ "w-full rounded-lg bg-white px-4 py-3",
6267
+ "overflow-y-auto whitespace-pre-wrap wrap-break-word focus:outline-none",
6268
+ disabled && "pointer-events-none opacity-60",
6269
+ editorClassName
6270
+ )
6271
+ }
6272
+ }
6273
+ });
6274
+ React4.useEffect(() => {
6275
+ if (!editor) return;
6276
+ editor.setEditable(!disabled);
6277
+ }, [disabled, editor]);
6278
+ React4.useEffect(() => {
6279
+ if (!editor || resolvedContent === void 0) return;
6280
+ if (ignorePropValueRef.current && typeof value === "string" && value === lastEmittedValueRef.current) {
6281
+ ignorePropValueRef.current = false;
6282
+ return;
6283
+ }
6284
+ ignorePropValueRef.current = false;
6285
+ if (typeof resolvedContent === "string") {
6286
+ const currentHTML = editor.getHTML();
6287
+ if (resolvedContent !== currentHTML) {
6288
+ editor.commands.setContent(resolvedContent, { emitUpdate: false });
6289
+ }
6290
+ return;
6291
+ }
6292
+ const currentJSON = JSON.stringify(editor.getJSON());
6293
+ const nextJSON = JSON.stringify(resolvedContent);
6294
+ if (currentJSON !== nextJSON) {
6295
+ editor.commands.setContent(resolvedContent, { emitUpdate: false });
6296
+ }
6297
+ }, [editor, resolvedContent, value]);
6298
+ const insertOperator = (operator) => {
6299
+ editor?.chain().focus().insertContent({ type: "operator", attrs: { value: operator } }).insertContent(" ").run();
6300
+ };
6301
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: cn("w-full space-y-2", className), children: [
6302
+ /* @__PURE__ */ jsxRuntime.jsxs(
6303
+ "div",
6304
+ {
6305
+ ref: field?.ref,
6306
+ onBlur: field?.onBlur,
6307
+ tabIndex: 0,
6308
+ className: "relative",
6309
+ onFocus: () => {
6310
+ if (editor && !editor.isFocused) {
6311
+ editor.chain().focus().run();
6312
+ }
6313
+ },
6314
+ children: [
6315
+ /* @__PURE__ */ jsxRuntime.jsx(react.EditorContent, { editor }),
6316
+ /* @__PURE__ */ jsxRuntime.jsx(
6317
+ Button,
6318
+ {
6319
+ type: "button",
6320
+ variant: "ghost",
6321
+ size: "icon",
6322
+ className: "absolute bottom-2 right-4 h-6 w-6 rounded-full bg-white shadow",
6323
+ onClick: () => setIsExpanded((prev) => !prev),
6324
+ children: isExpanded ? /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Minimize2, { className: "h-4 w-4" }) : /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Maximize2, { className: "h-4 w-4" })
6325
+ }
6326
+ )
6327
+ ]
6328
+ }
6329
+ ),
6330
+ hasError && /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-xs text-destructive", role: "alert", children: displayError }),
6331
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex flex-wrap justify-end gap-2 py-2", children: operators.map((operator) => /* @__PURE__ */ jsxRuntime.jsx(
6332
+ Button,
6333
+ {
6334
+ type: "button",
6335
+ onClick: () => insertOperator(operator.value),
6336
+ className: "min-w-10 rounded-sm px-3 bg-sus-blue-3",
6337
+ disabled,
6338
+ children: operator.label
6339
+ },
6340
+ operator.value
6341
+ )) })
6342
+ ] });
6343
+ };
5385
6344
  function TooltipProvider({
5386
6345
  delayDuration = 0,
5387
6346
  ...props
@@ -5597,7 +6556,7 @@ var GridSettingsModal = ({
5597
6556
  onSaveColumns({ ordering, visibility, pinning }, data.columns);
5598
6557
  }
5599
6558
  };
5600
- const sensors = core.useSensors(core.useSensor(core.PointerSensor, { activationConstraint: { distance: 5 } }));
6559
+ const sensors = core$1.useSensors(core$1.useSensor(core$1.PointerSensor, { activationConstraint: { distance: 5 } }));
5601
6560
  function handleDragEnd(event) {
5602
6561
  const { active, over } = event;
5603
6562
  if (!over || active.id === over.id) return;
@@ -5661,10 +6620,10 @@ var GridSettingsModal = ({
5661
6620
  fields[0]?.fieldId
5662
6621
  ) }),
5663
6622
  /* @__PURE__ */ jsxRuntime.jsx("div", { className: "space-y-3 [&_button:not([disabled])]:cursor-pointer", children: /* @__PURE__ */ jsxRuntime.jsx(
5664
- core.DndContext,
6623
+ core$1.DndContext,
5665
6624
  {
5666
6625
  sensors,
5667
- collisionDetection: core.closestCenter,
6626
+ collisionDetection: core$1.closestCenter,
5668
6627
  modifiers: [modifiers.restrictToParentElement, modifiers.restrictToVerticalAxis],
5669
6628
  onDragStart: () => setIsDragging(true),
5670
6629
  onDragEnd: (event) => {
@@ -8819,6 +9778,7 @@ exports.FormField = FormField;
8819
9778
  exports.FormItem = FormItem;
8820
9779
  exports.FormLabel = FormLabel;
8821
9780
  exports.FormMessage = FormMessage;
9781
+ exports.FormulaEditor = FormulaEditor;
8822
9782
  exports.GridSettingsModal = GridSettingsModal_default;
8823
9783
  exports.HeaderCell = HeaderCell_default;
8824
9784
  exports.Image = Image2;
@@ -8906,20 +9866,29 @@ exports.TooltipTrigger = TooltipTrigger;
8906
9866
  exports.Truncated = truncated_default;
8907
9867
  exports.UI = ui_exports;
8908
9868
  exports.booleanToSelectValue = booleanToSelectValue;
9869
+ exports.buildPrefixMap = buildPrefixMap;
8909
9870
  exports.buttonVariants = buttonVariants;
8910
9871
  exports.cn = cn;
8911
9872
  exports.compareAlphanumeric = compareAlphanumeric;
8912
9873
  exports.debounce = debounce;
9874
+ exports.defaultOperatorShortcuts = defaultOperatorShortcuts;
9875
+ exports.defaultOperators = defaultOperators;
8913
9876
  exports.formatISODate = formatISODate;
8914
9877
  exports.getDialogAlertControls = getDialogAlertControls;
8915
9878
  exports.getDialogTemplates = getDialogTemplates;
8916
9879
  exports.inputVariants = inputVariants;
8917
9880
  exports.isDefined = isDefined;
8918
9881
  exports.isEmptyObject = isEmptyObject;
9882
+ exports.isValidParentheses = isValidParentheses;
9883
+ exports.mapTokensToOutput = mapTokensToOutput;
9884
+ exports.parseFormula = parseFormula;
9885
+ exports.parseFormulaToToken = parseFormulaToToken;
8919
9886
  exports.selectValueToBoolean = selectValueToBoolean;
8920
9887
  exports.spinnerVariants = spinnerVariants;
9888
+ exports.splitOperators = splitOperators;
8921
9889
  exports.stripNullishObject = stripNullishObject;
8922
9890
  exports.throttle = throttle;
9891
+ exports.tokenizeFormulaString = tokenizeFormulaString;
8923
9892
  exports.useFormField = useFormField;
8924
9893
  exports.useGridSettingsStore = useGridSettingsStore_default;
8925
9894
  exports.useHover = useHover_default;
@@ -8930,5 +9899,6 @@ exports.usePreventPageLeaveStore = usePreventPageLeaveStore_default;
8930
9899
  exports.useScreenSize = useScreenSize_default;
8931
9900
  exports.useSidebar = useSidebar;
8932
9901
  exports.useTruncated = useTruncated_default;
9902
+ exports.validateTokenPrefixes = validateTokenPrefixes;
8933
9903
  //# sourceMappingURL=index.js.map
8934
9904
  //# sourceMappingURL=index.js.map