@wildwinter/expr-editor 0.8.0 → 0.10.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.d.cts CHANGED
@@ -214,6 +214,10 @@ interface EditCtx {
214
214
  apply(next: ExprNode | null): void;
215
215
  /** Open a popover anchored to `anchor`; `render(close)` builds the content. */
216
216
  openPopover(anchor: HTMLElement, render: (close: () => void) => Node): void;
217
+ /** Opener for the property right-click menu; falls back to openPopover. The
218
+ * read-only preview sets this (a real popover) while leaving openPopover a
219
+ * no-op, so property pills get their menu but left-click editing stays inert. */
220
+ openMenu?(anchor: HTMLElement, render: (close: () => void) => Node): void;
217
221
  /** Host-provided picker for a flow-node reference arg (e.g. `seen(...)` / `visits(...)`). When set,
218
222
  * the node-ref arg renders as a pill that opens this instead of a free-text field; `onPick` receives
219
223
  * the chosen node id. Absent in dialects / hosts that have no node catalogue. */
@@ -242,6 +246,11 @@ interface EditCtx {
242
246
  label: string;
243
247
  run: () => void;
244
248
  }>;
249
+ /** Read-only presentation hint: render more compactly than the editable tree.
250
+ * Set by the preview (never the interactive editor). Currently collapses a
251
+ * flag-delta call (e.g. `check_flags(@p, +a, -b)`) to `@p: +a -b`, dropping
252
+ * the function name and parentheses. */
253
+ compact?: boolean;
245
254
  }
246
255
 
247
256
  interface ExpressionEditorOptions {
@@ -383,6 +392,18 @@ interface PreviewOptions {
383
392
  scopeOrder?: string[];
384
393
  /** Resolve a node id to a readable label for seen()/visits() node pills. */
385
394
  nodeLabel?: (id: string) => string;
395
+ /** Host actions for a property pill (e.g. "Go to definition"). When set, the
396
+ * preview becomes right-click interactive on property pills (left-click still
397
+ * falls through to the host); otherwise the preview is fully non-interactive. */
398
+ propertyActions?(ref: {
399
+ scope: string;
400
+ name: string;
401
+ }): Array<{
402
+ label: string;
403
+ run: () => void;
404
+ }>;
405
+ /** Where the property menu mounts (default document.body). */
406
+ popoverContainer?: HTMLElement;
386
407
  }
387
408
  /** Read-only pill strip for a condition (name-form). Empty/unparseable is the caller's concern; a
388
409
  * non-empty unparseable string falls back to its raw text. */
package/dist/index.d.ts CHANGED
@@ -214,6 +214,10 @@ interface EditCtx {
214
214
  apply(next: ExprNode | null): void;
215
215
  /** Open a popover anchored to `anchor`; `render(close)` builds the content. */
216
216
  openPopover(anchor: HTMLElement, render: (close: () => void) => Node): void;
217
+ /** Opener for the property right-click menu; falls back to openPopover. The
218
+ * read-only preview sets this (a real popover) while leaving openPopover a
219
+ * no-op, so property pills get their menu but left-click editing stays inert. */
220
+ openMenu?(anchor: HTMLElement, render: (close: () => void) => Node): void;
217
221
  /** Host-provided picker for a flow-node reference arg (e.g. `seen(...)` / `visits(...)`). When set,
218
222
  * the node-ref arg renders as a pill that opens this instead of a free-text field; `onPick` receives
219
223
  * the chosen node id. Absent in dialects / hosts that have no node catalogue. */
@@ -242,6 +246,11 @@ interface EditCtx {
242
246
  label: string;
243
247
  run: () => void;
244
248
  }>;
249
+ /** Read-only presentation hint: render more compactly than the editable tree.
250
+ * Set by the preview (never the interactive editor). Currently collapses a
251
+ * flag-delta call (e.g. `check_flags(@p, +a, -b)`) to `@p: +a -b`, dropping
252
+ * the function name and parentheses. */
253
+ compact?: boolean;
245
254
  }
246
255
 
247
256
  interface ExpressionEditorOptions {
@@ -383,6 +392,18 @@ interface PreviewOptions {
383
392
  scopeOrder?: string[];
384
393
  /** Resolve a node id to a readable label for seen()/visits() node pills. */
385
394
  nodeLabel?: (id: string) => string;
395
+ /** Host actions for a property pill (e.g. "Go to definition"). When set, the
396
+ * preview becomes right-click interactive on property pills (left-click still
397
+ * falls through to the host); otherwise the preview is fully non-interactive. */
398
+ propertyActions?(ref: {
399
+ scope: string;
400
+ name: string;
401
+ }): Array<{
402
+ label: string;
403
+ run: () => void;
404
+ }>;
405
+ /** Where the property menu mounts (default document.body). */
406
+ popoverContainer?: HTMLElement;
386
407
  }
387
408
  /** Read-only pill strip for a condition (name-form). Empty/unparseable is the caller's concern; a
388
409
  * non-empty unparseable string falls back to its raw text. */
package/dist/index.js CHANGED
@@ -441,6 +441,13 @@ function renderNode(node, path, ctx, parentOp, side) {
441
441
  case "flagdelta":
442
442
  return pill(node.sign === "+" ? "flagpos" : "flagneg", `${node.sign}${node.name || "flag"}`, (b) => flagEditor(ctx, path, node, b), { issue, path });
443
443
  case "call": {
444
+ if (ctx.compact && ctx.dialect.functions[node.name]?.flagDeltaArgs && node.args.length >= 2 && node.args[0].kind === "scopedvar" && node.args.slice(1).every((a) => a.kind === "flagdelta")) {
445
+ const row2 = el("span", "exed-call exed-call-flags");
446
+ row2.append(renderNode(node.args[0], [...path, "args", 0], ctx));
447
+ row2.append(el("span", "exed-flagsep", [":"]));
448
+ for (let i = 1; i < node.args.length; i++) row2.append(renderNode(node.args[i], [...path, "args", i], ctx));
449
+ return row2;
450
+ }
444
451
  const row = el("span", "exed-call");
445
452
  row.append(pill("func", node.name, (b) => callEditor(ctx, path, node, b), { issue }));
446
453
  row.append(el("span", "exed-paren", ["("]));
@@ -490,9 +497,10 @@ function attachPropertyMenu(ctx, node, pillEl) {
490
497
  if (!ctx.propertyActions) return;
491
498
  const actions = ctx.propertyActions({ scope: node.scope, name: node.name });
492
499
  if (!actions.length) return;
500
+ const open = ctx.openMenu ?? ctx.openPopover;
493
501
  pillEl.addEventListener("contextmenu", (e) => {
494
502
  e.preventDefault();
495
- ctx.openPopover(pillEl, (close) => {
503
+ open(pillEl, (close) => {
496
504
  const wrap = el("div", "exed-menu");
497
505
  for (const a of actions) wrap.append(button("exed-opt", a.label, () => {
498
506
  a.run();
@@ -1653,6 +1661,7 @@ function frozenCtx(src, o) {
1653
1661
  catalogue: o.catalogue,
1654
1662
  scopeOrder: o.scopeOrder ?? [],
1655
1663
  functions: [],
1664
+ compact: true,
1656
1665
  byPath: v.byPath,
1657
1666
  getAst: () => v.ast,
1658
1667
  apply: () => {
@@ -1662,7 +1671,13 @@ function frozenCtx(src, o) {
1662
1671
  pickNode: () => {
1663
1672
  },
1664
1673
  // enables labelled node pills; never fires (preview is read-only)
1665
- ...o.nodeLabel ? { nodeLabel: o.nodeLabel } : {}
1674
+ ...o.nodeLabel ? { nodeLabel: o.nodeLabel } : {},
1675
+ ...o.propertyActions ? {
1676
+ propertyActions: o.propertyActions,
1677
+ openMenu: (anchor, render) => {
1678
+ openPopover(anchor, render, { container: o.popoverContainer });
1679
+ }
1680
+ } : {}
1666
1681
  };
1667
1682
  return { ctx, ast: v.ast };
1668
1683
  }
@@ -1671,7 +1686,8 @@ function exprPills(src, o) {
1671
1686
  return ast ? renderNode(ast, [], ctx) : el("span", "exed-preview-raw", [src]);
1672
1687
  }
1673
1688
  function renderConditionPreview(src, o) {
1674
- return el("div", "exed-preview", [exprPills(src, o)]);
1689
+ const cls = o.propertyActions ? "exed-preview exed-preview-interactive" : "exed-preview";
1690
+ return el("div", cls, [exprPills(src, o)]);
1675
1691
  }
1676
1692
  function renderEffectsPreview(effects, o) {
1677
1693
  const wrap = el("div", "exed-preview exed-preview-effects");