@octaviaflow/core 3.1.0-beta.102 → 3.1.0-beta.104

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.cjs CHANGED
@@ -41339,11 +41339,173 @@ ToolCard.displayName = "ToolCard";
41339
41339
 
41340
41340
  // src/components/ExpressionField/ExpressionField.tsx
41341
41341
  var import_react188 = require("react");
41342
+
41343
+ // src/components/ExpressionField/parseExpression.ts
41344
+ var CALL_NAME_SOURCE = "(?:[A-Z_][A-Z0-9_]*|user\\.[A-Za-z_][A-Za-z0-9_]*)";
41345
+ var CALL_HEAD = new RegExp(`^(${CALL_NAME_SOURCE})\\s*\\(`);
41346
+ function wrappedRegions(value) {
41347
+ const out = [];
41348
+ let i = 0;
41349
+ while (i < value.length) {
41350
+ const open = value.indexOf("{{", i);
41351
+ if (open === -1) break;
41352
+ let depth = 0;
41353
+ let quote = null;
41354
+ let close = -1;
41355
+ for (let j = open; j < value.length; j++) {
41356
+ const ch = value[j];
41357
+ if (quote) {
41358
+ if (ch === quote) quote = null;
41359
+ continue;
41360
+ }
41361
+ if (ch === '"' || ch === "'") {
41362
+ quote = ch;
41363
+ continue;
41364
+ }
41365
+ if (ch === "{" && value[j + 1] === "{") {
41366
+ depth++;
41367
+ j++;
41368
+ continue;
41369
+ }
41370
+ if (ch === "}" && value[j + 1] === "}") {
41371
+ depth--;
41372
+ j++;
41373
+ if (depth === 0) {
41374
+ close = j;
41375
+ break;
41376
+ }
41377
+ }
41378
+ }
41379
+ if (close === -1) break;
41380
+ out.push({ start: open, end: close });
41381
+ i = close + 1;
41382
+ }
41383
+ return out;
41384
+ }
41385
+ function splitArguments(argsText) {
41386
+ const trimmed = argsText.trim();
41387
+ if (!trimmed) return [];
41388
+ const args = [];
41389
+ let current = "";
41390
+ let depth = 0;
41391
+ let braces = 0;
41392
+ let quote = null;
41393
+ for (let i = 0; i < trimmed.length; i++) {
41394
+ const ch = trimmed[i];
41395
+ if (quote) {
41396
+ current += ch;
41397
+ if (ch === quote) quote = null;
41398
+ continue;
41399
+ }
41400
+ if (ch === '"' || ch === "'") {
41401
+ quote = ch;
41402
+ current += ch;
41403
+ continue;
41404
+ }
41405
+ if (ch === "{" && trimmed[i + 1] === "{") {
41406
+ braces++;
41407
+ current += "{{";
41408
+ i++;
41409
+ continue;
41410
+ }
41411
+ if (ch === "}" && trimmed[i + 1] === "}") {
41412
+ braces--;
41413
+ current += "}}";
41414
+ i++;
41415
+ continue;
41416
+ }
41417
+ if (ch === "(") depth++;
41418
+ if (ch === ")") depth--;
41419
+ if (ch === "," && depth === 0 && braces === 0) {
41420
+ args.push(current.trim());
41421
+ current = "";
41422
+ continue;
41423
+ }
41424
+ current += ch;
41425
+ }
41426
+ if (current.trim()) args.push(current.trim());
41427
+ return args;
41428
+ }
41429
+ function parseCall(raw) {
41430
+ const inner = raw.startsWith("{{") && raw.endsWith("}}") ? raw.slice(2, -2).trim() : raw.trim();
41431
+ const head = CALL_HEAD.exec(inner);
41432
+ if (!head) return void 0;
41433
+ const open = inner.indexOf("(");
41434
+ const close = inner.lastIndexOf(")");
41435
+ if (open === -1 || close < open) return void 0;
41436
+ return { name: head[1], args: splitArguments(inner.slice(open + 1, close)) };
41437
+ }
41438
+ function formatCall(name, args) {
41439
+ return `{{${name}(${args.filter((a) => a.trim() !== "").join(", ")})}}`;
41440
+ }
41441
+ function parseExpression(value, pillEveryToken = false) {
41442
+ const segments = [];
41443
+ let cursor = 0;
41444
+ const pushText = (raw) => {
41445
+ if (raw) segments.push({ kind: "text", raw });
41446
+ };
41447
+ for (const { start, end } of wrappedRegions(value)) {
41448
+ pushText(value.slice(cursor, start));
41449
+ const raw = value.slice(start, end + 1);
41450
+ const call = parseCall(raw);
41451
+ if (call) {
41452
+ segments.push({ kind: "call", raw, name: call.name, args: call.args });
41453
+ } else if (raw.startsWith("{{ods://") || pillEveryToken) {
41454
+ segments.push({ kind: "ref", raw });
41455
+ } else {
41456
+ pushText(raw);
41457
+ }
41458
+ cursor = end + 1;
41459
+ }
41460
+ const tail = value.slice(cursor);
41461
+ let last = 0;
41462
+ for (const m of tail.matchAll(/\$\{[^}]+\}/g)) {
41463
+ pushText(tail.slice(last, m.index));
41464
+ segments.push({ kind: "ref", raw: m[0] });
41465
+ last = (m.index ?? 0) + m[0].length;
41466
+ }
41467
+ pushText(tail.slice(last));
41468
+ return segments;
41469
+ }
41470
+ function callAt(raw, path) {
41471
+ let current = parseCall(raw);
41472
+ for (const index of path) {
41473
+ if (!current) return void 0;
41474
+ const arg = current.args[index];
41475
+ if (arg === void 0) return void 0;
41476
+ current = parseCall(arg);
41477
+ }
41478
+ return current;
41479
+ }
41480
+ function formatNestedCall(name, args) {
41481
+ return `${name}(${args.filter((a) => a.trim() !== "").join(", ")})`;
41482
+ }
41483
+ function replaceArgument(raw, path, next) {
41484
+ if (path.length === 0) return next;
41485
+ const [index, ...rest] = path;
41486
+ const call = parseCall(raw);
41487
+ if (!call || index === void 0 || index > call.args.length) return raw;
41488
+ const args = [...call.args];
41489
+ if (args[index] === void 0) args[index] = "";
41490
+ args[index] = rest.length === 0 ? next : replaceArgument(args[index], rest, next);
41491
+ const wrapped = raw.trim().startsWith("{{");
41492
+ return wrapped ? formatCall(call.name, args) : formatNestedCall(call.name, args);
41493
+ }
41494
+ function argumentKind(arg) {
41495
+ const trimmed = arg.trim();
41496
+ if (!trimmed) return "literal";
41497
+ if (parseCall(trimmed)) return "call";
41498
+ if (trimmed.startsWith("{{") || trimmed.startsWith("ods://") || trimmed.startsWith("${")) {
41499
+ return "reference";
41500
+ }
41501
+ return "literal";
41502
+ }
41503
+
41504
+ // src/components/ExpressionField/ExpressionField.tsx
41342
41505
  var import_jsx_runtime174 = require("react/jsx-runtime");
41343
- var REF_TOKEN = /(\{\{ods:\/\/[^}]+\}\}|\$\{[^}]+\})/g;
41344
- var ANY_TOKEN = /(\{\{[^}]+\}\}|\$\{[^}]+\})/g;
41345
- var CALL_TOKEN = /^\{\{\s*(?:[A-Z_][A-Z0-9_]*|user\.[A-Za-z_][A-Za-z0-9_]*)\s*\(/;
41346
41506
  var ZERO_WIDTH = "\u200B";
41507
+ var PILL = "ods-expression-field__pill";
41508
+ var CHIP = "ods-expression-field__chip";
41347
41509
  function shortReferenceLabel(token) {
41348
41510
  if (token.startsWith("{{ods://")) {
41349
41511
  const path = token.slice(8, -2);
@@ -41365,7 +41527,7 @@ function serialize(el) {
41365
41527
  }
41366
41528
  if (node.nodeType !== Node.ELEMENT_NODE) return;
41367
41529
  const elem = node;
41368
- if (elem.classList.contains("ods-expression-field__pill")) {
41530
+ if (elem.classList.contains(PILL) || elem.classList.contains(CHIP)) {
41369
41531
  out += elem.getAttribute("data-value") || "";
41370
41532
  } else if (elem.tagName === "BR") {
41371
41533
  out += "\n";
@@ -41396,6 +41558,19 @@ function insideCallArguments(textBeforeCaret) {
41396
41558
  }
41397
41559
  return depth > 0;
41398
41560
  }
41561
+ function argSummary(arg, nameOf, depth = 2) {
41562
+ const trimmed = arg.trim();
41563
+ const call = parseCall(trimmed);
41564
+ if (call) {
41565
+ if (depth <= 0) return `${nameOf(call.name)}(\u2026)`;
41566
+ const inner = call.args.map((a) => argSummary(a, nameOf, depth - 1)).filter(Boolean).join(", ");
41567
+ return `${nameOf(call.name)}(${inner})`;
41568
+ }
41569
+ if (trimmed.startsWith("{{") || trimmed.startsWith("ods://") || trimmed.startsWith("${")) {
41570
+ return shortReferenceLabel(trimmed.startsWith("ods://") ? `{{${trimmed}}}` : trimmed);
41571
+ }
41572
+ return trimmed;
41573
+ }
41399
41574
  function ExpressionField({
41400
41575
  value,
41401
41576
  onChange,
@@ -41408,6 +41583,7 @@ function ExpressionField({
41408
41583
  className,
41409
41584
  style,
41410
41585
  resolveLabel,
41586
+ resolveCall,
41411
41587
  pillEveryToken = false,
41412
41588
  "aria-label": ariaLabel
41413
41589
  }) {
@@ -41416,28 +41592,42 @@ function ExpressionField({
41416
41592
  const generatedId = (0, import_react188.useId)();
41417
41593
  const fieldId = id ?? generatedId;
41418
41594
  const helperId = `${fieldId}-helper`;
41419
- const splitPattern = pillEveryToken ? ANY_TOKEN : REF_TOKEN;
41420
- const isPillToken = (0, import_react188.useCallback)(
41421
- (part) => {
41422
- if (CALL_TOKEN.test(part)) return false;
41423
- return pillEveryToken ? /^(\{\{[^}]+\}\}|\$\{[^}]+\})$/.test(part) : /^(\{\{ods:\/\/[^}]+\}\}|\$\{[^}]+\})$/.test(part);
41424
- },
41425
- [pillEveryToken]
41426
- );
41595
+ const [editing, setEditing] = (0, import_react188.useState)(null);
41427
41596
  const labelFor = (0, import_react188.useCallback)(
41428
41597
  (token) => resolveLabel?.(token) ?? shortReferenceLabel(token),
41429
41598
  [resolveLabel]
41430
41599
  );
41431
- const makePill = (0, import_react188.useCallback)(
41432
- (token) => {
41433
- const pill = document.createElement("span");
41434
- pill.className = "ods-expression-field__pill";
41435
- pill.contentEditable = "false";
41436
- pill.setAttribute("data-value", token);
41437
- pill.textContent = labelFor(token);
41438
- return pill;
41600
+ const callLabel = (0, import_react188.useCallback)(
41601
+ (name) => resolveCall?.(name)?.label ?? (name.startsWith("user.") ? name.slice(5) : name),
41602
+ [resolveCall]
41603
+ );
41604
+ const chipText = (0, import_react188.useCallback)(
41605
+ (name, args) => {
41606
+ const summary = args.map((a) => argSummary(a, callLabel)).filter(Boolean).join(", ");
41607
+ return `${callLabel(name)}(${summary})`;
41439
41608
  },
41440
- [labelFor]
41609
+ [callLabel]
41610
+ );
41611
+ const makeToken = (0, import_react188.useCallback)(
41612
+ (segment) => {
41613
+ if (segment.kind === "text") return null;
41614
+ const el = document.createElement("span");
41615
+ el.contentEditable = "false";
41616
+ el.setAttribute("data-value", segment.raw);
41617
+ el.tabIndex = -1;
41618
+ if (segment.kind === "ref") {
41619
+ el.className = PILL;
41620
+ el.textContent = labelFor(segment.raw);
41621
+ el.title = segment.raw;
41622
+ } else {
41623
+ el.className = CHIP;
41624
+ el.setAttribute("data-call", segment.name);
41625
+ el.textContent = chipText(segment.name, segment.args);
41626
+ el.title = segment.raw;
41627
+ }
41628
+ return el;
41629
+ },
41630
+ [labelFor, chipText]
41441
41631
  );
41442
41632
  const render = (0, import_react188.useCallback)(
41443
41633
  (raw) => {
@@ -41445,17 +41635,19 @@ function ExpressionField({
41445
41635
  if (!el) return;
41446
41636
  el.innerHTML = "";
41447
41637
  if (!raw) return;
41448
- raw.split(new RegExp(splitPattern.source, "g")).forEach((part) => {
41449
- if (!part) return;
41450
- if (isPillToken(part)) {
41451
- el.appendChild(makePill(part));
41638
+ for (const segment of parseExpression(raw, pillEveryToken)) {
41639
+ if (segment.kind === "text") {
41640
+ el.appendChild(document.createTextNode(segment.raw));
41641
+ continue;
41642
+ }
41643
+ const token = makeToken(segment);
41644
+ if (token) {
41645
+ el.appendChild(token);
41452
41646
  el.appendChild(document.createTextNode(ZERO_WIDTH));
41453
- } else {
41454
- el.appendChild(document.createTextNode(part));
41455
41647
  }
41456
- });
41648
+ }
41457
41649
  },
41458
- [splitPattern, isPillToken, makePill]
41650
+ [pillEveryToken, makeToken]
41459
41651
  );
41460
41652
  (0, import_react188.useEffect)(() => {
41461
41653
  if (fromSelf.current) {
@@ -41476,9 +41668,8 @@ function ExpressionField({
41476
41668
  if (!el || !sel || sel.rangeCount === 0 || !el.contains(sel.anchorNode)) return "";
41477
41669
  const range = sel.getRangeAt(0).cloneRange();
41478
41670
  range.setStart(el, 0);
41479
- const fragment = range.cloneContents();
41480
41671
  const holder = document.createElement("div");
41481
- holder.appendChild(fragment);
41672
+ holder.appendChild(range.cloneContents());
41482
41673
  return serialize(holder);
41483
41674
  }, []);
41484
41675
  const insert = (0, import_react188.useCallback)(
@@ -41488,9 +41679,19 @@ function ExpressionField({
41488
41679
  const sel = window.getSelection();
41489
41680
  const hasCaret = !!sel && sel.rangeCount > 0 && el.contains(sel.anchorNode);
41490
41681
  const asArgument = hasCaret && insideCallArguments(textBeforeCaret());
41491
- const isCall = kind === "function" || /\(\s*\)?\s*$/.test(raw.trim());
41492
- const text = isCall ? raw : asArgument ? raw : raw.startsWith("ods://") ? `{{${raw}}}` : raw;
41493
- const node = !isCall && !asArgument && (text.startsWith("{{") || text.startsWith("${")) ? makePill(text) : document.createTextNode(text);
41682
+ const call = parseCall(raw);
41683
+ const isCall = kind === "function" || !!call;
41684
+ const text = isCall || asArgument ? raw : raw.startsWith("ods://") ? `{{${raw}}}` : raw;
41685
+ let node;
41686
+ if (asArgument) {
41687
+ node = document.createTextNode(text);
41688
+ } else if (isCall && call) {
41689
+ node = makeToken({ kind: "call", raw: text, name: call.name, args: call.args });
41690
+ } else if (text.startsWith("{{") || text.startsWith("${")) {
41691
+ node = makeToken({ kind: "ref", raw: text });
41692
+ } else {
41693
+ node = document.createTextNode(text);
41694
+ }
41494
41695
  const range = hasCaret && sel ? sel.getRangeAt(0) : (() => {
41495
41696
  const r = document.createRange();
41496
41697
  r.selectNodeContents(el);
@@ -41502,27 +41703,113 @@ function ExpressionField({
41502
41703
  const after = document.createTextNode(ZERO_WIDTH);
41503
41704
  range.setStartAfter(node);
41504
41705
  range.insertNode(after);
41505
- if (node.nodeType === Node.TEXT_NODE && isCall) {
41506
- const open = text.lastIndexOf("(");
41507
- const close = text.lastIndexOf(")");
41508
- if (open !== -1 && close === open + 1) {
41509
- range.setStart(node, open + 1);
41510
- range.collapse(true);
41511
- } else {
41512
- range.setStartAfter(after);
41513
- range.collapse(true);
41514
- }
41515
- } else {
41516
- range.setStartAfter(after);
41517
- range.collapse(true);
41518
- }
41706
+ range.setStartAfter(after);
41707
+ range.collapse(true);
41519
41708
  el.focus();
41520
41709
  const selection = window.getSelection();
41521
41710
  selection?.removeAllRanges();
41522
41711
  selection?.addRange(range);
41523
41712
  commit();
41713
+ if (isCall && call && node instanceof HTMLElement && call.args.length === 0) {
41714
+ openEditor(node);
41715
+ }
41716
+ },
41717
+ // eslint-disable-next-line react-hooks/exhaustive-deps
41718
+ [commit, disabled, makeToken, textBeforeCaret]
41719
+ );
41720
+ const selected = (0, import_react188.useRef)(null);
41721
+ const selectToken = (0, import_react188.useCallback)((token) => {
41722
+ selected.current = token;
41723
+ const range = document.createRange();
41724
+ range.selectNode(token);
41725
+ const sel = window.getSelection();
41726
+ sel?.removeAllRanges();
41727
+ sel?.addRange(range);
41728
+ fieldRef.current?.focus();
41729
+ }, []);
41730
+ const openEditor = (0, import_react188.useCallback)((chip) => {
41731
+ const raw = chip.getAttribute("data-value") ?? "";
41732
+ if (!parseCall(raw)) return;
41733
+ const box = chip.getBoundingClientRect();
41734
+ const host = fieldRef.current?.getBoundingClientRect();
41735
+ setEditing({
41736
+ el: chip,
41737
+ raw,
41738
+ rect: {
41739
+ top: box.bottom - (host?.top ?? 0) + 4,
41740
+ left: Math.max(0, box.left - (host?.left ?? 0)),
41741
+ width: Math.max(300, box.width)
41742
+ }
41743
+ });
41744
+ }, []);
41745
+ const applyEditor = (0, import_react188.useCallback)(
41746
+ (raw) => {
41747
+ if (!editing) return;
41748
+ const call = parseCall(raw);
41749
+ if (!call) return;
41750
+ editing.el.setAttribute("data-value", raw);
41751
+ editing.el.textContent = chipText(call.name, call.args);
41752
+ editing.el.title = raw;
41753
+ setEditing(null);
41754
+ commit();
41755
+ fieldRef.current?.focus();
41756
+ },
41757
+ [editing, chipText, commit]
41758
+ );
41759
+ const handleClick = (0, import_react188.useCallback)(
41760
+ (e) => {
41761
+ const target = e.target.closest(`.${PILL}, .${CHIP}`);
41762
+ if (!target) return;
41763
+ selectToken(target);
41764
+ if (!disabled && target.classList.contains(CHIP) && e.detail >= 2) {
41765
+ openEditor(target);
41766
+ }
41767
+ },
41768
+ [disabled, openEditor, selectToken]
41769
+ );
41770
+ const handleCopy = (0, import_react188.useCallback)(
41771
+ (e, cut = false) => {
41772
+ const sel = window.getSelection();
41773
+ if (!sel || sel.rangeCount === 0) return;
41774
+ const holder = document.createElement("div");
41775
+ holder.appendChild(sel.getRangeAt(0).cloneContents());
41776
+ const token = selected.current;
41777
+ const text = serialize(holder) || (token && fieldRef.current?.contains(token) && sel.containsNode(token, true) ? token.getAttribute("data-value") ?? "" : "");
41778
+ if (!text) return;
41779
+ e.preventDefault();
41780
+ e.clipboardData.setData("text/plain", text);
41781
+ if (cut && !disabled) {
41782
+ if (serialize(holder)) sel.getRangeAt(0).deleteContents();
41783
+ else token?.remove();
41784
+ commit();
41785
+ }
41786
+ },
41787
+ [commit, disabled]
41788
+ );
41789
+ const handlePaste = (0, import_react188.useCallback)(
41790
+ (e) => {
41791
+ e.preventDefault();
41792
+ const text = e.clipboardData.getData("text/plain");
41793
+ if (!text) return;
41794
+ document.execCommand("insertText", false, text);
41795
+ const next = serialize(fieldRef.current);
41796
+ fromSelf.current = false;
41797
+ onChange(next);
41524
41798
  },
41525
- [commit, disabled, makePill, textBeforeCaret]
41799
+ [onChange]
41800
+ );
41801
+ const handleKeyDown = (0, import_react188.useCallback)(
41802
+ (e) => {
41803
+ if (e.key !== "Enter" || !e.metaKey) return;
41804
+ const sel = window.getSelection();
41805
+ const node = sel?.anchorNode;
41806
+ const chip = node instanceof HTMLElement ? node.closest(`.${CHIP}`) : node?.parentElement?.closest(`.${CHIP}`) ?? null;
41807
+ if (chip) {
41808
+ e.preventDefault();
41809
+ openEditor(chip);
41810
+ }
41811
+ },
41812
+ [openEditor]
41526
41813
  );
41527
41814
  const handleDragOver = (0, import_react188.useCallback)(
41528
41815
  (e) => {
@@ -41549,42 +41836,244 @@ function ExpressionField({
41549
41836
  },
41550
41837
  [disabled, insert]
41551
41838
  );
41552
- const handlePaste = (0, import_react188.useCallback)((e) => {
41553
- e.preventDefault();
41554
- const text = e.clipboardData.getData("text/plain");
41555
- document.execCommand("insertText", false, text);
41556
- }, []);
41839
+ const handleBlur = (0, import_react188.useCallback)(() => {
41840
+ const el = fieldRef.current;
41841
+ if (!el) return;
41842
+ const next = serialize(el);
41843
+ if (next !== value) {
41844
+ fromSelf.current = true;
41845
+ onChange(next);
41846
+ }
41847
+ if (!editing) render(next);
41848
+ }, [editing, onChange, render, value]);
41557
41849
  return /* @__PURE__ */ (0, import_jsx_runtime174.jsxs)("div", { className: cn("ods-expression-field", className), style, children: [
41558
41850
  label && /* @__PURE__ */ (0, import_jsx_runtime174.jsx)("label", { className: "ods-expression-field__label", htmlFor: fieldId, children: label }),
41559
- /* @__PURE__ */ (0, import_jsx_runtime174.jsx)(
41560
- "div",
41561
- {
41562
- id: fieldId,
41563
- ref: fieldRef,
41564
- role: "textbox",
41565
- "aria-multiline": "true",
41566
- "aria-label": ariaLabel ?? (typeof label === "string" ? label : void 0),
41567
- "aria-describedby": helperText ? helperId : void 0,
41568
- "aria-disabled": disabled || void 0,
41569
- contentEditable: !disabled,
41570
- suppressContentEditableWarning: true,
41571
- "data-placeholder": placeholder,
41572
- className: cn(
41573
- "ods-expression-field__input",
41574
- disabled && "ods-expression-field__input--disabled"
41575
- ),
41576
- style: { minHeight: `${Math.max(1, rows) * 1.5 + 1}rem` },
41577
- onInput: commit,
41578
- onBlur: commit,
41579
- onDragOver: handleDragOver,
41580
- onDragLeave: handleDragLeave,
41581
- onDrop: handleDrop,
41582
- onPaste: handlePaste
41583
- }
41584
- ),
41851
+ /* @__PURE__ */ (0, import_jsx_runtime174.jsxs)("div", { className: "ods-expression-field__body", children: [
41852
+ /* @__PURE__ */ (0, import_jsx_runtime174.jsx)(
41853
+ "div",
41854
+ {
41855
+ id: fieldId,
41856
+ ref: fieldRef,
41857
+ role: "textbox",
41858
+ "aria-multiline": "true",
41859
+ "aria-label": ariaLabel ?? (typeof label === "string" ? label : void 0),
41860
+ "aria-describedby": helperText ? helperId : void 0,
41861
+ "aria-disabled": disabled || void 0,
41862
+ contentEditable: !disabled,
41863
+ suppressContentEditableWarning: true,
41864
+ "data-placeholder": placeholder,
41865
+ className: cn(
41866
+ "ods-expression-field__input",
41867
+ disabled && "ods-expression-field__input--disabled"
41868
+ ),
41869
+ style: { minHeight: `${Math.max(1, rows) * 1.5 + 1}rem` },
41870
+ onInput: commit,
41871
+ onBlur: handleBlur,
41872
+ onClick: handleClick,
41873
+ onKeyDown: handleKeyDown,
41874
+ onCopy: handleCopy,
41875
+ onCut: (e) => handleCopy(e, true),
41876
+ onPaste: handlePaste,
41877
+ onDragOver: handleDragOver,
41878
+ onDragLeave: handleDragLeave,
41879
+ onDrop: handleDrop
41880
+ }
41881
+ ),
41882
+ editing && /* @__PURE__ */ (0, import_jsx_runtime174.jsx)(
41883
+ CallEditor,
41884
+ {
41885
+ raw: editing.raw,
41886
+ rect: editing.rect,
41887
+ nameOf: callLabel,
41888
+ labelFor,
41889
+ resolveCall,
41890
+ onCancel: () => setEditing(null),
41891
+ onApply: applyEditor
41892
+ }
41893
+ )
41894
+ ] }),
41585
41895
  helperText && /* @__PURE__ */ (0, import_jsx_runtime174.jsx)("span", { id: helperId, className: "ods-expression-field__helper", children: helperText })
41586
41896
  ] });
41587
41897
  }
41898
+ function CallEditor({
41899
+ raw,
41900
+ rect,
41901
+ nameOf,
41902
+ labelFor,
41903
+ resolveCall,
41904
+ onApply,
41905
+ onCancel
41906
+ }) {
41907
+ const [draft, setDraft] = (0, import_react188.useState)(raw);
41908
+ const [path, setPath] = (0, import_react188.useState)([]);
41909
+ const [rawMode, setRawMode] = (0, import_react188.useState)(false);
41910
+ const panelRef = (0, import_react188.useRef)(null);
41911
+ (0, import_react188.useEffect)(() => {
41912
+ const onKey = (e) => {
41913
+ if (e.key === "Escape") onCancel();
41914
+ };
41915
+ document.addEventListener("keydown", onKey);
41916
+ return () => document.removeEventListener("keydown", onKey);
41917
+ }, [onCancel]);
41918
+ const current = callAt(draft, path);
41919
+ const signature = current ? resolveCall?.(current.name) : void 0;
41920
+ const names = signature?.args ?? [];
41921
+ const args = current ? current.args.length ? current.args : [""] : [];
41922
+ const crumbs = [];
41923
+ for (let i = 0; i <= path.length; i++) {
41924
+ const at = callAt(draft, path.slice(0, i));
41925
+ if (at) crumbs.push({ label: nameOf(at.name), path: path.slice(0, i) });
41926
+ }
41927
+ const setArg = (index, next) => setDraft((d) => replaceArgument(d, [...path, index], next));
41928
+ const dropInto = (index) => (e) => {
41929
+ const dropped = e.dataTransfer.getData("application/x-fx-insert");
41930
+ if (!dropped) return;
41931
+ e.preventDefault();
41932
+ e.stopPropagation();
41933
+ const kind = e.dataTransfer.getData("application/x-fx-type");
41934
+ const bare = dropped.startsWith("{{") && dropped.endsWith("}}") ? dropped.slice(2, -2).trim() : dropped;
41935
+ setDraft((d) => replaceArgument(d, [...path, index], bare));
41936
+ if (kind === "function" || parseCall(bare)) setPath((pth) => [...pth, index]);
41937
+ };
41938
+ return /* @__PURE__ */ (0, import_jsx_runtime174.jsxs)(
41939
+ "div",
41940
+ {
41941
+ ref: panelRef,
41942
+ className: "ods-expression-field__editor",
41943
+ style: { top: rect.top, left: rect.left, width: rect.width },
41944
+ role: "dialog",
41945
+ "aria-label": `Edit ${crumbs[0]?.label ?? "function"}`,
41946
+ children: [
41947
+ /* @__PURE__ */ (0, import_jsx_runtime174.jsxs)("div", { className: "ods-expression-field__editor-head", children: [
41948
+ /* @__PURE__ */ (0, import_jsx_runtime174.jsx)("nav", { className: "ods-expression-field__editor-crumbs", "aria-label": "Nested functions", children: crumbs.map((crumb, i) => /* @__PURE__ */ (0, import_jsx_runtime174.jsxs)("span", { children: [
41949
+ i > 0 && /* @__PURE__ */ (0, import_jsx_runtime174.jsx)("span", { "aria-hidden": "true", children: " \u203A " }),
41950
+ /* @__PURE__ */ (0, import_jsx_runtime174.jsx)(
41951
+ "button",
41952
+ {
41953
+ type: "button",
41954
+ disabled: i === crumbs.length - 1,
41955
+ onClick: () => setPath(crumb.path),
41956
+ children: crumb.label
41957
+ }
41958
+ )
41959
+ ] }, crumb.path.join(".") || "root")) }),
41960
+ /* @__PURE__ */ (0, import_jsx_runtime174.jsx)(
41961
+ "button",
41962
+ {
41963
+ type: "button",
41964
+ className: "ods-expression-field__editor-link",
41965
+ onClick: () => setRawMode((r) => !r),
41966
+ children: rawMode ? "Back to fields" : "Edit as text"
41967
+ }
41968
+ )
41969
+ ] }),
41970
+ rawMode ? /* @__PURE__ */ (0, import_jsx_runtime174.jsx)(
41971
+ "textarea",
41972
+ {
41973
+ className: "ods-expression-field__editor-raw",
41974
+ value: draft,
41975
+ rows: 3,
41976
+ "aria-label": "Expression",
41977
+ onChange: (e) => setDraft(e.target.value)
41978
+ }
41979
+ ) : /* @__PURE__ */ (0, import_jsx_runtime174.jsxs)("div", { className: "ods-expression-field__editor-rows", children: [
41980
+ args.map((arg, index) => {
41981
+ const kind = argumentKind(arg);
41982
+ const rowLabel = names[index] ?? `argument ${index + 1}`;
41983
+ const nested = kind === "call" ? parseCall(arg) : void 0;
41984
+ return (
41985
+ // eslint-disable-next-line react/no-array-index-key
41986
+ /* @__PURE__ */ (0, import_jsx_runtime174.jsxs)("div", { className: "ods-expression-field__editor-row", children: [
41987
+ /* @__PURE__ */ (0, import_jsx_runtime174.jsx)("span", { id: `arg-${index}-label`, children: rowLabel }),
41988
+ nested ? /* @__PURE__ */ (0, import_jsx_runtime174.jsxs)(
41989
+ "div",
41990
+ {
41991
+ className: "ods-expression-field__editor-nested",
41992
+ onDragOver: (e) => {
41993
+ if (e.dataTransfer.types.includes("application/x-fx-insert")) e.preventDefault();
41994
+ },
41995
+ onDrop: dropInto(index),
41996
+ children: [
41997
+ /* @__PURE__ */ (0, import_jsx_runtime174.jsx)("code", { children: argSummary(arg, nameOf) }),
41998
+ /* @__PURE__ */ (0, import_jsx_runtime174.jsxs)("span", { children: [
41999
+ /* @__PURE__ */ (0, import_jsx_runtime174.jsx)("button", { type: "button", onClick: () => setPath([...path, index]), children: "Open" }),
42000
+ /* @__PURE__ */ (0, import_jsx_runtime174.jsx)("button", { type: "button", onClick: () => setArg(index, ""), "aria-label": `Clear ${rowLabel}`, children: "Clear" })
42001
+ ] })
42002
+ ]
42003
+ }
42004
+ ) : kind === "reference" ? (
42005
+ // A reference is a value from somewhere else — named, never
42006
+ // its raw `ods://…` path, here as much as in the field.
42007
+ /* @__PURE__ */ (0, import_jsx_runtime174.jsxs)(
42008
+ "div",
42009
+ {
42010
+ className: "ods-expression-field__editor-ref",
42011
+ onDragOver: (e) => {
42012
+ if (e.dataTransfer.types.includes("application/x-fx-insert")) e.preventDefault();
42013
+ },
42014
+ onDrop: dropInto(index),
42015
+ children: [
42016
+ /* @__PURE__ */ (0, import_jsx_runtime174.jsx)(
42017
+ "span",
42018
+ {
42019
+ className: "ods-expression-field__pill",
42020
+ title: arg,
42021
+ "aria-labelledby": `arg-${index}-label`,
42022
+ children: labelFor(arg.startsWith("ods://") ? `{{${arg}}}` : arg)
42023
+ }
42024
+ ),
42025
+ /* @__PURE__ */ (0, import_jsx_runtime174.jsx)("button", { type: "button", onClick: () => setArg(index, ""), "aria-label": `Clear ${rowLabel}`, children: "Clear" })
42026
+ ]
42027
+ }
42028
+ )
42029
+ ) : /* @__PURE__ */ (0, import_jsx_runtime174.jsx)(
42030
+ "input",
42031
+ {
42032
+ "aria-labelledby": `arg-${index}-label`,
42033
+ value: arg,
42034
+ placeholder: 'a value, "text", or drag one in',
42035
+ "data-kind": kind,
42036
+ onChange: (e) => setArg(index, e.target.value),
42037
+ onDragOver: (e) => {
42038
+ if (e.dataTransfer.types.includes("application/x-fx-insert")) e.preventDefault();
42039
+ },
42040
+ onDrop: dropInto(index)
42041
+ }
42042
+ )
42043
+ ] }, index)
42044
+ );
42045
+ }),
42046
+ /* @__PURE__ */ (0, import_jsx_runtime174.jsx)(
42047
+ "button",
42048
+ {
42049
+ type: "button",
42050
+ className: "ods-expression-field__editor-link",
42051
+ onClick: () => setDraft((d) => {
42052
+ const at = callAt(d, path);
42053
+ if (!at) return d;
42054
+ const next = [...at.args, '""'];
42055
+ return path.length === 0 ? formatCall(at.name, next) : replaceArgument(d, path, `${at.name}(${next.join(", ")})`);
42056
+ }),
42057
+ children: "+ argument"
42058
+ }
42059
+ )
42060
+ ] }),
42061
+ /* @__PURE__ */ (0, import_jsx_runtime174.jsxs)("div", { className: "ods-expression-field__editor-foot", children: [
42062
+ /* @__PURE__ */ (0, import_jsx_runtime174.jsx)("button", { type: "button", onClick: onCancel, children: "Cancel" }),
42063
+ /* @__PURE__ */ (0, import_jsx_runtime174.jsx)(
42064
+ "button",
42065
+ {
42066
+ type: "button",
42067
+ className: "ods-expression-field__editor-primary",
42068
+ onClick: () => onApply(draft),
42069
+ children: "Apply"
42070
+ }
42071
+ )
42072
+ ] })
42073
+ ]
42074
+ }
42075
+ );
42076
+ }
41588
42077
 
41589
42078
  // src/components/TopBar/TopBar.tsx
41590
42079
  var import_jsx_runtime175 = require("react/jsx-runtime");