@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.
@@ -3217,11 +3217,6 @@ function hasAnyServiceOption(f) {
3217
3217
  var _a;
3218
3218
  return ((_a = f.options) != null ? _a : []).some((o) => isFiniteNumber(o.service_id));
3219
3219
  }
3220
- function isBoundTo(f, tagId) {
3221
- const b = f.bind_id;
3222
- if (!b) return false;
3223
- return Array.isArray(b) ? b.includes(tagId) : b === tagId;
3224
- }
3225
3220
  function getByPath(obj, path) {
3226
3221
  if (!path) return void 0;
3227
3222
  const parts = path.split(".");
@@ -3307,33 +3302,214 @@ function withAffected(details, ids) {
3307
3302
  return { ...details != null ? details : {}, affectedIds: ids };
3308
3303
  }
3309
3304
 
3305
+ // src/core/node-map.ts
3306
+ function buildNodeMap(props) {
3307
+ var _a, _b, _c;
3308
+ const map = /* @__PURE__ */ new Map();
3309
+ for (const t of (_a = props.filters) != null ? _a : []) {
3310
+ if (!map.has(t.id)) map.set(t.id, { kind: "tag", id: t.id, node: t });
3311
+ }
3312
+ for (const f of (_b = props.fields) != null ? _b : []) {
3313
+ if (!map.has(f.id)) map.set(f.id, { kind: "field", id: f.id, node: f });
3314
+ for (const o of (_c = f.options) != null ? _c : []) {
3315
+ if (!map.has(o.id))
3316
+ map.set(o.id, {
3317
+ kind: "option",
3318
+ id: o.id,
3319
+ node: o,
3320
+ fieldId: f.id
3321
+ });
3322
+ }
3323
+ }
3324
+ return map;
3325
+ }
3326
+ function resolveTrigger(trigger, nodeMap) {
3327
+ const idx = trigger.indexOf("::");
3328
+ if (idx !== -1) {
3329
+ const fieldId = trigger.slice(0, idx);
3330
+ const optionId = trigger.slice(idx + 2);
3331
+ return { kind: "composite", triggerKey: trigger, fieldId, optionId };
3332
+ }
3333
+ const direct = nodeMap.get(trigger);
3334
+ if (!direct) return void 0;
3335
+ if (direct.kind === "option") {
3336
+ return {
3337
+ kind: "option",
3338
+ triggerKey: trigger,
3339
+ id: direct.id,
3340
+ fieldId: direct.fieldId
3341
+ };
3342
+ }
3343
+ return { kind: direct.kind, triggerKey: trigger, id: direct.id };
3344
+ }
3345
+
3346
+ // src/core/visibility.ts
3347
+ function visibleFieldIdsUnder(props, tagId, opts = {}) {
3348
+ var _a, _b, _c, _d, _e, _f, _g, _h, _i, _j;
3349
+ const tags = (_a = props.filters) != null ? _a : [];
3350
+ const fields = (_b = props.fields) != null ? _b : [];
3351
+ const tagById = new Map(tags.map((t) => [t.id, t]));
3352
+ const tag = tagById.get(tagId);
3353
+ if (!tag) return [];
3354
+ const nodeMap = buildNodeMap(props);
3355
+ const lineageDepth = /* @__PURE__ */ new Map();
3356
+ {
3357
+ const guard = /* @__PURE__ */ new Set();
3358
+ let cur = tag;
3359
+ let d = 0;
3360
+ while (cur && !guard.has(cur.id)) {
3361
+ lineageDepth.set(cur.id, d++);
3362
+ guard.add(cur.id);
3363
+ const parentId = cur.bind_id;
3364
+ cur = parentId ? tagById.get(parentId) : void 0;
3365
+ }
3366
+ }
3367
+ const isTagInLineage = (id) => lineageDepth.has(id);
3368
+ const fieldById = new Map(fields.map((f) => [f.id, f]));
3369
+ const ownerDepthForField = (f) => {
3370
+ const b = f.bind_id;
3371
+ if (!b) return void 0;
3372
+ if (typeof b === "string") return lineageDepth.get(b);
3373
+ let best = void 0;
3374
+ for (const id of b) {
3375
+ const d = lineageDepth.get(id);
3376
+ if (d == null) continue;
3377
+ if (best == null || d < best) best = d;
3378
+ }
3379
+ return best;
3380
+ };
3381
+ const ownerDepthForTriggerKey = (triggerKey) => {
3382
+ const t = resolveTrigger(triggerKey, nodeMap);
3383
+ if (!t) return void 0;
3384
+ if (t.kind === "composite") {
3385
+ const f = fieldById.get(t.fieldId);
3386
+ if (!f) return void 0;
3387
+ return ownerDepthForField(f);
3388
+ }
3389
+ if (t.kind === "field") {
3390
+ const f = fieldById.get(t.id);
3391
+ if (!f || f.button !== true) return void 0;
3392
+ return ownerDepthForField(f);
3393
+ }
3394
+ if (t.kind === "option") {
3395
+ const f = t.fieldId ? fieldById.get(t.fieldId) : void 0;
3396
+ if (!f) return void 0;
3397
+ return ownerDepthForField(f);
3398
+ }
3399
+ return void 0;
3400
+ };
3401
+ const tagInclude = new Set((_c = tag.includes) != null ? _c : []);
3402
+ const tagExclude = new Set((_d = tag.excludes) != null ? _d : []);
3403
+ const selected = (_e = opts.selectedKeys) != null ? _e : /* @__PURE__ */ new Set();
3404
+ const incMap = (_f = props.includes_for_buttons) != null ? _f : {};
3405
+ const excMap = (_g = props.excludes_for_buttons) != null ? _g : {};
3406
+ const relevantTriggersInOrder = [];
3407
+ for (const key of selected) {
3408
+ const d = ownerDepthForTriggerKey(key);
3409
+ if (d == null) continue;
3410
+ relevantTriggersInOrder.push(key);
3411
+ }
3412
+ const visible = /* @__PURE__ */ new Set();
3413
+ const isBoundToLineage = (f) => {
3414
+ const b = f.bind_id;
3415
+ if (!b) return false;
3416
+ if (typeof b === "string") return isTagInLineage(b);
3417
+ for (const id of b) if (isTagInLineage(id)) return true;
3418
+ return false;
3419
+ };
3420
+ for (const f of fields) {
3421
+ if (isBoundToLineage(f)) visible.add(f.id);
3422
+ if (tagInclude.has(f.id)) visible.add(f.id);
3423
+ }
3424
+ for (const id of tagExclude) visible.delete(id);
3425
+ const decide = /* @__PURE__ */ new Map();
3426
+ const applyDecision = (fieldId, next) => {
3427
+ const prev = decide.get(fieldId);
3428
+ if (!prev) return void decide.set(fieldId, next);
3429
+ if (next.depth < prev.depth) return void decide.set(fieldId, next);
3430
+ if (next.depth > prev.depth) return;
3431
+ if (prev.kind === "include" && next.kind === "exclude") {
3432
+ decide.set(fieldId, next);
3433
+ }
3434
+ };
3435
+ const revealedOrder = [];
3436
+ const revealedSeen = /* @__PURE__ */ new Set();
3437
+ for (const triggerKey of relevantTriggersInOrder) {
3438
+ const depth = ownerDepthForTriggerKey(triggerKey);
3439
+ if (depth == null) continue;
3440
+ for (const id of (_h = incMap[triggerKey]) != null ? _h : []) {
3441
+ applyDecision(id, { depth, kind: "include" });
3442
+ if (!revealedSeen.has(id)) {
3443
+ revealedSeen.add(id);
3444
+ revealedOrder.push(id);
3445
+ }
3446
+ }
3447
+ for (const id of (_i = excMap[triggerKey]) != null ? _i : []) {
3448
+ applyDecision(id, { depth, kind: "exclude" });
3449
+ }
3450
+ }
3451
+ for (const [fid, d] of decide) {
3452
+ if (d.kind === "include") visible.add(fid);
3453
+ else visible.delete(fid);
3454
+ }
3455
+ const base = fields.filter((f) => visible.has(f.id)).map((f) => f.id);
3456
+ const order = (_j = props.order_for_tags) == null ? void 0 : _j[tagId];
3457
+ if (order && order.length) {
3458
+ const ordered = order.filter((fid) => visible.has(fid));
3459
+ const orderedSet = new Set(ordered);
3460
+ const rest = base.filter((fid) => !orderedSet.has(fid));
3461
+ return [...ordered, ...rest];
3462
+ }
3463
+ return base;
3464
+ }
3465
+ function visibleFieldsUnder(props, tagId, opts = {}) {
3466
+ var _a;
3467
+ const ids = visibleFieldIdsUnder(props, tagId, opts);
3468
+ const fieldById = new Map(((_a = props.fields) != null ? _a : []).map((f) => [f.id, f]));
3469
+ return ids.map((id) => fieldById.get(id)).filter(Boolean);
3470
+ }
3471
+
3310
3472
  // src/core/validate/steps/visibility.ts
3311
3473
  function createFieldsVisibleUnder(v) {
3312
3474
  return (tagId) => {
3313
- var _a, _b, _c, _d, _e, _f;
3314
- const tag = v.tagById.get(tagId);
3315
- const includesTag = new Set((_a = tag == null ? void 0 : tag.includes) != null ? _a : []);
3316
- const excludesTag = new Set((_b = tag == null ? void 0 : tag.excludes) != null ? _b : []);
3317
- const incForOpt = (_c = v.props.includes_for_buttons) != null ? _c : {};
3318
- const excForOpt = (_d = v.props.excludes_for_buttons) != null ? _d : {};
3319
- const includesOpt = /* @__PURE__ */ new Set();
3320
- const excludesOpt = /* @__PURE__ */ new Set();
3321
- for (const key of v.selectedKeys) {
3322
- for (const id of (_e = incForOpt[key]) != null ? _e : []) includesOpt.add(id);
3323
- for (const id of (_f = excForOpt[key]) != null ? _f : []) excludesOpt.add(id);
3324
- }
3325
- const merged = /* @__PURE__ */ new Map();
3326
- for (const f of v.fields) {
3327
- if (isBoundTo(f, tagId)) merged.set(f.id, f);
3328
- if (includesTag.has(f.id)) merged.set(f.id, f);
3329
- if (includesOpt.has(f.id)) merged.set(f.id, f);
3330
- }
3331
- for (const id of excludesTag) merged.delete(id);
3332
- for (const id of excludesOpt) merged.delete(id);
3333
- return Array.from(merged.values());
3475
+ return visibleFieldsUnder(v.props, tagId, {
3476
+ selectedKeys: v.selectedKeys
3477
+ });
3334
3478
  };
3335
3479
  }
3336
- function validateVisibility(v) {
3480
+ function stableKeyOfSelection(keys) {
3481
+ return Array.from(keys).sort().join("|");
3482
+ }
3483
+ function resolveRootTags(tags) {
3484
+ const roots = tags.filter((t) => !t.bind_id);
3485
+ return roots.length ? roots : tags.slice(0, 1);
3486
+ }
3487
+ function isEffectfulTrigger(v, trigger) {
3488
+ var _a, _b, _c, _d, _e, _f;
3489
+ const inc = (_a = v.props.includes_for_buttons) != null ? _a : {};
3490
+ const exc = (_b = v.props.excludes_for_buttons) != null ? _b : {};
3491
+ 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;
3492
+ }
3493
+ function collectSelectableTriggersInContext(v, tagId, selectedKeys, onlyEffectful) {
3494
+ var _a;
3495
+ const visible = visibleFieldsUnder(v.props, tagId, {
3496
+ selectedKeys
3497
+ });
3498
+ const triggers = [];
3499
+ for (const f of visible) {
3500
+ if (f.button === true) {
3501
+ const t = f.id;
3502
+ if (!onlyEffectful || isEffectfulTrigger(v, t)) triggers.push(t);
3503
+ }
3504
+ for (const o of (_a = f.options) != null ? _a : []) {
3505
+ const t = `${f.id}::${o.id}`;
3506
+ if (!onlyEffectful || isEffectfulTrigger(v, t)) triggers.push(t);
3507
+ }
3508
+ }
3509
+ triggers.sort();
3510
+ return triggers;
3511
+ }
3512
+ function runVisibilityRulesOnce(v) {
3337
3513
  var _a, _b, _c, _d, _e;
3338
3514
  for (const t of v.tags) {
3339
3515
  const visible = v.fieldsVisibleUnder(t.id);
@@ -3408,12 +3584,85 @@ function validateVisibility(v) {
3408
3584
  }
3409
3585
  }
3410
3586
  }
3587
+ function dedupeErrorsInPlace(v, startIndex) {
3588
+ const seen = /* @__PURE__ */ new Set();
3589
+ const keyOfErr = (e) => {
3590
+ var _a, _b, _c, _d, _e, _f, _g, _h, _i, _j;
3591
+ 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 : "";
3592
+ const other = (_g = (_f = e == null ? void 0 : e.details) == null ? void 0 : _f.other) != null ? _g : "";
3593
+ 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 : ""}`;
3594
+ };
3595
+ const kept = [];
3596
+ for (let i = startIndex; i < v.errors.length; i++) {
3597
+ const e = v.errors[i];
3598
+ const k = keyOfErr(e);
3599
+ if (seen.has(k)) continue;
3600
+ seen.add(k);
3601
+ kept.push(e);
3602
+ }
3603
+ v.errors.splice(startIndex, v.errors.length - startIndex, ...kept);
3604
+ }
3605
+ function validateVisibility(v, options = {}) {
3606
+ var _a, _b, _c;
3607
+ const simulate = options.simulate === true;
3608
+ if (!simulate) {
3609
+ runVisibilityRulesOnce(v);
3610
+ return;
3611
+ }
3612
+ const maxStates = Math.max(1, (_a = options.maxStates) != null ? _a : 500);
3613
+ const maxDepth = Math.max(0, (_b = options.maxDepth) != null ? _b : 6);
3614
+ const onlyEffectful = options.onlyEffectfulTriggers !== false;
3615
+ const roots = resolveRootTags(v.tags);
3616
+ const rootTags = options.simulateAllRoots ? roots : roots.slice(0, 1);
3617
+ const originalSelected = new Set((_c = v.selectedKeys) != null ? _c : []);
3618
+ const errorsStart = v.errors.length;
3619
+ const visited = /* @__PURE__ */ new Set();
3620
+ const stack = [];
3621
+ for (const rt of rootTags) {
3622
+ stack.push({
3623
+ rootTagId: rt.id,
3624
+ selected: new Set(originalSelected),
3625
+ depth: 0
3626
+ });
3627
+ }
3628
+ let validatedStates = 0;
3629
+ while (stack.length) {
3630
+ if (validatedStates >= maxStates) break;
3631
+ const state = stack.pop();
3632
+ const sig = stableKeyOfSelection(state.selected);
3633
+ if (visited.has(sig)) continue;
3634
+ visited.add(sig);
3635
+ v.selectedKeys = state.selected;
3636
+ validatedStates++;
3637
+ runVisibilityRulesOnce(v);
3638
+ if (state.depth >= maxDepth) continue;
3639
+ const triggers = collectSelectableTriggersInContext(
3640
+ v,
3641
+ state.rootTagId,
3642
+ state.selected,
3643
+ onlyEffectful
3644
+ );
3645
+ for (let i = triggers.length - 1; i >= 0; i--) {
3646
+ const trig = triggers[i];
3647
+ if (state.selected.has(trig)) continue;
3648
+ const next = new Set(state.selected);
3649
+ next.add(trig);
3650
+ stack.push({
3651
+ rootTagId: state.rootTagId,
3652
+ selected: next,
3653
+ depth: state.depth + 1
3654
+ });
3655
+ }
3656
+ }
3657
+ v.selectedKeys = originalSelected;
3658
+ dedupeErrorsInPlace(v, errorsStart);
3659
+ }
3411
3660
 
3412
3661
  // src/core/validate/steps/structure.ts
3413
3662
  function validateStructure(v) {
3414
3663
  const tags = v.tags;
3415
3664
  const fields = v.fields;
3416
- if (!tags.some((t) => t.id === "root")) {
3665
+ if (!tags.some((t) => t.id === "t:root")) {
3417
3666
  v.errors.push({
3418
3667
  code: "root_missing",
3419
3668
  severity: "error",
@@ -3603,58 +3852,91 @@ function validateIdentity(v) {
3603
3852
  }
3604
3853
 
3605
3854
  // src/core/validate/steps/option-maps.ts
3855
+ function parseFieldOptionKey(key) {
3856
+ const idx = key.indexOf("::");
3857
+ if (idx === -1) return null;
3858
+ const fieldId = key.slice(0, idx).trim();
3859
+ const optionId = key.slice(idx + 2).trim();
3860
+ if (!fieldId || !optionId) return null;
3861
+ return { fieldId, optionId };
3862
+ }
3863
+ function hasOption(v, fid, oid) {
3864
+ var _a;
3865
+ const f = v.fieldById.get(fid);
3866
+ if (!f) return false;
3867
+ return !!((_a = f.options) != null ? _a : []).find((o) => o.id === oid);
3868
+ }
3606
3869
  function validateOptionMaps(v) {
3607
3870
  var _a, _b;
3608
3871
  const incMap = (_a = v.props.includes_for_buttons) != null ? _a : {};
3609
3872
  const excMap = (_b = v.props.excludes_for_buttons) != null ? _b : {};
3610
- const parseKey = (key) => {
3611
- const parts = key.split("::");
3612
- const fid = parts[0];
3613
- const oid = parts[1];
3614
- if (!fid || !oid) return null;
3615
- return { fieldId: fid, optionId: oid };
3616
- };
3617
- const hasOption = (fid, oid) => {
3618
- var _a2;
3619
- const f = v.fieldById.get(fid);
3620
- if (!f) return false;
3621
- return !!((_a2 = f.options) != null ? _a2 : []).find((o) => o.id === oid);
3873
+ 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.`;
3874
+ const validateTriggerKey = (key) => {
3875
+ const ref = v.nodeMap.get(key);
3876
+ if (ref) {
3877
+ if (ref.kind === "option") {
3878
+ return {
3879
+ ok: true,
3880
+ nodeId: ref.fieldId,
3881
+ affected: [ref.fieldId, ref.id]
3882
+ };
3883
+ }
3884
+ if (ref.kind === "field") {
3885
+ const isButton = ref.node.button === true;
3886
+ if (!isButton)
3887
+ return { ok: false, nodeId: ref.id, affected: [ref.id] };
3888
+ return { ok: true, nodeId: ref.id, affected: [ref.id] };
3889
+ }
3890
+ return { ok: false, nodeId: ref.id, affected: [ref.id] };
3891
+ }
3892
+ const p = parseFieldOptionKey(key);
3893
+ if (!p) return { ok: false };
3894
+ if (!hasOption(v, p.fieldId, p.optionId))
3895
+ return {
3896
+ ok: false,
3897
+ nodeId: p.fieldId,
3898
+ affected: [p.fieldId, p.optionId]
3899
+ };
3900
+ return {
3901
+ ok: true,
3902
+ nodeId: p.fieldId,
3903
+ affected: [p.fieldId, p.optionId]
3904
+ };
3622
3905
  };
3623
- const badKeyMessage = (key) => `Invalid option-map key "${key}". Expected "fieldId::optionId" pointing to an existing option.`;
3624
3906
  for (const k of Object.keys(incMap)) {
3625
- const p = parseKey(k);
3626
- if (!p || !hasOption(p.fieldId, p.optionId)) {
3907
+ const r = validateTriggerKey(k);
3908
+ if (!r.ok) {
3627
3909
  v.errors.push({
3628
3910
  code: "bad_option_key",
3629
3911
  severity: "error",
3630
3912
  message: badKeyMessage(k),
3631
- details: { key: k }
3913
+ nodeId: r.nodeId,
3914
+ details: withAffected({ key: k }, r.affected)
3632
3915
  });
3633
3916
  }
3634
3917
  }
3635
3918
  for (const k of Object.keys(excMap)) {
3636
- const p = parseKey(k);
3637
- if (!p || !hasOption(p.fieldId, p.optionId)) {
3919
+ const r = validateTriggerKey(k);
3920
+ if (!r.ok) {
3638
3921
  v.errors.push({
3639
3922
  code: "bad_option_key",
3640
3923
  severity: "error",
3641
3924
  message: badKeyMessage(k),
3642
- details: { key: k }
3925
+ nodeId: r.nodeId,
3926
+ details: withAffected({ key: k }, r.affected)
3643
3927
  });
3644
3928
  }
3645
3929
  }
3646
3930
  for (const k of Object.keys(incMap)) {
3647
- if (k in excMap) {
3648
- const p = parseKey(k);
3649
- const affected = p ? [p.fieldId, p.optionId] : void 0;
3650
- v.errors.push({
3651
- code: "option_include_exclude_conflict",
3652
- severity: "error",
3653
- message: `Option-map key "${k}" appears in both includes_for_buttons and excludes_for_buttons.`,
3654
- nodeId: p == null ? void 0 : p.fieldId,
3655
- details: withAffected({ key: k }, affected)
3656
- });
3657
- }
3931
+ if (!(k in excMap)) continue;
3932
+ const r = validateTriggerKey(k);
3933
+ v.errors.push({
3934
+ code: "option_include_exclude_conflict",
3935
+ severity: "error",
3936
+ message: `Trigger-map key "${k}" appears in both includes_for_buttons and excludes_for_buttons.`,
3937
+ nodeId: r.nodeId,
3938
+ details: withAffected({ key: k }, r.affected)
3939
+ });
3658
3940
  }
3659
3941
  }
3660
3942
 
@@ -4559,8 +4841,23 @@ function applyPolicies(errors, props, serviceMap, policies, fieldsVisibleUnder,
4559
4841
  }
4560
4842
 
4561
4843
  // src/core/validate/index.ts
4844
+ function readVisibilitySimOpts(ctx) {
4845
+ const c = ctx;
4846
+ const simulate = c.simulateVisibility === true || c.visibilitySimulate === true || c.simulate === true;
4847
+ const maxStates = typeof c.maxVisibilityStates === "number" ? c.maxVisibilityStates : typeof c.visibilityMaxStates === "number" ? c.visibilityMaxStates : typeof c.maxStates === "number" ? c.maxStates : void 0;
4848
+ const maxDepth = typeof c.maxVisibilityDepth === "number" ? c.maxVisibilityDepth : typeof c.visibilityMaxDepth === "number" ? c.visibilityMaxDepth : typeof c.maxDepth === "number" ? c.maxDepth : void 0;
4849
+ const simulateAllRoots = c.simulateAllRoots === true || c.visibilitySimulateAllRoots === true;
4850
+ const onlyEffectfulTriggers = c.onlyEffectfulTriggers === false ? false : c.visibilityOnlyEffectfulTriggers !== false;
4851
+ return {
4852
+ simulate,
4853
+ maxStates,
4854
+ maxDepth,
4855
+ simulateAllRoots,
4856
+ onlyEffectfulTriggers
4857
+ };
4858
+ }
4562
4859
  function validate(props, ctx = {}) {
4563
- var _a, _b;
4860
+ var _a, _b, _c;
4564
4861
  const errors = [];
4565
4862
  const serviceMap = (_a = ctx.serviceMap) != null ? _a : {};
4566
4863
  const selectedKeys = new Set(
@@ -4574,6 +4871,7 @@ function validate(props, ctx = {}) {
4574
4871
  for (const f of fields) fieldById.set(f.id, f);
4575
4872
  const v = {
4576
4873
  props,
4874
+ nodeMap: (_c = ctx.nodeMap) != null ? _c : buildNodeMap(props),
4577
4875
  options: ctx,
4578
4876
  errors,
4579
4877
  serviceMap,
@@ -4588,7 +4886,8 @@ function validate(props, ctx = {}) {
4588
4886
  validateIdentity(v);
4589
4887
  validateOptionMaps(v);
4590
4888
  v.fieldsVisibleUnder = createFieldsVisibleUnder(v);
4591
- validateVisibility(v);
4889
+ const visSim = readVisibilitySimOpts(ctx);
4890
+ validateVisibility(v, visSim);
4592
4891
  applyPolicies(
4593
4892
  v.errors,
4594
4893
  v.props,
@@ -4624,11 +4923,21 @@ var BuilderImpl = class {
4624
4923
  this.optionOwnerById = /* @__PURE__ */ new Map();
4625
4924
  this.history = [];
4626
4925
  this.future = [];
4926
+ this._nodemap = null;
4627
4927
  var _a;
4628
4928
  this.options = { ...opts };
4629
4929
  this.historyLimit = (_a = opts.historyLimit) != null ? _a : 50;
4630
4930
  }
4631
4931
  /* ───── lifecycle ─────────────────────────────────────────────────────── */
4932
+ isTagId(id) {
4933
+ return this.tagById.has(id);
4934
+ }
4935
+ isFieldId(id) {
4936
+ return this.fieldById.has(id);
4937
+ }
4938
+ isOptionId(id) {
4939
+ return this.optionOwnerById.has(id);
4940
+ }
4632
4941
  load(raw) {
4633
4942
  const next = normalise(raw, {
4634
4943
  defaultPricingRole: "base",
@@ -4848,129 +5157,16 @@ var BuilderImpl = class {
4848
5157
  return validate(this.props, this.options);
4849
5158
  }
4850
5159
  visibleFields(tagId, selectedKeys) {
4851
- var _a, _b, _c, _d, _e, _f, _g, _h, _i, _j, _k;
4852
- const props = this.props;
4853
- const tags = (_a = props.filters) != null ? _a : [];
4854
- const fields = (_b = props.fields) != null ? _b : [];
4855
- const tagById = new Map(tags.map((t) => [t.id, t]));
4856
- const tag = tagById.get(tagId);
4857
- if (!tag) return [];
4858
- const lineageDepth = /* @__PURE__ */ new Map();
4859
- {
4860
- const guard = /* @__PURE__ */ new Set();
4861
- let cur = tag;
4862
- let d = 0;
4863
- while (cur && !guard.has(cur.id)) {
4864
- lineageDepth.set(cur.id, d++);
4865
- guard.add(cur.id);
4866
- const parentId = cur.bind_id;
4867
- cur = parentId ? tagById.get(parentId) : void 0;
4868
- }
4869
- }
4870
- const isTagInLineage = (id) => lineageDepth.has(id);
4871
- const fieldById = new Map(fields.map((f) => [f.id, f]));
4872
- const optionOwnerFieldId = /* @__PURE__ */ new Map();
4873
- for (const f of fields) {
4874
- for (const o of (_c = f.options) != null ? _c : []) optionOwnerFieldId.set(o.id, f.id);
4875
- }
4876
- const ownerDepthForField = (f) => {
4877
- const b = f.bind_id;
4878
- if (!b) return void 0;
4879
- if (typeof b === "string") return lineageDepth.get(b);
4880
- let best = void 0;
4881
- for (const id of b) {
4882
- const d = lineageDepth.get(id);
4883
- if (d == null) continue;
4884
- if (best == null || d < best) best = d;
4885
- }
4886
- return best;
4887
- };
4888
- const ownerDepthForTrigger = (triggerId) => {
4889
- if (triggerId.startsWith("o:")) {
4890
- const fid = optionOwnerFieldId.get(triggerId);
4891
- if (!fid) return void 0;
4892
- const f2 = fieldById.get(fid);
4893
- if (!f2) return void 0;
4894
- return ownerDepthForField(f2);
4895
- }
4896
- const f = fieldById.get(triggerId);
4897
- if (!f || f.button !== true) return void 0;
4898
- return ownerDepthForField(f);
4899
- };
4900
- const tagInclude = new Set((_d = tag.includes) != null ? _d : []);
4901
- const tagExclude = new Set((_e = tag.excludes) != null ? _e : []);
4902
- const selected = new Set(
4903
- (_f = selectedKeys != null ? selectedKeys : this.options.selectedOptionKeys) != null ? _f : []
4904
- );
4905
- const incMap = (_g = props.includes_for_buttons) != null ? _g : {};
4906
- const excMap = (_h = props.excludes_for_buttons) != null ? _h : {};
4907
- const relevantTriggersInOrder = [];
4908
- for (const key of selected) {
4909
- const d = ownerDepthForTrigger(key);
4910
- if (d == null) continue;
4911
- relevantTriggersInOrder.push(key);
4912
- }
4913
- const visible = /* @__PURE__ */ new Set();
4914
- const isBoundToLineage = (f) => {
4915
- const b = f.bind_id;
4916
- if (!b) return false;
4917
- if (typeof b === "string") return isTagInLineage(b);
4918
- for (const id of b) if (isTagInLineage(id)) return true;
4919
- return false;
4920
- };
4921
- for (const f of fields) {
4922
- if (isBoundToLineage(f)) visible.add(f.id);
4923
- if (tagInclude.has(f.id)) visible.add(f.id);
4924
- }
4925
- for (const id of tagExclude) visible.delete(id);
4926
- const decide = /* @__PURE__ */ new Map();
4927
- const applyDecision = (fieldId, next) => {
4928
- const prev = decide.get(fieldId);
4929
- if (!prev) {
4930
- decide.set(fieldId, next);
4931
- return;
4932
- }
4933
- if (next.depth < prev.depth) {
4934
- decide.set(fieldId, next);
4935
- return;
4936
- }
4937
- if (next.depth > prev.depth) return;
4938
- if (prev.kind === "include" && next.kind === "exclude") {
4939
- decide.set(fieldId, next);
4940
- }
4941
- };
4942
- const revealedOrder = [];
4943
- const revealedSeen = /* @__PURE__ */ new Set();
4944
- for (const triggerId of relevantTriggersInOrder) {
4945
- const depth = ownerDepthForTrigger(triggerId);
4946
- if (depth == null) continue;
4947
- for (const id of (_i = incMap[triggerId]) != null ? _i : []) {
4948
- applyDecision(id, { depth, kind: "include" });
4949
- if (!revealedSeen.has(id)) {
4950
- revealedSeen.add(id);
4951
- revealedOrder.push(id);
4952
- }
4953
- }
4954
- for (const id of (_j = excMap[triggerId]) != null ? _j : []) {
4955
- applyDecision(id, { depth, kind: "exclude" });
4956
- }
4957
- }
4958
- for (const [fid, d] of decide) {
4959
- if (d.kind === "include") visible.add(fid);
4960
- else visible.delete(fid);
4961
- }
4962
- const base = fields.filter((f) => visible.has(f.id)).map((f) => f.id);
4963
- const order = (_k = props.order_for_tags) == null ? void 0 : _k[tagId];
4964
- if (order && order.length) {
4965
- const ordered = order.filter((fid) => visible.has(fid));
4966
- const orderedSet = new Set(ordered);
4967
- const rest2 = base.filter((fid) => !orderedSet.has(fid));
4968
- return [...ordered, ...rest2];
4969
- }
4970
- const promoted = revealedOrder.filter((fid) => visible.has(fid));
4971
- const promotedSet = new Set(promoted);
4972
- const rest = base.filter((fid) => !promotedSet.has(fid));
4973
- return [...promoted, ...rest];
5160
+ var _a;
5161
+ return visibleFieldIdsUnder(this.props, tagId, {
5162
+ selectedKeys: new Set(
5163
+ (_a = selectedKeys != null ? selectedKeys : this.options.selectedOptionKeys) != null ? _a : []
5164
+ )
5165
+ });
5166
+ }
5167
+ getNodeMap() {
5168
+ if (!this._nodemap) this._nodemap = buildNodeMap(this.getProps());
5169
+ return this._nodemap;
4974
5170
  }
4975
5171
  /* ───── history ─────────────────────────────────────────────────────── */
4976
5172
  undo() {
@@ -4994,6 +5190,7 @@ var BuilderImpl = class {
4994
5190
  this.tagById.clear();
4995
5191
  this.fieldById.clear();
4996
5192
  this.optionOwnerById.clear();
5193
+ this._nodemap = null;
4997
5194
  for (const t of this.props.filters) this.tagById.set(t.id, t);
4998
5195
  for (const f of this.props.fields) {
4999
5196
  this.fieldById.set(f.id, f);
@@ -5221,6 +5418,7 @@ var toBindList = (b) => {
5221
5418
  function createNodeIndex(builder) {
5222
5419
  var _a, _b, _c, _d;
5223
5420
  const props = builder.getProps();
5421
+ const nodeMap = builder.getNodeMap();
5224
5422
  const tags = (_a = props.filters) != null ? _a : [];
5225
5423
  const fields = (_b = props.fields) != null ? _b : [];
5226
5424
  const tagById = new Map(tags.map((t) => [t.id, t]));
@@ -5424,7 +5622,9 @@ function createNodeIndex(builder) {
5424
5622
  return parentById.get(id) === tid;
5425
5623
  },
5426
5624
  getDescendant(descendantId) {
5427
- return this.getDescendants().find((item) => item.id == descendantId);
5625
+ return this.getDescendants().find(
5626
+ (item) => item.id == descendantId
5627
+ );
5428
5628
  },
5429
5629
  getDescendants() {
5430
5630
  const results = [];
@@ -5433,7 +5633,9 @@ function createNodeIndex(builder) {
5433
5633
  const node2 = getField(fieldId);
5434
5634
  if (!node2) continue;
5435
5635
  const explicit = includes.has(fieldId) || isFieldBoundDirectToTag(fieldId, id);
5436
- results.push(explicit ? node2 : { ...node2, isInherited: true });
5636
+ results.push(
5637
+ explicit ? node2 : { ...node2, isInherited: true }
5638
+ );
5437
5639
  }
5438
5640
  return Object.freeze(results);
5439
5641
  }
@@ -5482,7 +5684,9 @@ function createNodeIndex(builder) {
5482
5684
  return false;
5483
5685
  },
5484
5686
  getDescendant(descendantId, context) {
5485
- return this.getDescendants(context).find((item) => item.id == descendantId);
5687
+ return this.getDescendants(context).find(
5688
+ (item) => item.id == descendantId
5689
+ );
5486
5690
  },
5487
5691
  getDescendants(tagId) {
5488
5692
  return resolveDescendants(id, includes, tagId, !isButton);
@@ -5524,7 +5728,9 @@ function createNodeIndex(builder) {
5524
5728
  return owner.isBoundTo(tagId);
5525
5729
  },
5526
5730
  getDescendant(descendantId, context) {
5527
- return this.getDescendants(context).find((item) => item.id == descendantId);
5731
+ return this.getDescendants(context).find(
5732
+ (item) => item.id == descendantId
5733
+ );
5528
5734
  },
5529
5735
  getDescendants(tagId) {
5530
5736
  return resolveDescendants(id, includes, tagId);
@@ -5535,7 +5741,7 @@ function createNodeIndex(builder) {
5535
5741
  return node;
5536
5742
  };
5537
5743
  const getNode = (input) => {
5538
- var _a2, _b2, _c2, _d2, _e, _f, _g, _h, _i;
5744
+ var _a2, _b2, _c2, _d2, _e;
5539
5745
  if (typeof input !== "string") {
5540
5746
  if ("bind_id" in input && !("type" in input))
5541
5747
  return (_a2 = getTag(input.id)) != null ? _a2 : mkUnknown(input.id);
@@ -5545,11 +5751,7 @@ function createNodeIndex(builder) {
5545
5751
  }
5546
5752
  const cached = nodeCache.get(input);
5547
5753
  if (cached) return cached;
5548
- const id = input;
5549
- if (id.startsWith("t:")) return (_d2 = getTag(id)) != null ? _d2 : mkUnknown(id);
5550
- if (id.startsWith("f:")) return (_e = getField(id)) != null ? _e : mkUnknown(id);
5551
- if (id.startsWith("o:")) return (_f = getOption(id)) != null ? _f : mkUnknown(id);
5552
- return (_i = (_h = (_g = getTag(id)) != null ? _g : getField(id)) != null ? _h : getOption(id)) != null ? _i : mkUnknown(id);
5754
+ return (_e = (_d2 = nodeMap.get(input)) == null ? void 0 : _d2.node) != null ? _e : mkUnknown(input);
5553
5755
  };
5554
5756
  const mkUnknown = (id) => {
5555
5757
  const u = {
@@ -5609,9 +5811,6 @@ function rateOk(svcMap, candidate, primary, policy) {
5609
5811
 
5610
5812
  // src/react/canvas/editor.ts
5611
5813
  var MAX_LIMIT = 100;
5612
- var isTagId = (id) => id.startsWith("t:");
5613
- var isFieldId = (id) => id.startsWith("f:");
5614
- var isOptionId = (id) => id.startsWith("o:");
5615
5814
  function ownerOfOption(props, optionId) {
5616
5815
  var _a, _b;
5617
5816
  for (const f of (_a = props.fields) != null ? _a : []) {
@@ -5653,6 +5852,15 @@ var Editor = class {
5653
5852
  this.pushHistory(this.makeSnapshot("init"));
5654
5853
  }
5655
5854
  /* ───────────────────────── Public API ───────────────────────── */
5855
+ isTagId(id) {
5856
+ return this.builder.isTagId(id);
5857
+ }
5858
+ isFieldId(id) {
5859
+ return this.builder.isFieldId(id);
5860
+ }
5861
+ isOptionId(id) {
5862
+ return this.builder.isOptionId(id);
5863
+ }
5656
5864
  getProps() {
5657
5865
  return this.builder.getProps();
5658
5866
  }
@@ -5748,7 +5956,7 @@ var Editor = class {
5748
5956
  name: "reLabel",
5749
5957
  do: () => this.patchProps((p) => {
5750
5958
  var _a, _b, _c, _d, _e, _f, _g;
5751
- if (isTagId(id)) {
5959
+ if (this.isTagId(id)) {
5752
5960
  const t = ((_a = p.filters) != null ? _a : []).find((x) => x.id === id);
5753
5961
  if (!t) return;
5754
5962
  if (((_b = t.label) != null ? _b : "") === label) return;
@@ -5756,7 +5964,7 @@ var Editor = class {
5756
5964
  this.api.refreshGraph();
5757
5965
  return;
5758
5966
  }
5759
- if (isOptionId(id)) {
5967
+ if (this.isOptionId(id)) {
5760
5968
  const own = ownerOfOption(p, id);
5761
5969
  if (!own) return;
5762
5970
  const f = ((_c = p.fields) != null ? _c : []).find(
@@ -6057,7 +6265,7 @@ var Editor = class {
6057
6265
  * - Option: use placeOption() instead
6058
6266
  */
6059
6267
  placeNode(id, opts) {
6060
- if (isTagId(id)) {
6268
+ if (this.isTagId(id)) {
6061
6269
  this.exec({
6062
6270
  name: "placeTag",
6063
6271
  do: () => this.patchProps((p) => {
@@ -6104,7 +6312,7 @@ var Editor = class {
6104
6312
  }),
6105
6313
  undo: () => this.api.undo()
6106
6314
  });
6107
- } else if (isFieldId(id)) {
6315
+ } else if (this.isFieldId(id)) {
6108
6316
  if (!opts.scopeTagId)
6109
6317
  throw new Error("placeNode(field): scopeTagId is required");
6110
6318
  const fieldId = id;
@@ -6131,14 +6339,14 @@ var Editor = class {
6131
6339
  }),
6132
6340
  undo: () => this.api.undo()
6133
6341
  });
6134
- } else if (isOptionId(id)) {
6342
+ } else if (this.isOptionId(id)) {
6135
6343
  this.placeOption(id, opts);
6136
6344
  } else {
6137
6345
  throw new Error("placeNode: unknown id prefix");
6138
6346
  }
6139
6347
  }
6140
6348
  placeOption(optionId, opts) {
6141
- if (!isOptionId(optionId))
6349
+ if (!this.isOptionId(optionId))
6142
6350
  throw new Error('placeOption: optionId must start with "o:"');
6143
6351
  this.exec({
6144
6352
  name: "placeOption",
@@ -6195,7 +6403,7 @@ var Editor = class {
6195
6403
  return id;
6196
6404
  }
6197
6405
  updateOption(optionId, patch) {
6198
- if (!isOptionId(optionId))
6406
+ if (!this.isOptionId(optionId))
6199
6407
  throw new Error('updateOption: optionId must start with "o:"');
6200
6408
  this.exec({
6201
6409
  name: "updateOption",
@@ -6214,7 +6422,7 @@ var Editor = class {
6214
6422
  });
6215
6423
  }
6216
6424
  removeOption(optionId) {
6217
- if (!isOptionId(optionId))
6425
+ if (!this.isOptionId(optionId))
6218
6426
  throw new Error('removeOption: optionId must start with "o:"');
6219
6427
  this.exec({
6220
6428
  name: "removeOption",
@@ -6245,17 +6453,17 @@ var Editor = class {
6245
6453
  name: "editLabel",
6246
6454
  do: () => this.patchProps((p) => {
6247
6455
  var _a, _b, _c, _d;
6248
- if (isTagId(id)) {
6456
+ if (this.isTagId(id)) {
6249
6457
  const t = ((_a = p.filters) != null ? _a : []).find((x) => x.id === id);
6250
6458
  if (t) t.label = next;
6251
6459
  return;
6252
6460
  }
6253
- if (isFieldId(id)) {
6461
+ if (this.isFieldId(id)) {
6254
6462
  const f = ((_b = p.fields) != null ? _b : []).find((x) => x.id === id);
6255
6463
  if (f) f.label = next;
6256
6464
  return;
6257
6465
  }
6258
- if (isOptionId(id)) {
6466
+ if (this.isOptionId(id)) {
6259
6467
  const own = ownerOfOption(p, id);
6260
6468
  if (!own) return;
6261
6469
  const f = ((_c = p.fields) != null ? _c : []).find(
@@ -6294,7 +6502,7 @@ var Editor = class {
6294
6502
  const validId = hasSidKey && typeof input.service_id === "number" && Number.isFinite(input.service_id);
6295
6503
  const sid = validId ? Number(input.service_id) : void 0;
6296
6504
  const nextRole = input.pricing_role;
6297
- if (isTagId(id)) {
6505
+ if (this.isTagId(id)) {
6298
6506
  const t = ((_a = p.filters) != null ? _a : []).find((x) => x.id === id);
6299
6507
  if (!t) return;
6300
6508
  if (hasSidKey) {
@@ -6303,7 +6511,7 @@ var Editor = class {
6303
6511
  }
6304
6512
  return;
6305
6513
  }
6306
- if (isOptionId(id)) {
6514
+ if (this.isOptionId(id)) {
6307
6515
  const own = ownerOfOption(p, id);
6308
6516
  if (!own) return;
6309
6517
  const f2 = ((_b = p.fields) != null ? _b : []).find(
@@ -6510,7 +6718,7 @@ var Editor = class {
6510
6718
  });
6511
6719
  }
6512
6720
  remove(id) {
6513
- if (isTagId(id)) {
6721
+ if (this.isTagId(id)) {
6514
6722
  this.exec({
6515
6723
  name: "removeTag",
6516
6724
  do: () => this.patchProps((p) => {
@@ -6550,7 +6758,7 @@ var Editor = class {
6550
6758
  });
6551
6759
  return;
6552
6760
  }
6553
- if (isFieldId(id)) {
6761
+ if (this.isFieldId(id)) {
6554
6762
  this.exec({
6555
6763
  name: "removeField",
6556
6764
  do: () => this.patchProps((p) => {
@@ -6586,7 +6794,7 @@ var Editor = class {
6586
6794
  });
6587
6795
  return;
6588
6796
  }
6589
- if (isOptionId(id)) {
6797
+ if (this.isOptionId(id)) {
6590
6798
  this.removeOption(id);
6591
6799
  return;
6592
6800
  }
@@ -6595,7 +6803,7 @@ var Editor = class {
6595
6803
  getNode(id) {
6596
6804
  var _a, _b, _c, _d;
6597
6805
  const props = this.builder.getProps();
6598
- if (isTagId(id)) {
6806
+ if (this.isTagId(id)) {
6599
6807
  const t = ((_a = props.filters) != null ? _a : []).find((x) => x.id === id);
6600
6808
  return {
6601
6809
  kind: "tag",
@@ -6603,12 +6811,12 @@ var Editor = class {
6603
6811
  owners: { parentTagId: t == null ? void 0 : t.bind_id }
6604
6812
  };
6605
6813
  }
6606
- if (isFieldId(id)) {
6814
+ if (this.isFieldId(id)) {
6607
6815
  const f = ((_b = props.fields) != null ? _b : []).find((x) => x.id === id);
6608
6816
  const bind = Array.isArray(f == null ? void 0 : f.bind_id) ? f.bind_id : (f == null ? void 0 : f.bind_id) ? [f.bind_id] : [];
6609
6817
  return { kind: "field", data: f, owners: { bindTagIds: bind } };
6610
6818
  }
6611
- if (isOptionId(id)) {
6819
+ if (this.isOptionId(id)) {
6612
6820
  const own = ownerOfOption(props, id);
6613
6821
  const f = own ? ((_c = props.fields) != null ? _c : []).find((x) => x.id === own.fieldId) : void 0;
6614
6822
  const o = (_d = f == null ? void 0 : f.options) == null ? void 0 : _d.find((x) => x.id === id);
@@ -6686,7 +6894,7 @@ var Editor = class {
6686
6894
  if (receiverId === targetId) return true;
6687
6895
  const getDirectRelations = (id) => {
6688
6896
  var _a, _b, _c, _d, _e, _f, _g;
6689
- if (isTagId(id)) {
6897
+ if (this.isTagId(id)) {
6690
6898
  const t = ((_a = p.filters) != null ? _a : []).find((x) => x.id === id);
6691
6899
  return [...(_b = t == null ? void 0 : t.includes) != null ? _b : [], ...(_c = t == null ? void 0 : t.excludes) != null ? _c : []];
6692
6900
  }
@@ -6906,7 +7114,7 @@ var Editor = class {
6906
7114
  do: () => this.patchProps((p) => {
6907
7115
  var _a, _b, _c, _d, _e;
6908
7116
  if (kind === "bind") {
6909
- if (isTagId(fromId) && isTagId(toId2)) {
7117
+ if (this.isTagId(fromId) && this.isTagId(toId2)) {
6910
7118
  if (this.wouldCreateTagCycle(p, fromId, toId2)) {
6911
7119
  throw new Error(
6912
7120
  `bind would create a cycle: ${fromId} \u2192 ${toId2}`
@@ -6918,9 +7126,9 @@ var Editor = class {
6918
7126
  if (child) child.bind_id = fromId;
6919
7127
  return;
6920
7128
  }
6921
- if (isTagId(fromId) && isFieldId(toId2) || isFieldId(fromId) && isTagId(toId2)) {
6922
- const fieldId = isFieldId(toId2) ? toId2 : fromId;
6923
- const tagId = isTagId(fromId) ? fromId : toId2;
7129
+ if (this.isTagId(fromId) && this.isFieldId(toId2) || this.isFieldId(fromId) && this.isTagId(toId2)) {
7130
+ const fieldId = this.isFieldId(toId2) ? toId2 : fromId;
7131
+ const tagId = this.isTagId(fromId) ? fromId : toId2;
6924
7132
  const f = ((_b = p.fields) != null ? _b : []).find(
6925
7133
  (x) => x.id === fieldId
6926
7134
  );
@@ -6944,7 +7152,7 @@ var Editor = class {
6944
7152
  }
6945
7153
  if (kind === "include" || kind === "exclude") {
6946
7154
  const key = kind === "include" ? "includes" : "excludes";
6947
- if (isTagId(fromId) && isFieldId(toId2)) {
7155
+ if (this.isTagId(fromId) && this.isFieldId(toId2)) {
6948
7156
  const t = ((_c = p.filters) != null ? _c : []).find(
6949
7157
  (x) => x.id === fromId
6950
7158
  );
@@ -6953,7 +7161,7 @@ var Editor = class {
6953
7161
  if (!arr.includes(toId2)) arr.push(toId2);
6954
7162
  return;
6955
7163
  }
6956
- if (isOptionId(fromId) && isFieldId(toId2)) {
7164
+ if (this.isOptionId(fromId) && this.isFieldId(toId2)) {
6957
7165
  const mapKey = kind === "include" ? "includes_for_options" : "excludes_for_options";
6958
7166
  const maps = p[mapKey];
6959
7167
  const next = { ...maps != null ? maps : {} };
@@ -7021,16 +7229,16 @@ var Editor = class {
7021
7229
  do: () => this.patchProps((p) => {
7022
7230
  var _a, _b, _c, _d, _e, _f, _g, _h;
7023
7231
  if (kind === "bind") {
7024
- if (isTagId(fromId) && isTagId(toId2)) {
7232
+ if (this.isTagId(fromId) && this.isTagId(toId2)) {
7025
7233
  const child = ((_a = p.filters) != null ? _a : []).find(
7026
7234
  (t) => t.id === toId2
7027
7235
  );
7028
7236
  if ((child == null ? void 0 : child.bind_id) === fromId) delete child.bind_id;
7029
7237
  return;
7030
7238
  }
7031
- if (isTagId(fromId) && isFieldId(toId2) || isFieldId(fromId) && isTagId(toId2)) {
7032
- const fieldId = isFieldId(toId2) ? toId2 : fromId;
7033
- const tagId = isTagId(fromId) ? fromId : toId2;
7239
+ if (this.isTagId(fromId) && this.isFieldId(toId2) || this.isFieldId(fromId) && this.isTagId(toId2)) {
7240
+ const fieldId = this.isFieldId(toId2) ? toId2 : fromId;
7241
+ const tagId = this.isTagId(fromId) ? fromId : toId2;
7034
7242
  const f = ((_b = p.fields) != null ? _b : []).find(
7035
7243
  (x) => x.id === fieldId
7036
7244
  );
@@ -7051,7 +7259,7 @@ var Editor = class {
7051
7259
  }
7052
7260
  if (kind === "include" || kind === "exclude") {
7053
7261
  const key = kind === "include" ? "includes" : "excludes";
7054
- if (isTagId(fromId) && isFieldId(toId2)) {
7262
+ if (this.isTagId(fromId) && this.isFieldId(toId2)) {
7055
7263
  const t = ((_d = p.filters) != null ? _d : []).find(
7056
7264
  (x) => x.id === fromId
7057
7265
  );
@@ -7060,7 +7268,7 @@ var Editor = class {
7060
7268
  if (!((_f = t[key]) == null ? void 0 : _f.length)) delete t[key];
7061
7269
  return;
7062
7270
  }
7063
- if (isOptionId(fromId) && isFieldId(toId2)) {
7271
+ if (this.isOptionId(fromId) && this.isFieldId(toId2)) {
7064
7272
  const mapKey = kind === "include" ? "includes_for_options" : "excludes_for_options";
7065
7273
  const maps = p[mapKey];
7066
7274
  if (!maps) return;
@@ -7527,8 +7735,6 @@ function toStrSet(v) {
7527
7735
  }
7528
7736
 
7529
7737
  // src/react/canvas/selection.ts
7530
- var isTagId2 = (id) => typeof id === "string" && id.startsWith("t:");
7531
- var isOptionId2 = (id) => typeof id === "string" && id.startsWith("o:");
7532
7738
  var Selection = class {
7533
7739
  constructor(builder, opts) {
7534
7740
  this.builder = builder;
@@ -7546,6 +7752,7 @@ var Selection = class {
7546
7752
  this.emit();
7547
7753
  }
7548
7754
  add(id) {
7755
+ if (this.set.has(id)) this.set.delete(id);
7549
7756
  this.set.add(id);
7550
7757
  this.primaryId = id;
7551
7758
  this.updateCurrentTagFrom(id);
@@ -7600,7 +7807,9 @@ var Selection = class {
7600
7807
  var _a;
7601
7808
  const props = this.builder.getProps();
7602
7809
  if (((_a = this.opts.env) != null ? _a : "client") === "workspace") {
7603
- const tagIds = Array.from(this.set).filter(isTagId2);
7810
+ const tagIds = Array.from(this.set).filter(
7811
+ this.builder.isTagId.bind(this.builder)
7812
+ );
7604
7813
  if (tagIds.length > 1) {
7605
7814
  return { kind: "multi", groups: Array.from(this.set) };
7606
7815
  }
@@ -7611,6 +7820,98 @@ var Selection = class {
7611
7820
  const group = this.computeGroupForTag(props, tagId);
7612
7821
  return { kind: "single", group };
7613
7822
  }
7823
+ /**
7824
+ * Build a fieldId -> triggerKeys[] map from the current selection set.
7825
+ *
7826
+ * What counts as a "button selection" (trigger key):
7827
+ * - field key where the field has button === true (e.g. "f:dripfeed")
7828
+ * - option key (e.g. "o:fast")
7829
+ * - composite key "fieldId::optionId" (e.g. "f:speed::o:fast")
7830
+ *
7831
+ * Grouping:
7832
+ * - button-field trigger groups under its own fieldId
7833
+ * - option/composite groups under the option's owning fieldId (from nodeMap)
7834
+ *
7835
+ * Deterministic:
7836
+ * - preserves selection insertion order
7837
+ * - de-dupes per field
7838
+ */
7839
+ buttonSelectionsByFieldId() {
7840
+ var _a, _b, _c, _d, _e;
7841
+ const nodeMap = this.builder.getNodeMap();
7842
+ const out = {};
7843
+ const push = (fieldId, triggerKey) => {
7844
+ var _a2;
7845
+ const arr = (_a2 = out[fieldId]) != null ? _a2 : out[fieldId] = [];
7846
+ if (!arr.includes(triggerKey)) arr.push(triggerKey);
7847
+ };
7848
+ for (const key of this.set) {
7849
+ if (!key) continue;
7850
+ const idx = key.indexOf("::");
7851
+ if (idx !== -1) {
7852
+ const optionId = key.slice(idx + 2);
7853
+ const optRef = nodeMap.get(optionId);
7854
+ if ((optRef == null ? void 0 : optRef.kind) === "option" && typeof optRef.fieldId === "string") {
7855
+ push(optRef.fieldId, key);
7856
+ }
7857
+ continue;
7858
+ }
7859
+ const ref = nodeMap.get(key);
7860
+ if (!ref) continue;
7861
+ if (ref.kind === "option" && typeof ref.fieldId === "string") {
7862
+ push(ref.fieldId, (_a = ref.id) != null ? _a : key);
7863
+ continue;
7864
+ }
7865
+ if (ref.kind === "field") {
7866
+ const field = (_c = (_b = ref.node) != null ? _b : ref.field) != null ? _c : ref;
7867
+ const fieldId = (_e = (_d = ref.id) != null ? _d : field == null ? void 0 : field.id) != null ? _e : key;
7868
+ if ((field == null ? void 0 : field.button) === true && typeof fieldId === "string") {
7869
+ push(fieldId, fieldId);
7870
+ }
7871
+ }
7872
+ }
7873
+ return out;
7874
+ }
7875
+ /**
7876
+ * Returns only selection keys that are valid "trigger buttons":
7877
+ * - field keys where field.button === true
7878
+ * - option keys
7879
+ * - composite keys "fieldId::optionId" (validated by optionId)
7880
+ * Excludes tags and non-button fields.
7881
+ */
7882
+ selectedButtons() {
7883
+ var _a, _b;
7884
+ const nodeMap = this.builder.getNodeMap();
7885
+ const out = [];
7886
+ const seen = /* @__PURE__ */ new Set();
7887
+ const push = (k) => {
7888
+ if (!seen.has(k)) {
7889
+ seen.add(k);
7890
+ out.push(k);
7891
+ }
7892
+ };
7893
+ for (const key of this.set) {
7894
+ if (!key) continue;
7895
+ const idx = key.indexOf("::");
7896
+ if (idx !== -1) {
7897
+ const optionId = key.slice(idx + 2);
7898
+ const optRef = nodeMap.get(optionId);
7899
+ if ((optRef == null ? void 0 : optRef.kind) === "option") push(key);
7900
+ continue;
7901
+ }
7902
+ const ref = nodeMap.get(key);
7903
+ if (!ref) continue;
7904
+ if (ref.kind === "option") {
7905
+ push(key);
7906
+ continue;
7907
+ }
7908
+ if (ref.kind === "field") {
7909
+ const field = (_b = (_a = ref.node) != null ? _a : ref.field) != null ? _b : ref;
7910
+ if ((field == null ? void 0 : field.button) === true) push(key);
7911
+ }
7912
+ }
7913
+ return out;
7914
+ }
7614
7915
  // ── Internals ────────────────────────────────────────────────────────────
7615
7916
  emit() {
7616
7917
  const payload = {
@@ -7633,7 +7934,7 @@ var Selection = class {
7633
7934
  this.currentTagId = Array.isArray(f.bind_id) ? f.bind_id[0] : f.bind_id;
7634
7935
  return;
7635
7936
  }
7636
- if (isOptionId2(id)) {
7937
+ if (this.builder.isOptionId(id)) {
7637
7938
  const host = fields.find(
7638
7939
  (x) => {
7639
7940
  var _a2;
@@ -7657,7 +7958,7 @@ var Selection = class {
7657
7958
  resolveTagContextId(props) {
7658
7959
  var _a;
7659
7960
  if (this.currentTagId) return this.currentTagId;
7660
- for (const id of this.set) if (isTagId2(id)) return id;
7961
+ for (const id of this.set) if (this.builder.isTagId(id)) return id;
7661
7962
  const fields = (_a = props.fields) != null ? _a : [];
7662
7963
  for (const id of this.set) {
7663
7964
  const f = fields.find((x) => x.id === id);
@@ -7665,7 +7966,7 @@ var Selection = class {
7665
7966
  return Array.isArray(f.bind_id) ? f.bind_id[0] : f.bind_id;
7666
7967
  }
7667
7968
  for (const id of this.set) {
7668
- if (isOptionId2(id)) {
7969
+ if (this.builder.isOptionId(id)) {
7669
7970
  const host = fields.find(
7670
7971
  (x) => {
7671
7972
  var _a2;
@@ -7684,28 +7985,13 @@ var Selection = class {
7684
7985
  }
7685
7986
  return this.opts.rootTagId;
7686
7987
  }
7687
- selectedButtonTriggerIds(props) {
7688
- var _a;
7689
- const fields = (_a = props.fields) != null ? _a : [];
7690
- const fieldById = new Map(fields.map((f) => [f.id, f]));
7691
- const out = [];
7692
- for (const selId of this.set) {
7693
- if (selId.startsWith("o:")) {
7694
- out.push(selId);
7695
- continue;
7696
- }
7697
- const f = fieldById.get(selId);
7698
- if ((f == null ? void 0 : f.button) === true) out.push(selId);
7699
- }
7700
- return out;
7701
- }
7702
7988
  computeGroupForTag(props, tagId) {
7703
7989
  var _a, _b, _c, _d, _e, _f, _g;
7704
7990
  const tags = (_a = props.filters) != null ? _a : [];
7705
7991
  const fields = (_b = props.fields) != null ? _b : [];
7706
7992
  const tagById = new Map(tags.map((t) => [t.id, t]));
7707
7993
  const tag = tagById.get(tagId);
7708
- const selectedTriggerIds = this.selectedButtonTriggerIds(props);
7994
+ const selectedTriggerIds = this.selectedButtons();
7709
7995
  const fieldIds = this.builder.visibleFields(tagId, selectedTriggerIds);
7710
7996
  const fieldById = new Map(fields.map((f) => [f.id, f]));
7711
7997
  const visible = fieldIds.map((id) => fieldById.get(id)).filter(Boolean);
@@ -7786,7 +8072,7 @@ var Selection = class {
7786
8072
  }
7787
8073
  findOptionById(fields, selId) {
7788
8074
  var _a, _b;
7789
- if (isOptionId2(selId)) {
8075
+ if (this.builder.isOptionId(selId)) {
7790
8076
  for (const f of fields) {
7791
8077
  const o = (_a = f.options) == null ? void 0 : _a.find((x) => x.id === selId);
7792
8078
  if (o) return o;
@@ -8208,8 +8494,8 @@ function useCanvasOwned(initialProps, canvasOpts, builderOpts) {
8208
8494
  // src/react/workspace/context/hooks/use-canvas.ts
8209
8495
  import * as React16 from "react";
8210
8496
  import { useMemo as useMemo15 } from "react";
8211
- var isTagId3 = (id) => id.startsWith("t:");
8212
- var isOptionId3 = (id) => id.startsWith("o:");
8497
+ var isTagId = (id) => id.startsWith("t:");
8498
+ var isOptionId = (id) => id.startsWith("o:");
8213
8499
  function deriveSelectionInfo(props, ids) {
8214
8500
  var _a, _b, _c, _d;
8215
8501
  const tags = [];
@@ -8218,11 +8504,11 @@ function deriveSelectionInfo(props, ids) {
8218
8504
  const fieldById = /* @__PURE__ */ new Map();
8219
8505
  for (const f of (_a = props.fields) != null ? _a : []) fieldById.set(f.id, f);
8220
8506
  for (const id of ids) {
8221
- if (isTagId3(id)) {
8507
+ if (isTagId(id)) {
8222
8508
  tags.push(id);
8223
8509
  continue;
8224
8510
  }
8225
- if (isOptionId3(id)) {
8511
+ if (isOptionId(id)) {
8226
8512
  options.push(id);
8227
8513
  continue;
8228
8514
  }