@wildwinter/expr-editor 0.3.0 → 0.4.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
@@ -224,6 +224,14 @@ interface EditCtx {
224
224
  * next render — used by insert-then-refine templates so the author lands
225
225
  * straight in the first unfilled slot instead of chasing the error ring. */
226
226
  requestFocus?(path: AstPath): void;
227
+ /** When true, a delete that would empty the whole expression is blocked (the
228
+ * Delete affordance is withheld). Used by single-value fields that must
229
+ * always hold at least one term; conditions leave it false (empty = always). */
230
+ requireNonEmpty?: boolean;
231
+ /** Enum values offered when editing the single root literal of a value field
232
+ * (an enum-typed outcome target). Lets the string editor show an enum picker
233
+ * even without a comparison peer. No effect on non-root literals. */
234
+ valueEnumValues?: string[];
227
235
  }
228
236
 
229
237
  interface ExpressionEditorOptions {
@@ -258,6 +266,16 @@ interface ExpressionEditorOptions {
258
266
  * container inside a focus-trapping dialog (Radix, etc.) so opening a pill's
259
267
  * popover does not dismiss the surrounding dialog. */
260
268
  popoverContainer?: HTMLElement;
269
+ /** Block a delete that would empty the whole expression (single-value fields
270
+ * that must always hold a term). Conditions leave this off (empty = always). */
271
+ requireNonEmpty?: boolean;
272
+ /** Enum values for the single root literal of a value field (an enum-typed
273
+ * target): the literal editor then offers them as a picker. Flat mode only. */
274
+ valueEnumValues?: string[];
275
+ /** Flat single-value field: an empty value renders one editable "set a value…"
276
+ * placeholder pill (a root string literal) rather than the condition
277
+ * clause-menu empty state. Pairs with mode:"flat". */
278
+ valueField?: boolean;
261
279
  /** Emitted on every edit (name-form; "" when cleared). */
262
280
  onChange: (src: string) => void;
263
281
  /** Notified when the author starts (true) / stops (false) editing inside a popover
package/dist/index.d.ts CHANGED
@@ -224,6 +224,14 @@ interface EditCtx {
224
224
  * next render — used by insert-then-refine templates so the author lands
225
225
  * straight in the first unfilled slot instead of chasing the error ring. */
226
226
  requestFocus?(path: AstPath): void;
227
+ /** When true, a delete that would empty the whole expression is blocked (the
228
+ * Delete affordance is withheld). Used by single-value fields that must
229
+ * always hold at least one term; conditions leave it false (empty = always). */
230
+ requireNonEmpty?: boolean;
231
+ /** Enum values offered when editing the single root literal of a value field
232
+ * (an enum-typed outcome target). Lets the string editor show an enum picker
233
+ * even without a comparison peer. No effect on non-root literals. */
234
+ valueEnumValues?: string[];
227
235
  }
228
236
 
229
237
  interface ExpressionEditorOptions {
@@ -258,6 +266,16 @@ interface ExpressionEditorOptions {
258
266
  * container inside a focus-trapping dialog (Radix, etc.) so opening a pill's
259
267
  * popover does not dismiss the surrounding dialog. */
260
268
  popoverContainer?: HTMLElement;
269
+ /** Block a delete that would empty the whole expression (single-value fields
270
+ * that must always hold a term). Conditions leave this off (empty = always). */
271
+ requireNonEmpty?: boolean;
272
+ /** Enum values for the single root literal of a value field (an enum-typed
273
+ * target): the literal editor then offers them as a picker. Flat mode only. */
274
+ valueEnumValues?: string[];
275
+ /** Flat single-value field: an empty value renders one editable "set a value…"
276
+ * placeholder pill (a root string literal) rather than the condition
277
+ * clause-menu empty state. Pairs with mode:"flat". */
278
+ valueField?: boolean;
261
279
  /** Emitted on every edit (name-form; "" when cleared). */
262
280
  onChange: (src: string) => void;
263
281
  /** Notified when the author starts (true) / stops (false) editing inside a popover
package/dist/index.js CHANGED
@@ -488,11 +488,14 @@ function numberEditor(ctx, path, value, anchor) {
488
488
  function stringEditor(ctx, path, node, anchor) {
489
489
  const peer = findEnumPeer(ctx.getAst(), path);
490
490
  const enumEntry = peer ? lookup(ctx.catalogue, peer.scope, peer.name) : null;
491
+ const rootValueEnums = path.length === 0 && ctx.valueEnumValues?.length ? ctx.valueEnumValues : null;
492
+ const enumValues = enumEntry?.enumValues?.length ? enumEntry.enumValues : rootValueEnums;
493
+ const enumHeadLabel = enumEntry ? `${displayName(enumEntry, ctx.defaultScope)} value` : "Value";
491
494
  ctx.openPopover(anchor, (close) => {
492
- if (enumEntry?.enumValues?.length) {
495
+ if (enumValues?.length) {
493
496
  const wrap = el("div", "exed-menu");
494
- wrap.append(el("div", "exed-menu-head", [`${displayName(enumEntry, ctx.defaultScope)} value`]));
495
- for (const v of enumEntry.enumValues) {
497
+ wrap.append(el("div", "exed-menu-head", [enumHeadLabel]));
498
+ for (const v of enumValues) {
496
499
  wrap.append(button(`exed-opt${v === node.value ? " sel" : ""}`, v, () => {
497
500
  replace(ctx, path, strLit(v));
498
501
  close();
@@ -564,6 +567,7 @@ function flagEditor(ctx, path, node, anchor) {
564
567
  return wrap;
565
568
  });
566
569
  }
570
+ var canDelete = (ctx, path) => !(ctx.requireNonEmpty && path.length === 0);
567
571
  function callEditor(ctx, path, node, anchor) {
568
572
  ctx.openPopover(anchor, (close) => {
569
573
  const wrap = el("div", "exed-menu");
@@ -575,10 +579,12 @@ function callEditor(ctx, path, node, anchor) {
575
579
  close();
576
580
  }));
577
581
  }
578
- wrap.append(button("exed-opt danger", "Delete", () => {
579
- ctx.apply(deleteAt(ctx.getAst(), path));
580
- close();
581
- }));
582
+ if (canDelete(ctx, path)) {
583
+ wrap.append(button("exed-opt danger", "Delete", () => {
584
+ ctx.apply(deleteAt(ctx.getAst(), path));
585
+ close();
586
+ }));
587
+ }
582
588
  return wrap;
583
589
  });
584
590
  }
@@ -589,10 +595,10 @@ function variableEditor(ctx, path, node, anchor) {
589
595
  replace(ctx, path, scopedVar(entry.scope, entry.name));
590
596
  close();
591
597
  },
592
- footer: button("exed-opt danger", "Delete", () => {
598
+ ...canDelete(ctx, path) ? { footer: button("exed-opt danger", "Delete", () => {
593
599
  ctx.apply(deleteAt(ctx.getAst(), path));
594
600
  close();
595
- })
601
+ }) } : {}
596
602
  }));
597
603
  }
598
604
  function propertyPicker(ctx, opts) {
@@ -1057,6 +1063,8 @@ function mountExpressionEditor(host, opts) {
1057
1063
  requestFocus: (p) => {
1058
1064
  pendingFocus = p;
1059
1065
  },
1066
+ ...opts.requireNonEmpty ? { requireNonEmpty: true } : {},
1067
+ ...opts.valueEnumValues ? { valueEnumValues: opts.valueEnumValues } : {},
1060
1068
  ...opts.pickNode ? { pickNode: opts.pickNode } : {},
1061
1069
  ...opts.nodeLabel ? { nodeLabel: opts.nodeLabel } : {}
1062
1070
  };
@@ -1068,7 +1076,7 @@ function mountExpressionEditor(host, opts) {
1068
1076
  if (raw || v.unparseable) {
1069
1077
  host.append(rawArea());
1070
1078
  } else if (!src.trim() || !v.ast) {
1071
- host.append(emptyState());
1079
+ host.append(opts.valueField ? valueEmptyState() : emptyState());
1072
1080
  } else {
1073
1081
  const ctx = buildCtx(v.ast);
1074
1082
  const body = el("div", "exed-body");
@@ -1112,6 +1120,10 @@ function mountExpressionEditor(host, opts) {
1112
1120
  }));
1113
1121
  return wrap;
1114
1122
  }
1123
+ function valueEmptyState() {
1124
+ const ctx = buildCtx(strLit(""));
1125
+ return el("div", "exed-flat", [renderNode(strLit(""), [], ctx)]);
1126
+ }
1115
1127
  function addTermControl(ctx) {
1116
1128
  const boolean = opts.addTerm === "boolean";
1117
1129
  const ops = boolean ? ["and", "or"] : ARITHMETIC_OPS;