@wildwinter/expr-editor 0.10.3 → 0.12.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/README.md CHANGED
@@ -35,6 +35,20 @@ Notable options beyond the basics:
35
35
  multi-step flow (e.g. tag → operator → threshold) with no editor changes.
36
36
  Templates without a wizard insert-then-refine: the editor auto-opens the
37
37
  first unfilled slot of the inserted clause.
38
+ - **Qualities** (the `quality` property type) need nothing from the host beyond a
39
+ catalogue entry carrying `stages` — the ladder, in order. The editor then offers
40
+ the property in a comparison clause, gives it the **ordering** operators (a
41
+ ladder exists for "at or past a stage"), lists its stages wherever a value is
42
+ picked, labels that list "stage", and seeds an outcome on it with
43
+ `advance(@ref)` — the form that keeps a story insertion-safe, because it names
44
+ no destination. That seed is the same on both paths (adding an effect and
45
+ re-pointing one), via `initialValueFor`, and a quality skips the value wizard
46
+ entirely: there is no question left worth asking. A host still passing a
47
+ ladder through `enumValues` keeps its value picker, but not the ordering
48
+ affordances.
49
+ - `valueWizard`'s `expectedChoices` supersedes `expectedEnumValues` — one
50
+ channel for "the target's closed set of values", filled with `choicesOf(entry)`
51
+ so a quality's ladder arrives with its type. The old name still works.
38
52
  - `onEditingChange(editing)` — fires as popover micro-editors open/close, so the
39
53
  host can suppress its own validation display mid-edit.
40
54
  - `messages: false` — hide the editor's internal validation list when the host
package/dist/index.cjs CHANGED
@@ -222,6 +222,10 @@ var BINARY_LABEL = {
222
222
  var UNARY_LABEL = { not: "NOT", neg: "\u2212" };
223
223
  var COMPARISON_OPS = ["==", "!=", ">", ">=", "<", "<="];
224
224
  var ARITHMETIC_OPS = ["+", "-", "*", "/"];
225
+ var COMPARABLE_TYPES = ["boolean", "number", "string", "enum", "quality"];
226
+ var comparisonOpsFor = (t) => t === "number" || t === "quality" ? COMPARISON_OPS : EQUALITY_OPS;
227
+ var EQUALITY_OPS = ["==", "!="];
228
+ var rhsTypesFor = (t) => t === "number" ? ["number"] : t === "boolean" ? ["boolean"] : t === "quality" ? ["quality"] : t === "enum" ? ["enum", "string"] : ["string", "enum"];
225
229
  function opSwapGroup(op) {
226
230
  if (COMPARISON_OPS.includes(op)) return COMPARISON_OPS;
227
231
  if (ARITHMETIC_OPS.includes(op)) return ARITHMETIC_OPS;
@@ -251,6 +255,12 @@ function formatNumber(n) {
251
255
  }
252
256
 
253
257
  // src/schema.ts
258
+ var choicesOf = (e) => {
259
+ if (!e) return void 0;
260
+ if (e.type === "quality") return e.stages?.length ? e.stages : e.enumValues;
261
+ if (e.type === "enum") return e.enumValues;
262
+ return void 0;
263
+ };
254
264
  var refOf = (e, defaultScope) => e.scope === defaultScope ? `@${e.name}` : `@${e.scope}.${e.name}`;
255
265
  var displayName = (e, defaultScope) => e.scope === defaultScope ? e.name : `${e.scope}.${e.name}`;
256
266
  function filterCatalogue(entries, filter = {}) {
@@ -554,8 +564,9 @@ function stringEditor(ctx, path, node, anchor) {
554
564
  const peer = findEnumPeer(ctx.getAst(), path);
555
565
  const enumEntry = peer ? lookup(ctx.catalogue, peer.scope, peer.name) : null;
556
566
  const rootValueEnums = path.length === 0 && ctx.valueEnumValues?.length ? ctx.valueEnumValues : null;
557
- const enumValues = enumEntry?.enumValues?.length ? enumEntry.enumValues : rootValueEnums;
558
- const enumHeadLabel = enumEntry ? `${displayName(enumEntry, ctx.defaultScope)} value` : "Value";
567
+ const peerChoices = choicesOf(enumEntry);
568
+ const enumValues = peerChoices?.length ? peerChoices : rootValueEnums;
569
+ const enumHeadLabel = enumEntry ? `${displayName(enumEntry, ctx.defaultScope)} ${enumEntry.type === "quality" ? "stage" : "value"}` : "Value";
559
570
  ctx.openPopover(anchor, (close) => {
560
571
  if (enumValues?.length) {
561
572
  const wrap = el("div", "exed-menu");
@@ -705,7 +716,6 @@ function propertyPicker(ctx, opts) {
705
716
  }
706
717
 
707
718
  // src/clausewizard.ts
708
- var EQUALITY = ["==", "!="];
709
719
  var OP_WORD = {
710
720
  "==": "equals",
711
721
  "!=": "not equal to",
@@ -719,8 +729,6 @@ var opButton = (o, onClick) => {
719
729
  b.append(el("span", "exed-opt-name", [BINARY_LABEL[o]]), el("span", "exed-opt-purpose", [OP_WORD[o] ?? ""]));
720
730
  return b;
721
731
  };
722
- var opsForType = (t) => t === "number" ? COMPARISON_OPS : EQUALITY;
723
- var rhsTypesFor = (t) => t === "number" ? ["number"] : t === "boolean" ? ["boolean"] : t === "enum" ? ["enum", "string"] : ["string", "enum"];
724
732
  function header(host, title, back, cancel) {
725
733
  const h = el("div", "exed-vwiz-head");
726
734
  if (back) h.append(button("exed-vwiz-back", "\u2190", back, "Back"));
@@ -751,8 +759,8 @@ function valueStep(host, w, lhs, op, back, cancel, commit) {
751
759
  const row = el("div", "exed-field-row");
752
760
  row.append(button("exed-opt", "true", () => done(boolLit(true))), button("exed-opt", "false", () => done(boolLit(false))));
753
761
  body.append(row);
754
- } else if (lhs.type === "enum" && lhs.enumValues?.length) {
755
- for (const v of lhs.enumValues) body.append(button("exed-opt", v, () => done(strLit(v))));
762
+ } else if (choicesOf(lhs)?.length) {
763
+ for (const v of choicesOf(lhs)) body.append(button("exed-opt", v, () => done(strLit(v))));
756
764
  } else if (lhs.type === "number") {
757
765
  body.append(textField({ caption: "Number", placeholder: "e.g. 3", validate: (v) => v.trim() !== "" && Number.isFinite(Number(v)), onCommit: (v) => done(numLit(Number(v))) }));
758
766
  } else {
@@ -774,8 +782,13 @@ function comparisonWizard(host, w, commit, cancel) {
774
782
  const body = el("div", "exed-vwiz-body");
775
783
  host.append(body);
776
784
  body.append(propertyPicker(pickCtxOf(w), {
777
- accept: ["boolean", "number", "string", "enum"],
778
- onPick: (e) => pickOp({ ref: refDisplay(w, e), type: e.type, ...e.enumValues ? { enumValues: e.enumValues } : {} })
785
+ accept: COMPARABLE_TYPES,
786
+ onPick: (e) => pickOp({
787
+ ref: refDisplay(w, e),
788
+ type: e.type,
789
+ ...e.enumValues ? { enumValues: e.enumValues } : {},
790
+ ...e.stages ? { stages: e.stages } : {}
791
+ })
779
792
  }));
780
793
  };
781
794
  const pickOp = (lhs) => {
@@ -783,7 +796,7 @@ function comparisonWizard(host, w, commit, cancel) {
783
796
  header(host, el("span", void 0, ["Operator for ", mono(lhs.ref)]), pickLhs, cancel);
784
797
  const body = el("div", "exed-vwiz-body");
785
798
  host.append(body);
786
- for (const o of opsForType(lhs.type)) {
799
+ for (const o of comparisonOpsFor(lhs.type)) {
787
800
  body.append(opButton(o, () => valueStep(host, w, lhs, o, () => pickOp(lhs), cancel, commit)));
788
801
  }
789
802
  };
@@ -1337,7 +1350,9 @@ function valueWizard(opts) {
1337
1350
  host.append(head("Pick a value"));
1338
1351
  const body = el("div", "exed-vwiz-body");
1339
1352
  host.append(body);
1340
- let kind = opts.expectedType === "number" ? "number" : opts.expectedType === "string" ? "text" : opts.expectedType === "boolean" ? "bool" : opts.expectedType === "enum" && opts.expectedEnumValues?.length ? "enum" : "menu";
1353
+ const choices = opts.expectedChoices?.length ? opts.expectedChoices : opts.expectedEnumValues;
1354
+ const isStage = opts.expectedType === "quality";
1355
+ let kind = opts.expectedType === "number" ? "number" : opts.expectedType === "string" ? "text" : opts.expectedType === "boolean" ? "bool" : (opts.expectedType === "enum" || opts.expectedType === "quality") && choices?.length ? "enum" : "menu";
1341
1356
  const draw = () => {
1342
1357
  body.replaceChildren();
1343
1358
  const other = () => button("exed-vwiz-other", "\u21A9 a different kind", () => {
@@ -1362,7 +1377,7 @@ function valueWizard(opts) {
1362
1377
  kind = "bool";
1363
1378
  draw();
1364
1379
  }));
1365
- if (opts.expectedEnumValues?.length) body.append(optBtn("A listed value", () => {
1380
+ if (choices?.length) body.append(optBtn(isStage ? "A stage" : "A listed value", () => {
1366
1381
  kind = "enum";
1367
1382
  draw();
1368
1383
  }));
@@ -1386,7 +1401,8 @@ function valueWizard(opts) {
1386
1401
  break;
1387
1402
  }
1388
1403
  case "enum":
1389
- for (const v of opts.expectedEnumValues ?? []) body.append(optBtn(v, () => opts.onCommit(enumSrc(v))));
1404
+ body.append(el("div", "exed-vwiz-note", [isStage ? "Stages, in ladder order:" : ""]));
1405
+ for (const v of choices ?? []) body.append(optBtn(v, () => opts.onCommit(enumSrc(v))));
1390
1406
  body.append(other());
1391
1407
  break;
1392
1408
  }
@@ -1435,14 +1451,23 @@ function removeArgAt(list, i, argIdx) {
1435
1451
  cur.args.splice(argIdx, 1);
1436
1452
  return next;
1437
1453
  }
1438
- function seedValueSrc(type, enumValues) {
1454
+ function initialValueFor(entry, defaultScope) {
1455
+ const ref = refOf(entry, defaultScope);
1456
+ return {
1457
+ src: seedValueSrc(entry.type, choicesOf(entry), ref),
1458
+ ask: entry.type !== "quality"
1459
+ };
1460
+ }
1461
+ function seedValueSrc(type, values, ref) {
1439
1462
  switch (type) {
1440
1463
  case "boolean":
1441
1464
  return "true";
1442
1465
  case "number":
1443
1466
  return "0";
1444
1467
  case "enum":
1445
- return JSON.stringify(enumValues?.[0] ?? "");
1468
+ return JSON.stringify(values?.[0] ?? "");
1469
+ case "quality":
1470
+ return ref ? `advance(${ref})` : JSON.stringify(values?.[0] ?? "");
1446
1471
  case "string":
1447
1472
  return '""';
1448
1473
  default:
@@ -1472,7 +1497,9 @@ function mountEffectsEditor(host, opts) {
1472
1497
  scopeOrder: opts.scopeOrder ?? [],
1473
1498
  defaultScope,
1474
1499
  ...expected?.type ? { expectedType: expected.type } : {},
1475
- ...expected?.enumValues ? { expectedEnumValues: expected.enumValues } : {},
1500
+ // choicesOf, never a raw field: a quality's ladder lives in `stages`, and reading `enumValues`
1501
+ // here told the wizard the type and withheld the values.
1502
+ ...choicesOf(expected) ? { expectedChoices: choicesOf(expected) } : {},
1476
1503
  onCommit: (src) => {
1477
1504
  onCommit(src);
1478
1505
  close();
@@ -1540,7 +1567,7 @@ function mountEffectsEditor(host, opts) {
1540
1567
  const row = el("div", "exed-effect exed-effect-set");
1541
1568
  const targetBtn = button("exed-pill exed-pill-prop", eff.target || "(pick property)", (e) => {
1542
1569
  pickTarget(e.currentTarget, void 0, (entry) => {
1543
- commit(updateAt(effects, i, { target: refOf(entry, defaultScope), value: seedValueSrc(entry.type, entry.enumValues) }));
1570
+ commit(updateAt(effects, i, { target: refOf(entry, defaultScope), value: initialValueFor(entry, defaultScope).src }));
1544
1571
  });
1545
1572
  }, "change the target property");
1546
1573
  row.append(targetBtn, el("span", "exed-effect-eq", ["="]));
@@ -1626,7 +1653,14 @@ function mountEffectsEditor(host, opts) {
1626
1653
  const bar = el("div", "exed-effect-addbar");
1627
1654
  bar.append(button("exed-eff-add", "+ set property", (e) => {
1628
1655
  const anchor = e.currentTarget;
1629
- pickTarget(anchor, void 0, (entry) => openValueWizard(anchor, entry, (src) => commit(addSet(effects, refOf(entry, defaultScope), src))));
1656
+ pickTarget(anchor, void 0, (entry) => {
1657
+ const { src, ask } = initialValueFor(entry, defaultScope);
1658
+ if (!ask) {
1659
+ commit(addSet(effects, refOf(entry, defaultScope), src));
1660
+ return;
1661
+ }
1662
+ openValueWizard(anchor, entry, (chosen) => commit(addSet(effects, refOf(entry, defaultScope), chosen)));
1663
+ });
1630
1664
  }));
1631
1665
  if (opts.allowEmit !== false) {
1632
1666
  bar.append(button("exed-eff-add", "+ emit event", (e) => editEvent(e.currentTarget, "", (name) => commit(addEmit(effects, name)))));
@@ -1716,7 +1750,9 @@ function renderEffectsPreview(effects, o) {
1716
1750
 
1717
1751
  exports.ARITHMETIC_OPS = ARITHMETIC_OPS;
1718
1752
  exports.BINARY_LABEL = BINARY_LABEL;
1753
+ exports.COMPARABLE_TYPES = COMPARABLE_TYPES;
1719
1754
  exports.COMPARISON_OPS = COMPARISON_OPS;
1755
+ exports.EQUALITY_OPS = EQUALITY_OPS;
1720
1756
  exports.UNARY_LABEL = UNARY_LABEL;
1721
1757
  exports.addArg = addArg;
1722
1758
  exports.addChildToContainer = addChildToContainer;
@@ -1727,6 +1763,8 @@ exports.binary = binary;
1727
1763
  exports.boolLit = boolLit;
1728
1764
  exports.buildSubGroupClause = buildSubGroupClause;
1729
1765
  exports.callNode = callNode;
1766
+ exports.choicesOf = choicesOf;
1767
+ exports.comparisonOpsFor = comparisonOpsFor;
1730
1768
  exports.deleteAt = deleteAt;
1731
1769
  exports.displayName = displayName;
1732
1770
  exports.filterCatalogue = filterCatalogue;
@@ -1759,6 +1797,7 @@ exports.removeArgAt = removeArgAt;
1759
1797
  exports.removeAt = removeAt;
1760
1798
  exports.renderConditionPreview = renderConditionPreview;
1761
1799
  exports.renderEffectsPreview = renderEffectsPreview;
1800
+ exports.rhsTypesFor = rhsTypesFor;
1762
1801
  exports.scopedVar = scopedVar;
1763
1802
  exports.searchCatalogue = searchCatalogue;
1764
1803
  exports.seedValueSrc = seedValueSrc;