@timeax/digital-service-engine 0.0.1 → 0.0.3

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.
@@ -3260,11 +3260,6 @@ function hasAnyServiceOption(f) {
3260
3260
  var _a;
3261
3261
  return ((_a = f.options) != null ? _a : []).some((o) => isFiniteNumber(o.service_id));
3262
3262
  }
3263
- function isBoundTo(f, tagId) {
3264
- const b = f.bind_id;
3265
- if (!b) return false;
3266
- return Array.isArray(b) ? b.includes(tagId) : b === tagId;
3267
- }
3268
3263
  function getByPath(obj, path) {
3269
3264
  if (!path) return void 0;
3270
3265
  const parts = path.split(".");
@@ -3350,33 +3345,214 @@ function withAffected(details, ids) {
3350
3345
  return { ...details != null ? details : {}, affectedIds: ids };
3351
3346
  }
3352
3347
 
3348
+ // src/core/node-map.ts
3349
+ function buildNodeMap(props) {
3350
+ var _a, _b, _c;
3351
+ const map = /* @__PURE__ */ new Map();
3352
+ for (const t of (_a = props.filters) != null ? _a : []) {
3353
+ if (!map.has(t.id)) map.set(t.id, { kind: "tag", id: t.id, node: t });
3354
+ }
3355
+ for (const f of (_b = props.fields) != null ? _b : []) {
3356
+ if (!map.has(f.id)) map.set(f.id, { kind: "field", id: f.id, node: f });
3357
+ for (const o of (_c = f.options) != null ? _c : []) {
3358
+ if (!map.has(o.id))
3359
+ map.set(o.id, {
3360
+ kind: "option",
3361
+ id: o.id,
3362
+ node: o,
3363
+ fieldId: f.id
3364
+ });
3365
+ }
3366
+ }
3367
+ return map;
3368
+ }
3369
+ function resolveTrigger(trigger, nodeMap) {
3370
+ const idx = trigger.indexOf("::");
3371
+ if (idx !== -1) {
3372
+ const fieldId = trigger.slice(0, idx);
3373
+ const optionId = trigger.slice(idx + 2);
3374
+ return { kind: "composite", triggerKey: trigger, fieldId, optionId };
3375
+ }
3376
+ const direct = nodeMap.get(trigger);
3377
+ if (!direct) return void 0;
3378
+ if (direct.kind === "option") {
3379
+ return {
3380
+ kind: "option",
3381
+ triggerKey: trigger,
3382
+ id: direct.id,
3383
+ fieldId: direct.fieldId
3384
+ };
3385
+ }
3386
+ return { kind: direct.kind, triggerKey: trigger, id: direct.id };
3387
+ }
3388
+
3389
+ // src/core/visibility.ts
3390
+ function visibleFieldIdsUnder(props, tagId, opts = {}) {
3391
+ var _a, _b, _c, _d, _e, _f, _g, _h, _i, _j;
3392
+ const tags = (_a = props.filters) != null ? _a : [];
3393
+ const fields = (_b = props.fields) != null ? _b : [];
3394
+ const tagById = new Map(tags.map((t) => [t.id, t]));
3395
+ const tag = tagById.get(tagId);
3396
+ if (!tag) return [];
3397
+ const nodeMap = buildNodeMap(props);
3398
+ const lineageDepth = /* @__PURE__ */ new Map();
3399
+ {
3400
+ const guard = /* @__PURE__ */ new Set();
3401
+ let cur = tag;
3402
+ let d = 0;
3403
+ while (cur && !guard.has(cur.id)) {
3404
+ lineageDepth.set(cur.id, d++);
3405
+ guard.add(cur.id);
3406
+ const parentId = cur.bind_id;
3407
+ cur = parentId ? tagById.get(parentId) : void 0;
3408
+ }
3409
+ }
3410
+ const isTagInLineage = (id) => lineageDepth.has(id);
3411
+ const fieldById = new Map(fields.map((f) => [f.id, f]));
3412
+ const ownerDepthForField = (f) => {
3413
+ const b = f.bind_id;
3414
+ if (!b) return void 0;
3415
+ if (typeof b === "string") return lineageDepth.get(b);
3416
+ let best = void 0;
3417
+ for (const id of b) {
3418
+ const d = lineageDepth.get(id);
3419
+ if (d == null) continue;
3420
+ if (best == null || d < best) best = d;
3421
+ }
3422
+ return best;
3423
+ };
3424
+ const ownerDepthForTriggerKey = (triggerKey) => {
3425
+ const t = resolveTrigger(triggerKey, nodeMap);
3426
+ if (!t) return void 0;
3427
+ if (t.kind === "composite") {
3428
+ const f = fieldById.get(t.fieldId);
3429
+ if (!f) return void 0;
3430
+ return ownerDepthForField(f);
3431
+ }
3432
+ if (t.kind === "field") {
3433
+ const f = fieldById.get(t.id);
3434
+ if (!f || f.button !== true) return void 0;
3435
+ return ownerDepthForField(f);
3436
+ }
3437
+ if (t.kind === "option") {
3438
+ const f = t.fieldId ? fieldById.get(t.fieldId) : void 0;
3439
+ if (!f) return void 0;
3440
+ return ownerDepthForField(f);
3441
+ }
3442
+ return void 0;
3443
+ };
3444
+ const tagInclude = new Set((_c = tag.includes) != null ? _c : []);
3445
+ const tagExclude = new Set((_d = tag.excludes) != null ? _d : []);
3446
+ const selected = (_e = opts.selectedKeys) != null ? _e : /* @__PURE__ */ new Set();
3447
+ const incMap = (_f = props.includes_for_buttons) != null ? _f : {};
3448
+ const excMap = (_g = props.excludes_for_buttons) != null ? _g : {};
3449
+ const relevantTriggersInOrder = [];
3450
+ for (const key of selected) {
3451
+ const d = ownerDepthForTriggerKey(key);
3452
+ if (d == null) continue;
3453
+ relevantTriggersInOrder.push(key);
3454
+ }
3455
+ const visible = /* @__PURE__ */ new Set();
3456
+ const isBoundToLineage = (f) => {
3457
+ const b = f.bind_id;
3458
+ if (!b) return false;
3459
+ if (typeof b === "string") return isTagInLineage(b);
3460
+ for (const id of b) if (isTagInLineage(id)) return true;
3461
+ return false;
3462
+ };
3463
+ for (const f of fields) {
3464
+ if (isBoundToLineage(f)) visible.add(f.id);
3465
+ if (tagInclude.has(f.id)) visible.add(f.id);
3466
+ }
3467
+ for (const id of tagExclude) visible.delete(id);
3468
+ const decide = /* @__PURE__ */ new Map();
3469
+ const applyDecision = (fieldId, next) => {
3470
+ const prev = decide.get(fieldId);
3471
+ if (!prev) return void decide.set(fieldId, next);
3472
+ if (next.depth < prev.depth) return void decide.set(fieldId, next);
3473
+ if (next.depth > prev.depth) return;
3474
+ if (prev.kind === "include" && next.kind === "exclude") {
3475
+ decide.set(fieldId, next);
3476
+ }
3477
+ };
3478
+ const revealedOrder = [];
3479
+ const revealedSeen = /* @__PURE__ */ new Set();
3480
+ for (const triggerKey of relevantTriggersInOrder) {
3481
+ const depth = ownerDepthForTriggerKey(triggerKey);
3482
+ if (depth == null) continue;
3483
+ for (const id of (_h = incMap[triggerKey]) != null ? _h : []) {
3484
+ applyDecision(id, { depth, kind: "include" });
3485
+ if (!revealedSeen.has(id)) {
3486
+ revealedSeen.add(id);
3487
+ revealedOrder.push(id);
3488
+ }
3489
+ }
3490
+ for (const id of (_i = excMap[triggerKey]) != null ? _i : []) {
3491
+ applyDecision(id, { depth, kind: "exclude" });
3492
+ }
3493
+ }
3494
+ for (const [fid, d] of decide) {
3495
+ if (d.kind === "include") visible.add(fid);
3496
+ else visible.delete(fid);
3497
+ }
3498
+ const base = fields.filter((f) => visible.has(f.id)).map((f) => f.id);
3499
+ const order = (_j = props.order_for_tags) == null ? void 0 : _j[tagId];
3500
+ if (order && order.length) {
3501
+ const ordered = order.filter((fid) => visible.has(fid));
3502
+ const orderedSet = new Set(ordered);
3503
+ const rest = base.filter((fid) => !orderedSet.has(fid));
3504
+ return [...ordered, ...rest];
3505
+ }
3506
+ return base;
3507
+ }
3508
+ function visibleFieldsUnder(props, tagId, opts = {}) {
3509
+ var _a;
3510
+ const ids = visibleFieldIdsUnder(props, tagId, opts);
3511
+ const fieldById = new Map(((_a = props.fields) != null ? _a : []).map((f) => [f.id, f]));
3512
+ return ids.map((id) => fieldById.get(id)).filter(Boolean);
3513
+ }
3514
+
3353
3515
  // src/core/validate/steps/visibility.ts
3354
3516
  function createFieldsVisibleUnder(v) {
3355
3517
  return (tagId) => {
3356
- var _a, _b, _c, _d, _e, _f;
3357
- const tag = v.tagById.get(tagId);
3358
- const includesTag = new Set((_a = tag == null ? void 0 : tag.includes) != null ? _a : []);
3359
- const excludesTag = new Set((_b = tag == null ? void 0 : tag.excludes) != null ? _b : []);
3360
- const incForOpt = (_c = v.props.includes_for_buttons) != null ? _c : {};
3361
- const excForOpt = (_d = v.props.excludes_for_buttons) != null ? _d : {};
3362
- const includesOpt = /* @__PURE__ */ new Set();
3363
- const excludesOpt = /* @__PURE__ */ new Set();
3364
- for (const key of v.selectedKeys) {
3365
- for (const id of (_e = incForOpt[key]) != null ? _e : []) includesOpt.add(id);
3366
- for (const id of (_f = excForOpt[key]) != null ? _f : []) excludesOpt.add(id);
3367
- }
3368
- const merged = /* @__PURE__ */ new Map();
3369
- for (const f of v.fields) {
3370
- if (isBoundTo(f, tagId)) merged.set(f.id, f);
3371
- if (includesTag.has(f.id)) merged.set(f.id, f);
3372
- if (includesOpt.has(f.id)) merged.set(f.id, f);
3373
- }
3374
- for (const id of excludesTag) merged.delete(id);
3375
- for (const id of excludesOpt) merged.delete(id);
3376
- return Array.from(merged.values());
3518
+ return visibleFieldsUnder(v.props, tagId, {
3519
+ selectedKeys: v.selectedKeys
3520
+ });
3377
3521
  };
3378
3522
  }
3379
- function validateVisibility(v) {
3523
+ function stableKeyOfSelection(keys) {
3524
+ return Array.from(keys).sort().join("|");
3525
+ }
3526
+ function resolveRootTags(tags) {
3527
+ const roots = tags.filter((t) => !t.bind_id);
3528
+ return roots.length ? roots : tags.slice(0, 1);
3529
+ }
3530
+ function isEffectfulTrigger(v, trigger) {
3531
+ var _a, _b, _c, _d, _e, _f;
3532
+ const inc = (_a = v.props.includes_for_buttons) != null ? _a : {};
3533
+ const exc = (_b = v.props.excludes_for_buttons) != null ? _b : {};
3534
+ return ((_d = (_c = inc[trigger]) == null ? void 0 : _c.length) != null ? _d : 0) > 0 || ((_f = (_e = exc[trigger]) == null ? void 0 : _e.length) != null ? _f : 0) > 0;
3535
+ }
3536
+ function collectSelectableTriggersInContext(v, tagId, selectedKeys, onlyEffectful) {
3537
+ var _a;
3538
+ const visible = visibleFieldsUnder(v.props, tagId, {
3539
+ selectedKeys
3540
+ });
3541
+ const triggers = [];
3542
+ for (const f of visible) {
3543
+ if (f.button === true) {
3544
+ const t = f.id;
3545
+ if (!onlyEffectful || isEffectfulTrigger(v, t)) triggers.push(t);
3546
+ }
3547
+ for (const o of (_a = f.options) != null ? _a : []) {
3548
+ const t = `${f.id}::${o.id}`;
3549
+ if (!onlyEffectful || isEffectfulTrigger(v, t)) triggers.push(t);
3550
+ }
3551
+ }
3552
+ triggers.sort();
3553
+ return triggers;
3554
+ }
3555
+ function runVisibilityRulesOnce(v) {
3380
3556
  var _a, _b, _c, _d, _e;
3381
3557
  for (const t of v.tags) {
3382
3558
  const visible = v.fieldsVisibleUnder(t.id);
@@ -3451,12 +3627,85 @@ function validateVisibility(v) {
3451
3627
  }
3452
3628
  }
3453
3629
  }
3630
+ function dedupeErrorsInPlace(v, startIndex) {
3631
+ const seen = /* @__PURE__ */ new Set();
3632
+ const keyOfErr = (e) => {
3633
+ var _a, _b, _c, _d, _e, _f, _g, _h, _i, _j;
3634
+ const tagId = (_e = (_d = (_a = e == null ? void 0 : e.details) == null ? void 0 : _a.tagId) != null ? _d : (_c = (_b = e == null ? void 0 : e.details) == null ? void 0 : _b.affected) == null ? void 0 : _c.tagId) != null ? _e : "";
3635
+ const other = (_g = (_f = e == null ? void 0 : e.details) == null ? void 0 : _f.other) != null ? _g : "";
3636
+ return `${(_h = e == null ? void 0 : e.code) != null ? _h : ""}::${(_i = e == null ? void 0 : e.nodeId) != null ? _i : ""}::${tagId}::${other}::${(_j = e == null ? void 0 : e.message) != null ? _j : ""}`;
3637
+ };
3638
+ const kept = [];
3639
+ for (let i = startIndex; i < v.errors.length; i++) {
3640
+ const e = v.errors[i];
3641
+ const k = keyOfErr(e);
3642
+ if (seen.has(k)) continue;
3643
+ seen.add(k);
3644
+ kept.push(e);
3645
+ }
3646
+ v.errors.splice(startIndex, v.errors.length - startIndex, ...kept);
3647
+ }
3648
+ function validateVisibility(v, options = {}) {
3649
+ var _a, _b, _c;
3650
+ const simulate = options.simulate === true;
3651
+ if (!simulate) {
3652
+ runVisibilityRulesOnce(v);
3653
+ return;
3654
+ }
3655
+ const maxStates = Math.max(1, (_a = options.maxStates) != null ? _a : 500);
3656
+ const maxDepth = Math.max(0, (_b = options.maxDepth) != null ? _b : 6);
3657
+ const onlyEffectful = options.onlyEffectfulTriggers !== false;
3658
+ const roots = resolveRootTags(v.tags);
3659
+ const rootTags = options.simulateAllRoots ? roots : roots.slice(0, 1);
3660
+ const originalSelected = new Set((_c = v.selectedKeys) != null ? _c : []);
3661
+ const errorsStart = v.errors.length;
3662
+ const visited = /* @__PURE__ */ new Set();
3663
+ const stack = [];
3664
+ for (const rt of rootTags) {
3665
+ stack.push({
3666
+ rootTagId: rt.id,
3667
+ selected: new Set(originalSelected),
3668
+ depth: 0
3669
+ });
3670
+ }
3671
+ let validatedStates = 0;
3672
+ while (stack.length) {
3673
+ if (validatedStates >= maxStates) break;
3674
+ const state = stack.pop();
3675
+ const sig = stableKeyOfSelection(state.selected);
3676
+ if (visited.has(sig)) continue;
3677
+ visited.add(sig);
3678
+ v.selectedKeys = state.selected;
3679
+ validatedStates++;
3680
+ runVisibilityRulesOnce(v);
3681
+ if (state.depth >= maxDepth) continue;
3682
+ const triggers = collectSelectableTriggersInContext(
3683
+ v,
3684
+ state.rootTagId,
3685
+ state.selected,
3686
+ onlyEffectful
3687
+ );
3688
+ for (let i = triggers.length - 1; i >= 0; i--) {
3689
+ const trig = triggers[i];
3690
+ if (state.selected.has(trig)) continue;
3691
+ const next = new Set(state.selected);
3692
+ next.add(trig);
3693
+ stack.push({
3694
+ rootTagId: state.rootTagId,
3695
+ selected: next,
3696
+ depth: state.depth + 1
3697
+ });
3698
+ }
3699
+ }
3700
+ v.selectedKeys = originalSelected;
3701
+ dedupeErrorsInPlace(v, errorsStart);
3702
+ }
3454
3703
 
3455
3704
  // src/core/validate/steps/structure.ts
3456
3705
  function validateStructure(v) {
3457
3706
  const tags = v.tags;
3458
3707
  const fields = v.fields;
3459
- if (!tags.some((t) => t.id === "root")) {
3708
+ if (!tags.some((t) => t.id === "t:root")) {
3460
3709
  v.errors.push({
3461
3710
  code: "root_missing",
3462
3711
  severity: "error",
@@ -3646,58 +3895,91 @@ function validateIdentity(v) {
3646
3895
  }
3647
3896
 
3648
3897
  // src/core/validate/steps/option-maps.ts
3898
+ function parseFieldOptionKey(key) {
3899
+ const idx = key.indexOf("::");
3900
+ if (idx === -1) return null;
3901
+ const fieldId = key.slice(0, idx).trim();
3902
+ const optionId = key.slice(idx + 2).trim();
3903
+ if (!fieldId || !optionId) return null;
3904
+ return { fieldId, optionId };
3905
+ }
3906
+ function hasOption(v, fid, oid) {
3907
+ var _a;
3908
+ const f = v.fieldById.get(fid);
3909
+ if (!f) return false;
3910
+ return !!((_a = f.options) != null ? _a : []).find((o) => o.id === oid);
3911
+ }
3649
3912
  function validateOptionMaps(v) {
3650
3913
  var _a, _b;
3651
3914
  const incMap = (_a = v.props.includes_for_buttons) != null ? _a : {};
3652
3915
  const excMap = (_b = v.props.excludes_for_buttons) != null ? _b : {};
3653
- const parseKey = (key) => {
3654
- const parts = key.split("::");
3655
- const fid = parts[0];
3656
- const oid = parts[1];
3657
- if (!fid || !oid) return null;
3658
- return { fieldId: fid, optionId: oid };
3659
- };
3660
- const hasOption = (fid, oid) => {
3661
- var _a2;
3662
- const f = v.fieldById.get(fid);
3663
- if (!f) return false;
3664
- return !!((_a2 = f.options) != null ? _a2 : []).find((o) => o.id === oid);
3916
+ const badKeyMessage = (key) => `Invalid trigger-map key "${key}". Expected a known node id (option or button-field), or "fieldId::optionId" pointing to an existing option.`;
3917
+ const validateTriggerKey = (key) => {
3918
+ const ref = v.nodeMap.get(key);
3919
+ if (ref) {
3920
+ if (ref.kind === "option") {
3921
+ return {
3922
+ ok: true,
3923
+ nodeId: ref.fieldId,
3924
+ affected: [ref.fieldId, ref.id]
3925
+ };
3926
+ }
3927
+ if (ref.kind === "field") {
3928
+ const isButton = ref.node.button === true;
3929
+ if (!isButton)
3930
+ return { ok: false, nodeId: ref.id, affected: [ref.id] };
3931
+ return { ok: true, nodeId: ref.id, affected: [ref.id] };
3932
+ }
3933
+ return { ok: false, nodeId: ref.id, affected: [ref.id] };
3934
+ }
3935
+ const p = parseFieldOptionKey(key);
3936
+ if (!p) return { ok: false };
3937
+ if (!hasOption(v, p.fieldId, p.optionId))
3938
+ return {
3939
+ ok: false,
3940
+ nodeId: p.fieldId,
3941
+ affected: [p.fieldId, p.optionId]
3942
+ };
3943
+ return {
3944
+ ok: true,
3945
+ nodeId: p.fieldId,
3946
+ affected: [p.fieldId, p.optionId]
3947
+ };
3665
3948
  };
3666
- const badKeyMessage = (key) => `Invalid option-map key "${key}". Expected "fieldId::optionId" pointing to an existing option.`;
3667
3949
  for (const k of Object.keys(incMap)) {
3668
- const p = parseKey(k);
3669
- if (!p || !hasOption(p.fieldId, p.optionId)) {
3950
+ const r = validateTriggerKey(k);
3951
+ if (!r.ok) {
3670
3952
  v.errors.push({
3671
3953
  code: "bad_option_key",
3672
3954
  severity: "error",
3673
3955
  message: badKeyMessage(k),
3674
- details: { key: k }
3956
+ nodeId: r.nodeId,
3957
+ details: withAffected({ key: k }, r.affected)
3675
3958
  });
3676
3959
  }
3677
3960
  }
3678
3961
  for (const k of Object.keys(excMap)) {
3679
- const p = parseKey(k);
3680
- if (!p || !hasOption(p.fieldId, p.optionId)) {
3962
+ const r = validateTriggerKey(k);
3963
+ if (!r.ok) {
3681
3964
  v.errors.push({
3682
3965
  code: "bad_option_key",
3683
3966
  severity: "error",
3684
3967
  message: badKeyMessage(k),
3685
- details: { key: k }
3968
+ nodeId: r.nodeId,
3969
+ details: withAffected({ key: k }, r.affected)
3686
3970
  });
3687
3971
  }
3688
3972
  }
3689
3973
  for (const k of Object.keys(incMap)) {
3690
- if (k in excMap) {
3691
- const p = parseKey(k);
3692
- const affected = p ? [p.fieldId, p.optionId] : void 0;
3693
- v.errors.push({
3694
- code: "option_include_exclude_conflict",
3695
- severity: "error",
3696
- message: `Option-map key "${k}" appears in both includes_for_buttons and excludes_for_buttons.`,
3697
- nodeId: p == null ? void 0 : p.fieldId,
3698
- details: withAffected({ key: k }, affected)
3699
- });
3700
- }
3974
+ if (!(k in excMap)) continue;
3975
+ const r = validateTriggerKey(k);
3976
+ v.errors.push({
3977
+ code: "option_include_exclude_conflict",
3978
+ severity: "error",
3979
+ message: `Trigger-map key "${k}" appears in both includes_for_buttons and excludes_for_buttons.`,
3980
+ nodeId: r.nodeId,
3981
+ details: withAffected({ key: k }, r.affected)
3982
+ });
3701
3983
  }
3702
3984
  }
3703
3985
 
@@ -4602,8 +4884,23 @@ function applyPolicies(errors, props, serviceMap, policies, fieldsVisibleUnder,
4602
4884
  }
4603
4885
 
4604
4886
  // src/core/validate/index.ts
4887
+ function readVisibilitySimOpts(ctx) {
4888
+ const c = ctx;
4889
+ const simulate = c.simulateVisibility === true || c.visibilitySimulate === true || c.simulate === true;
4890
+ const maxStates = typeof c.maxVisibilityStates === "number" ? c.maxVisibilityStates : typeof c.visibilityMaxStates === "number" ? c.visibilityMaxStates : typeof c.maxStates === "number" ? c.maxStates : void 0;
4891
+ const maxDepth = typeof c.maxVisibilityDepth === "number" ? c.maxVisibilityDepth : typeof c.visibilityMaxDepth === "number" ? c.visibilityMaxDepth : typeof c.maxDepth === "number" ? c.maxDepth : void 0;
4892
+ const simulateAllRoots = c.simulateAllRoots === true || c.visibilitySimulateAllRoots === true;
4893
+ const onlyEffectfulTriggers = c.onlyEffectfulTriggers === false ? false : c.visibilityOnlyEffectfulTriggers !== false;
4894
+ return {
4895
+ simulate,
4896
+ maxStates,
4897
+ maxDepth,
4898
+ simulateAllRoots,
4899
+ onlyEffectfulTriggers
4900
+ };
4901
+ }
4605
4902
  function validate(props, ctx = {}) {
4606
- var _a, _b;
4903
+ var _a, _b, _c;
4607
4904
  const errors = [];
4608
4905
  const serviceMap = (_a = ctx.serviceMap) != null ? _a : {};
4609
4906
  const selectedKeys = new Set(
@@ -4617,6 +4914,7 @@ function validate(props, ctx = {}) {
4617
4914
  for (const f of fields) fieldById.set(f.id, f);
4618
4915
  const v = {
4619
4916
  props,
4917
+ nodeMap: (_c = ctx.nodeMap) != null ? _c : buildNodeMap(props),
4620
4918
  options: ctx,
4621
4919
  errors,
4622
4920
  serviceMap,
@@ -4631,7 +4929,8 @@ function validate(props, ctx = {}) {
4631
4929
  validateIdentity(v);
4632
4930
  validateOptionMaps(v);
4633
4931
  v.fieldsVisibleUnder = createFieldsVisibleUnder(v);
4634
- validateVisibility(v);
4932
+ const visSim = readVisibilitySimOpts(ctx);
4933
+ validateVisibility(v, visSim);
4635
4934
  applyPolicies(
4636
4935
  v.errors,
4637
4936
  v.props,
@@ -4667,11 +4966,21 @@ var BuilderImpl = class {
4667
4966
  this.optionOwnerById = /* @__PURE__ */ new Map();
4668
4967
  this.history = [];
4669
4968
  this.future = [];
4969
+ this._nodemap = null;
4670
4970
  var _a;
4671
4971
  this.options = { ...opts };
4672
4972
  this.historyLimit = (_a = opts.historyLimit) != null ? _a : 50;
4673
4973
  }
4674
4974
  /* ───── lifecycle ─────────────────────────────────────────────────────── */
4975
+ isTagId(id) {
4976
+ return this.tagById.has(id);
4977
+ }
4978
+ isFieldId(id) {
4979
+ return this.fieldById.has(id);
4980
+ }
4981
+ isOptionId(id) {
4982
+ return this.optionOwnerById.has(id);
4983
+ }
4675
4984
  load(raw) {
4676
4985
  const next = normalise(raw, {
4677
4986
  defaultPricingRole: "base",
@@ -4891,129 +5200,16 @@ var BuilderImpl = class {
4891
5200
  return validate(this.props, this.options);
4892
5201
  }
4893
5202
  visibleFields(tagId, selectedKeys) {
4894
- var _a, _b, _c, _d, _e, _f, _g, _h, _i, _j, _k;
4895
- const props = this.props;
4896
- const tags = (_a = props.filters) != null ? _a : [];
4897
- const fields = (_b = props.fields) != null ? _b : [];
4898
- const tagById = new Map(tags.map((t) => [t.id, t]));
4899
- const tag = tagById.get(tagId);
4900
- if (!tag) return [];
4901
- const lineageDepth = /* @__PURE__ */ new Map();
4902
- {
4903
- const guard = /* @__PURE__ */ new Set();
4904
- let cur = tag;
4905
- let d = 0;
4906
- while (cur && !guard.has(cur.id)) {
4907
- lineageDepth.set(cur.id, d++);
4908
- guard.add(cur.id);
4909
- const parentId = cur.bind_id;
4910
- cur = parentId ? tagById.get(parentId) : void 0;
4911
- }
4912
- }
4913
- const isTagInLineage = (id) => lineageDepth.has(id);
4914
- const fieldById = new Map(fields.map((f) => [f.id, f]));
4915
- const optionOwnerFieldId = /* @__PURE__ */ new Map();
4916
- for (const f of fields) {
4917
- for (const o of (_c = f.options) != null ? _c : []) optionOwnerFieldId.set(o.id, f.id);
4918
- }
4919
- const ownerDepthForField = (f) => {
4920
- const b = f.bind_id;
4921
- if (!b) return void 0;
4922
- if (typeof b === "string") return lineageDepth.get(b);
4923
- let best = void 0;
4924
- for (const id of b) {
4925
- const d = lineageDepth.get(id);
4926
- if (d == null) continue;
4927
- if (best == null || d < best) best = d;
4928
- }
4929
- return best;
4930
- };
4931
- const ownerDepthForTrigger = (triggerId) => {
4932
- if (triggerId.startsWith("o:")) {
4933
- const fid = optionOwnerFieldId.get(triggerId);
4934
- if (!fid) return void 0;
4935
- const f2 = fieldById.get(fid);
4936
- if (!f2) return void 0;
4937
- return ownerDepthForField(f2);
4938
- }
4939
- const f = fieldById.get(triggerId);
4940
- if (!f || f.button !== true) return void 0;
4941
- return ownerDepthForField(f);
4942
- };
4943
- const tagInclude = new Set((_d = tag.includes) != null ? _d : []);
4944
- const tagExclude = new Set((_e = tag.excludes) != null ? _e : []);
4945
- const selected = new Set(
4946
- (_f = selectedKeys != null ? selectedKeys : this.options.selectedOptionKeys) != null ? _f : []
4947
- );
4948
- const incMap = (_g = props.includes_for_buttons) != null ? _g : {};
4949
- const excMap = (_h = props.excludes_for_buttons) != null ? _h : {};
4950
- const relevantTriggersInOrder = [];
4951
- for (const key of selected) {
4952
- const d = ownerDepthForTrigger(key);
4953
- if (d == null) continue;
4954
- relevantTriggersInOrder.push(key);
4955
- }
4956
- const visible = /* @__PURE__ */ new Set();
4957
- const isBoundToLineage = (f) => {
4958
- const b = f.bind_id;
4959
- if (!b) return false;
4960
- if (typeof b === "string") return isTagInLineage(b);
4961
- for (const id of b) if (isTagInLineage(id)) return true;
4962
- return false;
4963
- };
4964
- for (const f of fields) {
4965
- if (isBoundToLineage(f)) visible.add(f.id);
4966
- if (tagInclude.has(f.id)) visible.add(f.id);
4967
- }
4968
- for (const id of tagExclude) visible.delete(id);
4969
- const decide = /* @__PURE__ */ new Map();
4970
- const applyDecision = (fieldId, next) => {
4971
- const prev = decide.get(fieldId);
4972
- if (!prev) {
4973
- decide.set(fieldId, next);
4974
- return;
4975
- }
4976
- if (next.depth < prev.depth) {
4977
- decide.set(fieldId, next);
4978
- return;
4979
- }
4980
- if (next.depth > prev.depth) return;
4981
- if (prev.kind === "include" && next.kind === "exclude") {
4982
- decide.set(fieldId, next);
4983
- }
4984
- };
4985
- const revealedOrder = [];
4986
- const revealedSeen = /* @__PURE__ */ new Set();
4987
- for (const triggerId of relevantTriggersInOrder) {
4988
- const depth = ownerDepthForTrigger(triggerId);
4989
- if (depth == null) continue;
4990
- for (const id of (_i = incMap[triggerId]) != null ? _i : []) {
4991
- applyDecision(id, { depth, kind: "include" });
4992
- if (!revealedSeen.has(id)) {
4993
- revealedSeen.add(id);
4994
- revealedOrder.push(id);
4995
- }
4996
- }
4997
- for (const id of (_j = excMap[triggerId]) != null ? _j : []) {
4998
- applyDecision(id, { depth, kind: "exclude" });
4999
- }
5000
- }
5001
- for (const [fid, d] of decide) {
5002
- if (d.kind === "include") visible.add(fid);
5003
- else visible.delete(fid);
5004
- }
5005
- const base = fields.filter((f) => visible.has(f.id)).map((f) => f.id);
5006
- const order = (_k = props.order_for_tags) == null ? void 0 : _k[tagId];
5007
- if (order && order.length) {
5008
- const ordered = order.filter((fid) => visible.has(fid));
5009
- const orderedSet = new Set(ordered);
5010
- const rest2 = base.filter((fid) => !orderedSet.has(fid));
5011
- return [...ordered, ...rest2];
5012
- }
5013
- const promoted = revealedOrder.filter((fid) => visible.has(fid));
5014
- const promotedSet = new Set(promoted);
5015
- const rest = base.filter((fid) => !promotedSet.has(fid));
5016
- return [...promoted, ...rest];
5203
+ var _a;
5204
+ return visibleFieldIdsUnder(this.props, tagId, {
5205
+ selectedKeys: new Set(
5206
+ (_a = selectedKeys != null ? selectedKeys : this.options.selectedOptionKeys) != null ? _a : []
5207
+ )
5208
+ });
5209
+ }
5210
+ getNodeMap() {
5211
+ if (!this._nodemap) this._nodemap = buildNodeMap(this.getProps());
5212
+ return this._nodemap;
5017
5213
  }
5018
5214
  /* ───── history ─────────────────────────────────────────────────────── */
5019
5215
  undo() {
@@ -5037,6 +5233,7 @@ var BuilderImpl = class {
5037
5233
  this.tagById.clear();
5038
5234
  this.fieldById.clear();
5039
5235
  this.optionOwnerById.clear();
5236
+ this._nodemap = null;
5040
5237
  for (const t of this.props.filters) this.tagById.set(t.id, t);
5041
5238
  for (const f of this.props.fields) {
5042
5239
  this.fieldById.set(f.id, f);
@@ -5264,6 +5461,7 @@ var toBindList = (b) => {
5264
5461
  function createNodeIndex(builder) {
5265
5462
  var _a, _b, _c, _d;
5266
5463
  const props = builder.getProps();
5464
+ const nodeMap = builder.getNodeMap();
5267
5465
  const tags = (_a = props.filters) != null ? _a : [];
5268
5466
  const fields = (_b = props.fields) != null ? _b : [];
5269
5467
  const tagById = new Map(tags.map((t) => [t.id, t]));
@@ -5467,7 +5665,9 @@ function createNodeIndex(builder) {
5467
5665
  return parentById.get(id) === tid;
5468
5666
  },
5469
5667
  getDescendant(descendantId) {
5470
- return this.getDescendants().find((item) => item.id == descendantId);
5668
+ return this.getDescendants().find(
5669
+ (item) => item.id == descendantId
5670
+ );
5471
5671
  },
5472
5672
  getDescendants() {
5473
5673
  const results = [];
@@ -5476,7 +5676,9 @@ function createNodeIndex(builder) {
5476
5676
  const node2 = getField(fieldId);
5477
5677
  if (!node2) continue;
5478
5678
  const explicit = includes.has(fieldId) || isFieldBoundDirectToTag(fieldId, id);
5479
- results.push(explicit ? node2 : { ...node2, isInherited: true });
5679
+ results.push(
5680
+ explicit ? node2 : { ...node2, isInherited: true }
5681
+ );
5480
5682
  }
5481
5683
  return Object.freeze(results);
5482
5684
  }
@@ -5525,7 +5727,9 @@ function createNodeIndex(builder) {
5525
5727
  return false;
5526
5728
  },
5527
5729
  getDescendant(descendantId, context) {
5528
- return this.getDescendants(context).find((item) => item.id == descendantId);
5730
+ return this.getDescendants(context).find(
5731
+ (item) => item.id == descendantId
5732
+ );
5529
5733
  },
5530
5734
  getDescendants(tagId) {
5531
5735
  return resolveDescendants(id, includes, tagId, !isButton);
@@ -5567,7 +5771,9 @@ function createNodeIndex(builder) {
5567
5771
  return owner.isBoundTo(tagId);
5568
5772
  },
5569
5773
  getDescendant(descendantId, context) {
5570
- return this.getDescendants(context).find((item) => item.id == descendantId);
5774
+ return this.getDescendants(context).find(
5775
+ (item) => item.id == descendantId
5776
+ );
5571
5777
  },
5572
5778
  getDescendants(tagId) {
5573
5779
  return resolveDescendants(id, includes, tagId);
@@ -5578,7 +5784,7 @@ function createNodeIndex(builder) {
5578
5784
  return node;
5579
5785
  };
5580
5786
  const getNode = (input) => {
5581
- var _a2, _b2, _c2, _d2, _e, _f, _g, _h, _i;
5787
+ var _a2, _b2, _c2, _d2, _e;
5582
5788
  if (typeof input !== "string") {
5583
5789
  if ("bind_id" in input && !("type" in input))
5584
5790
  return (_a2 = getTag(input.id)) != null ? _a2 : mkUnknown(input.id);
@@ -5588,11 +5794,7 @@ function createNodeIndex(builder) {
5588
5794
  }
5589
5795
  const cached = nodeCache.get(input);
5590
5796
  if (cached) return cached;
5591
- const id = input;
5592
- if (id.startsWith("t:")) return (_d2 = getTag(id)) != null ? _d2 : mkUnknown(id);
5593
- if (id.startsWith("f:")) return (_e = getField(id)) != null ? _e : mkUnknown(id);
5594
- if (id.startsWith("o:")) return (_f = getOption(id)) != null ? _f : mkUnknown(id);
5595
- return (_i = (_h = (_g = getTag(id)) != null ? _g : getField(id)) != null ? _h : getOption(id)) != null ? _i : mkUnknown(id);
5797
+ return (_e = (_d2 = nodeMap.get(input)) == null ? void 0 : _d2.node) != null ? _e : mkUnknown(input);
5596
5798
  };
5597
5799
  const mkUnknown = (id) => {
5598
5800
  const u = {
@@ -5652,9 +5854,6 @@ function rateOk(svcMap, candidate, primary, policy) {
5652
5854
 
5653
5855
  // src/react/canvas/editor.ts
5654
5856
  var MAX_LIMIT = 100;
5655
- var isTagId = (id) => id.startsWith("t:");
5656
- var isFieldId = (id) => id.startsWith("f:");
5657
- var isOptionId = (id) => id.startsWith("o:");
5658
5857
  function ownerOfOption(props, optionId) {
5659
5858
  var _a, _b;
5660
5859
  for (const f of (_a = props.fields) != null ? _a : []) {
@@ -5696,6 +5895,15 @@ var Editor = class {
5696
5895
  this.pushHistory(this.makeSnapshot("init"));
5697
5896
  }
5698
5897
  /* ───────────────────────── Public API ───────────────────────── */
5898
+ isTagId(id) {
5899
+ return this.builder.isTagId(id);
5900
+ }
5901
+ isFieldId(id) {
5902
+ return this.builder.isFieldId(id);
5903
+ }
5904
+ isOptionId(id) {
5905
+ return this.builder.isOptionId(id);
5906
+ }
5699
5907
  getProps() {
5700
5908
  return this.builder.getProps();
5701
5909
  }
@@ -5791,7 +5999,7 @@ var Editor = class {
5791
5999
  name: "reLabel",
5792
6000
  do: () => this.patchProps((p) => {
5793
6001
  var _a, _b, _c, _d, _e, _f, _g;
5794
- if (isTagId(id)) {
6002
+ if (this.isTagId(id)) {
5795
6003
  const t = ((_a = p.filters) != null ? _a : []).find((x) => x.id === id);
5796
6004
  if (!t) return;
5797
6005
  if (((_b = t.label) != null ? _b : "") === label) return;
@@ -5799,7 +6007,7 @@ var Editor = class {
5799
6007
  this.api.refreshGraph();
5800
6008
  return;
5801
6009
  }
5802
- if (isOptionId(id)) {
6010
+ if (this.isOptionId(id)) {
5803
6011
  const own = ownerOfOption(p, id);
5804
6012
  if (!own) return;
5805
6013
  const f = ((_c = p.fields) != null ? _c : []).find(
@@ -6100,7 +6308,7 @@ var Editor = class {
6100
6308
  * - Option: use placeOption() instead
6101
6309
  */
6102
6310
  placeNode(id, opts) {
6103
- if (isTagId(id)) {
6311
+ if (this.isTagId(id)) {
6104
6312
  this.exec({
6105
6313
  name: "placeTag",
6106
6314
  do: () => this.patchProps((p) => {
@@ -6147,7 +6355,7 @@ var Editor = class {
6147
6355
  }),
6148
6356
  undo: () => this.api.undo()
6149
6357
  });
6150
- } else if (isFieldId(id)) {
6358
+ } else if (this.isFieldId(id)) {
6151
6359
  if (!opts.scopeTagId)
6152
6360
  throw new Error("placeNode(field): scopeTagId is required");
6153
6361
  const fieldId = id;
@@ -6174,14 +6382,14 @@ var Editor = class {
6174
6382
  }),
6175
6383
  undo: () => this.api.undo()
6176
6384
  });
6177
- } else if (isOptionId(id)) {
6385
+ } else if (this.isOptionId(id)) {
6178
6386
  this.placeOption(id, opts);
6179
6387
  } else {
6180
6388
  throw new Error("placeNode: unknown id prefix");
6181
6389
  }
6182
6390
  }
6183
6391
  placeOption(optionId, opts) {
6184
- if (!isOptionId(optionId))
6392
+ if (!this.isOptionId(optionId))
6185
6393
  throw new Error('placeOption: optionId must start with "o:"');
6186
6394
  this.exec({
6187
6395
  name: "placeOption",
@@ -6238,7 +6446,7 @@ var Editor = class {
6238
6446
  return id;
6239
6447
  }
6240
6448
  updateOption(optionId, patch) {
6241
- if (!isOptionId(optionId))
6449
+ if (!this.isOptionId(optionId))
6242
6450
  throw new Error('updateOption: optionId must start with "o:"');
6243
6451
  this.exec({
6244
6452
  name: "updateOption",
@@ -6257,7 +6465,7 @@ var Editor = class {
6257
6465
  });
6258
6466
  }
6259
6467
  removeOption(optionId) {
6260
- if (!isOptionId(optionId))
6468
+ if (!this.isOptionId(optionId))
6261
6469
  throw new Error('removeOption: optionId must start with "o:"');
6262
6470
  this.exec({
6263
6471
  name: "removeOption",
@@ -6288,17 +6496,17 @@ var Editor = class {
6288
6496
  name: "editLabel",
6289
6497
  do: () => this.patchProps((p) => {
6290
6498
  var _a, _b, _c, _d;
6291
- if (isTagId(id)) {
6499
+ if (this.isTagId(id)) {
6292
6500
  const t = ((_a = p.filters) != null ? _a : []).find((x) => x.id === id);
6293
6501
  if (t) t.label = next;
6294
6502
  return;
6295
6503
  }
6296
- if (isFieldId(id)) {
6504
+ if (this.isFieldId(id)) {
6297
6505
  const f = ((_b = p.fields) != null ? _b : []).find((x) => x.id === id);
6298
6506
  if (f) f.label = next;
6299
6507
  return;
6300
6508
  }
6301
- if (isOptionId(id)) {
6509
+ if (this.isOptionId(id)) {
6302
6510
  const own = ownerOfOption(p, id);
6303
6511
  if (!own) return;
6304
6512
  const f = ((_c = p.fields) != null ? _c : []).find(
@@ -6337,7 +6545,7 @@ var Editor = class {
6337
6545
  const validId = hasSidKey && typeof input.service_id === "number" && Number.isFinite(input.service_id);
6338
6546
  const sid = validId ? Number(input.service_id) : void 0;
6339
6547
  const nextRole = input.pricing_role;
6340
- if (isTagId(id)) {
6548
+ if (this.isTagId(id)) {
6341
6549
  const t = ((_a = p.filters) != null ? _a : []).find((x) => x.id === id);
6342
6550
  if (!t) return;
6343
6551
  if (hasSidKey) {
@@ -6346,7 +6554,7 @@ var Editor = class {
6346
6554
  }
6347
6555
  return;
6348
6556
  }
6349
- if (isOptionId(id)) {
6557
+ if (this.isOptionId(id)) {
6350
6558
  const own = ownerOfOption(p, id);
6351
6559
  if (!own) return;
6352
6560
  const f2 = ((_b = p.fields) != null ? _b : []).find(
@@ -6553,7 +6761,7 @@ var Editor = class {
6553
6761
  });
6554
6762
  }
6555
6763
  remove(id) {
6556
- if (isTagId(id)) {
6764
+ if (this.isTagId(id)) {
6557
6765
  this.exec({
6558
6766
  name: "removeTag",
6559
6767
  do: () => this.patchProps((p) => {
@@ -6593,7 +6801,7 @@ var Editor = class {
6593
6801
  });
6594
6802
  return;
6595
6803
  }
6596
- if (isFieldId(id)) {
6804
+ if (this.isFieldId(id)) {
6597
6805
  this.exec({
6598
6806
  name: "removeField",
6599
6807
  do: () => this.patchProps((p) => {
@@ -6629,7 +6837,7 @@ var Editor = class {
6629
6837
  });
6630
6838
  return;
6631
6839
  }
6632
- if (isOptionId(id)) {
6840
+ if (this.isOptionId(id)) {
6633
6841
  this.removeOption(id);
6634
6842
  return;
6635
6843
  }
@@ -6638,7 +6846,7 @@ var Editor = class {
6638
6846
  getNode(id) {
6639
6847
  var _a, _b, _c, _d;
6640
6848
  const props = this.builder.getProps();
6641
- if (isTagId(id)) {
6849
+ if (this.isTagId(id)) {
6642
6850
  const t = ((_a = props.filters) != null ? _a : []).find((x) => x.id === id);
6643
6851
  return {
6644
6852
  kind: "tag",
@@ -6646,12 +6854,12 @@ var Editor = class {
6646
6854
  owners: { parentTagId: t == null ? void 0 : t.bind_id }
6647
6855
  };
6648
6856
  }
6649
- if (isFieldId(id)) {
6857
+ if (this.isFieldId(id)) {
6650
6858
  const f = ((_b = props.fields) != null ? _b : []).find((x) => x.id === id);
6651
6859
  const bind = Array.isArray(f == null ? void 0 : f.bind_id) ? f.bind_id : (f == null ? void 0 : f.bind_id) ? [f.bind_id] : [];
6652
6860
  return { kind: "field", data: f, owners: { bindTagIds: bind } };
6653
6861
  }
6654
- if (isOptionId(id)) {
6862
+ if (this.isOptionId(id)) {
6655
6863
  const own = ownerOfOption(props, id);
6656
6864
  const f = own ? ((_c = props.fields) != null ? _c : []).find((x) => x.id === own.fieldId) : void 0;
6657
6865
  const o = (_d = f == null ? void 0 : f.options) == null ? void 0 : _d.find((x) => x.id === id);
@@ -6729,7 +6937,7 @@ var Editor = class {
6729
6937
  if (receiverId === targetId) return true;
6730
6938
  const getDirectRelations = (id) => {
6731
6939
  var _a, _b, _c, _d, _e, _f, _g;
6732
- if (isTagId(id)) {
6940
+ if (this.isTagId(id)) {
6733
6941
  const t = ((_a = p.filters) != null ? _a : []).find((x) => x.id === id);
6734
6942
  return [...(_b = t == null ? void 0 : t.includes) != null ? _b : [], ...(_c = t == null ? void 0 : t.excludes) != null ? _c : []];
6735
6943
  }
@@ -6949,7 +7157,7 @@ var Editor = class {
6949
7157
  do: () => this.patchProps((p) => {
6950
7158
  var _a, _b, _c, _d, _e;
6951
7159
  if (kind === "bind") {
6952
- if (isTagId(fromId) && isTagId(toId2)) {
7160
+ if (this.isTagId(fromId) && this.isTagId(toId2)) {
6953
7161
  if (this.wouldCreateTagCycle(p, fromId, toId2)) {
6954
7162
  throw new Error(
6955
7163
  `bind would create a cycle: ${fromId} \u2192 ${toId2}`
@@ -6961,9 +7169,9 @@ var Editor = class {
6961
7169
  if (child) child.bind_id = fromId;
6962
7170
  return;
6963
7171
  }
6964
- if (isTagId(fromId) && isFieldId(toId2) || isFieldId(fromId) && isTagId(toId2)) {
6965
- const fieldId = isFieldId(toId2) ? toId2 : fromId;
6966
- const tagId = isTagId(fromId) ? fromId : toId2;
7172
+ if (this.isTagId(fromId) && this.isFieldId(toId2) || this.isFieldId(fromId) && this.isTagId(toId2)) {
7173
+ const fieldId = this.isFieldId(toId2) ? toId2 : fromId;
7174
+ const tagId = this.isTagId(fromId) ? fromId : toId2;
6967
7175
  const f = ((_b = p.fields) != null ? _b : []).find(
6968
7176
  (x) => x.id === fieldId
6969
7177
  );
@@ -6987,7 +7195,7 @@ var Editor = class {
6987
7195
  }
6988
7196
  if (kind === "include" || kind === "exclude") {
6989
7197
  const key = kind === "include" ? "includes" : "excludes";
6990
- if (isTagId(fromId) && isFieldId(toId2)) {
7198
+ if (this.isTagId(fromId) && this.isFieldId(toId2)) {
6991
7199
  const t = ((_c = p.filters) != null ? _c : []).find(
6992
7200
  (x) => x.id === fromId
6993
7201
  );
@@ -6996,7 +7204,7 @@ var Editor = class {
6996
7204
  if (!arr.includes(toId2)) arr.push(toId2);
6997
7205
  return;
6998
7206
  }
6999
- if (isOptionId(fromId) && isFieldId(toId2)) {
7207
+ if (this.isOptionId(fromId) && this.isFieldId(toId2)) {
7000
7208
  const mapKey = kind === "include" ? "includes_for_options" : "excludes_for_options";
7001
7209
  const maps = p[mapKey];
7002
7210
  const next = { ...maps != null ? maps : {} };
@@ -7064,16 +7272,16 @@ var Editor = class {
7064
7272
  do: () => this.patchProps((p) => {
7065
7273
  var _a, _b, _c, _d, _e, _f, _g, _h;
7066
7274
  if (kind === "bind") {
7067
- if (isTagId(fromId) && isTagId(toId2)) {
7275
+ if (this.isTagId(fromId) && this.isTagId(toId2)) {
7068
7276
  const child = ((_a = p.filters) != null ? _a : []).find(
7069
7277
  (t) => t.id === toId2
7070
7278
  );
7071
7279
  if ((child == null ? void 0 : child.bind_id) === fromId) delete child.bind_id;
7072
7280
  return;
7073
7281
  }
7074
- if (isTagId(fromId) && isFieldId(toId2) || isFieldId(fromId) && isTagId(toId2)) {
7075
- const fieldId = isFieldId(toId2) ? toId2 : fromId;
7076
- const tagId = isTagId(fromId) ? fromId : toId2;
7282
+ if (this.isTagId(fromId) && this.isFieldId(toId2) || this.isFieldId(fromId) && this.isTagId(toId2)) {
7283
+ const fieldId = this.isFieldId(toId2) ? toId2 : fromId;
7284
+ const tagId = this.isTagId(fromId) ? fromId : toId2;
7077
7285
  const f = ((_b = p.fields) != null ? _b : []).find(
7078
7286
  (x) => x.id === fieldId
7079
7287
  );
@@ -7094,7 +7302,7 @@ var Editor = class {
7094
7302
  }
7095
7303
  if (kind === "include" || kind === "exclude") {
7096
7304
  const key = kind === "include" ? "includes" : "excludes";
7097
- if (isTagId(fromId) && isFieldId(toId2)) {
7305
+ if (this.isTagId(fromId) && this.isFieldId(toId2)) {
7098
7306
  const t = ((_d = p.filters) != null ? _d : []).find(
7099
7307
  (x) => x.id === fromId
7100
7308
  );
@@ -7103,7 +7311,7 @@ var Editor = class {
7103
7311
  if (!((_f = t[key]) == null ? void 0 : _f.length)) delete t[key];
7104
7312
  return;
7105
7313
  }
7106
- if (isOptionId(fromId) && isFieldId(toId2)) {
7314
+ if (this.isOptionId(fromId) && this.isFieldId(toId2)) {
7107
7315
  const mapKey = kind === "include" ? "includes_for_options" : "excludes_for_options";
7108
7316
  const maps = p[mapKey];
7109
7317
  if (!maps) return;
@@ -7570,8 +7778,6 @@ function toStrSet(v) {
7570
7778
  }
7571
7779
 
7572
7780
  // src/react/canvas/selection.ts
7573
- var isTagId2 = (id) => typeof id === "string" && id.startsWith("t:");
7574
- var isOptionId2 = (id) => typeof id === "string" && id.startsWith("o:");
7575
7781
  var Selection = class {
7576
7782
  constructor(builder, opts) {
7577
7783
  this.builder = builder;
@@ -7589,6 +7795,7 @@ var Selection = class {
7589
7795
  this.emit();
7590
7796
  }
7591
7797
  add(id) {
7798
+ if (this.set.has(id)) this.set.delete(id);
7592
7799
  this.set.add(id);
7593
7800
  this.primaryId = id;
7594
7801
  this.updateCurrentTagFrom(id);
@@ -7643,7 +7850,9 @@ var Selection = class {
7643
7850
  var _a;
7644
7851
  const props = this.builder.getProps();
7645
7852
  if (((_a = this.opts.env) != null ? _a : "client") === "workspace") {
7646
- const tagIds = Array.from(this.set).filter(isTagId2);
7853
+ const tagIds = Array.from(this.set).filter(
7854
+ this.builder.isTagId.bind(this.builder)
7855
+ );
7647
7856
  if (tagIds.length > 1) {
7648
7857
  return { kind: "multi", groups: Array.from(this.set) };
7649
7858
  }
@@ -7654,6 +7863,98 @@ var Selection = class {
7654
7863
  const group = this.computeGroupForTag(props, tagId);
7655
7864
  return { kind: "single", group };
7656
7865
  }
7866
+ /**
7867
+ * Build a fieldId -> triggerKeys[] map from the current selection set.
7868
+ *
7869
+ * What counts as a "button selection" (trigger key):
7870
+ * - field key where the field has button === true (e.g. "f:dripfeed")
7871
+ * - option key (e.g. "o:fast")
7872
+ * - composite key "fieldId::optionId" (e.g. "f:speed::o:fast")
7873
+ *
7874
+ * Grouping:
7875
+ * - button-field trigger groups under its own fieldId
7876
+ * - option/composite groups under the option's owning fieldId (from nodeMap)
7877
+ *
7878
+ * Deterministic:
7879
+ * - preserves selection insertion order
7880
+ * - de-dupes per field
7881
+ */
7882
+ buttonSelectionsByFieldId() {
7883
+ var _a, _b, _c, _d, _e;
7884
+ const nodeMap = this.builder.getNodeMap();
7885
+ const out = {};
7886
+ const push = (fieldId, triggerKey) => {
7887
+ var _a2;
7888
+ const arr = (_a2 = out[fieldId]) != null ? _a2 : out[fieldId] = [];
7889
+ if (!arr.includes(triggerKey)) arr.push(triggerKey);
7890
+ };
7891
+ for (const key of this.set) {
7892
+ if (!key) continue;
7893
+ const idx = key.indexOf("::");
7894
+ if (idx !== -1) {
7895
+ const optionId = key.slice(idx + 2);
7896
+ const optRef = nodeMap.get(optionId);
7897
+ if ((optRef == null ? void 0 : optRef.kind) === "option" && typeof optRef.fieldId === "string") {
7898
+ push(optRef.fieldId, key);
7899
+ }
7900
+ continue;
7901
+ }
7902
+ const ref = nodeMap.get(key);
7903
+ if (!ref) continue;
7904
+ if (ref.kind === "option" && typeof ref.fieldId === "string") {
7905
+ push(ref.fieldId, (_a = ref.id) != null ? _a : key);
7906
+ continue;
7907
+ }
7908
+ if (ref.kind === "field") {
7909
+ const field = (_c = (_b = ref.node) != null ? _b : ref.field) != null ? _c : ref;
7910
+ const fieldId = (_e = (_d = ref.id) != null ? _d : field == null ? void 0 : field.id) != null ? _e : key;
7911
+ if ((field == null ? void 0 : field.button) === true && typeof fieldId === "string") {
7912
+ push(fieldId, fieldId);
7913
+ }
7914
+ }
7915
+ }
7916
+ return out;
7917
+ }
7918
+ /**
7919
+ * Returns only selection keys that are valid "trigger buttons":
7920
+ * - field keys where field.button === true
7921
+ * - option keys
7922
+ * - composite keys "fieldId::optionId" (validated by optionId)
7923
+ * Excludes tags and non-button fields.
7924
+ */
7925
+ selectedButtons() {
7926
+ var _a, _b;
7927
+ const nodeMap = this.builder.getNodeMap();
7928
+ const out = [];
7929
+ const seen = /* @__PURE__ */ new Set();
7930
+ const push = (k) => {
7931
+ if (!seen.has(k)) {
7932
+ seen.add(k);
7933
+ out.push(k);
7934
+ }
7935
+ };
7936
+ for (const key of this.set) {
7937
+ if (!key) continue;
7938
+ const idx = key.indexOf("::");
7939
+ if (idx !== -1) {
7940
+ const optionId = key.slice(idx + 2);
7941
+ const optRef = nodeMap.get(optionId);
7942
+ if ((optRef == null ? void 0 : optRef.kind) === "option") push(key);
7943
+ continue;
7944
+ }
7945
+ const ref = nodeMap.get(key);
7946
+ if (!ref) continue;
7947
+ if (ref.kind === "option") {
7948
+ push(key);
7949
+ continue;
7950
+ }
7951
+ if (ref.kind === "field") {
7952
+ const field = (_b = (_a = ref.node) != null ? _a : ref.field) != null ? _b : ref;
7953
+ if ((field == null ? void 0 : field.button) === true) push(key);
7954
+ }
7955
+ }
7956
+ return out;
7957
+ }
7657
7958
  // ── Internals ────────────────────────────────────────────────────────────
7658
7959
  emit() {
7659
7960
  const payload = {
@@ -7676,7 +7977,7 @@ var Selection = class {
7676
7977
  this.currentTagId = Array.isArray(f.bind_id) ? f.bind_id[0] : f.bind_id;
7677
7978
  return;
7678
7979
  }
7679
- if (isOptionId2(id)) {
7980
+ if (this.builder.isOptionId(id)) {
7680
7981
  const host = fields.find(
7681
7982
  (x) => {
7682
7983
  var _a2;
@@ -7700,7 +8001,7 @@ var Selection = class {
7700
8001
  resolveTagContextId(props) {
7701
8002
  var _a;
7702
8003
  if (this.currentTagId) return this.currentTagId;
7703
- for (const id of this.set) if (isTagId2(id)) return id;
8004
+ for (const id of this.set) if (this.builder.isTagId(id)) return id;
7704
8005
  const fields = (_a = props.fields) != null ? _a : [];
7705
8006
  for (const id of this.set) {
7706
8007
  const f = fields.find((x) => x.id === id);
@@ -7708,7 +8009,7 @@ var Selection = class {
7708
8009
  return Array.isArray(f.bind_id) ? f.bind_id[0] : f.bind_id;
7709
8010
  }
7710
8011
  for (const id of this.set) {
7711
- if (isOptionId2(id)) {
8012
+ if (this.builder.isOptionId(id)) {
7712
8013
  const host = fields.find(
7713
8014
  (x) => {
7714
8015
  var _a2;
@@ -7727,28 +8028,13 @@ var Selection = class {
7727
8028
  }
7728
8029
  return this.opts.rootTagId;
7729
8030
  }
7730
- selectedButtonTriggerIds(props) {
7731
- var _a;
7732
- const fields = (_a = props.fields) != null ? _a : [];
7733
- const fieldById = new Map(fields.map((f) => [f.id, f]));
7734
- const out = [];
7735
- for (const selId of this.set) {
7736
- if (selId.startsWith("o:")) {
7737
- out.push(selId);
7738
- continue;
7739
- }
7740
- const f = fieldById.get(selId);
7741
- if ((f == null ? void 0 : f.button) === true) out.push(selId);
7742
- }
7743
- return out;
7744
- }
7745
8031
  computeGroupForTag(props, tagId) {
7746
8032
  var _a, _b, _c, _d, _e, _f, _g;
7747
8033
  const tags = (_a = props.filters) != null ? _a : [];
7748
8034
  const fields = (_b = props.fields) != null ? _b : [];
7749
8035
  const tagById = new Map(tags.map((t) => [t.id, t]));
7750
8036
  const tag = tagById.get(tagId);
7751
- const selectedTriggerIds = this.selectedButtonTriggerIds(props);
8037
+ const selectedTriggerIds = this.selectedButtons();
7752
8038
  const fieldIds = this.builder.visibleFields(tagId, selectedTriggerIds);
7753
8039
  const fieldById = new Map(fields.map((f) => [f.id, f]));
7754
8040
  const visible = fieldIds.map((id) => fieldById.get(id)).filter(Boolean);
@@ -7829,7 +8115,7 @@ var Selection = class {
7829
8115
  }
7830
8116
  findOptionById(fields, selId) {
7831
8117
  var _a, _b;
7832
- if (isOptionId2(selId)) {
8118
+ if (this.builder.isOptionId(selId)) {
7833
8119
  for (const f of fields) {
7834
8120
  const o = (_a = f.options) == null ? void 0 : _a.find((x) => x.id === selId);
7835
8121
  if (o) return o;
@@ -8251,8 +8537,8 @@ function useCanvasOwned(initialProps, canvasOpts, builderOpts) {
8251
8537
  // src/react/workspace/context/hooks/use-canvas.ts
8252
8538
  var React16 = __toESM(require("react"), 1);
8253
8539
  var import_react4 = require("react");
8254
- var isTagId3 = (id) => id.startsWith("t:");
8255
- var isOptionId3 = (id) => id.startsWith("o:");
8540
+ var isTagId = (id) => id.startsWith("t:");
8541
+ var isOptionId = (id) => id.startsWith("o:");
8256
8542
  function deriveSelectionInfo(props, ids) {
8257
8543
  var _a, _b, _c, _d;
8258
8544
  const tags = [];
@@ -8261,11 +8547,11 @@ function deriveSelectionInfo(props, ids) {
8261
8547
  const fieldById = /* @__PURE__ */ new Map();
8262
8548
  for (const f of (_a = props.fields) != null ? _a : []) fieldById.set(f.id, f);
8263
8549
  for (const id of ids) {
8264
- if (isTagId3(id)) {
8550
+ if (isTagId(id)) {
8265
8551
  tags.push(id);
8266
8552
  continue;
8267
8553
  }
8268
- if (isOptionId3(id)) {
8554
+ if (isOptionId(id)) {
8269
8555
  options.push(id);
8270
8556
  continue;
8271
8557
  }