@timeax/digital-service-engine 0.2.1 → 0.2.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.
@@ -1845,6 +1845,9 @@ function toFiniteNumber(v) {
1845
1845
  const n = Number(v);
1846
1846
  return Number.isFinite(n) ? n : NaN;
1847
1847
  }
1848
+ function isValidServiceIdRef(value) {
1849
+ return typeof value === "number" && Number.isFinite(value) || typeof value === "string" && value.trim().length > 0;
1850
+ }
1848
1851
  function constraintFitOk(svcMap, candidate, constraints) {
1849
1852
  const cap = getServiceCapability(svcMap, candidate);
1850
1853
  if (!cap) return false;
@@ -1853,18 +1856,32 @@ function constraintFitOk(svcMap, candidate, constraints) {
1853
1856
  return !(constraints.cancel === true && !cap.cancel);
1854
1857
  }
1855
1858
  function getServiceCapability(svcMap, candidate) {
1856
- if (candidate === void 0 || candidate === null) return void 0;
1857
- const direct = svcMap[candidate];
1858
- if (direct) return direct;
1859
- const byString = svcMap[String(candidate)];
1860
- if (byString) return byString;
1861
- if (typeof candidate === "string") {
1862
- const maybeNumber = Number(candidate);
1863
- if (Number.isFinite(maybeNumber)) {
1864
- return svcMap[maybeNumber];
1865
- }
1859
+ var _a;
1860
+ return (_a = getServiceCapabilityEntry(svcMap, candidate)) == null ? void 0 : _a.capability;
1861
+ }
1862
+ function getServiceCapabilityCanonicalRef(svcMap, candidate) {
1863
+ const entry = getServiceCapabilityEntry(svcMap, candidate);
1864
+ if (!entry) return void 0;
1865
+ return getCanonicalServiceRef(entry.key, entry.capability);
1866
+ }
1867
+ function getServiceCapabilityAliases(svcMap, candidate) {
1868
+ const entry = getServiceCapabilityEntry(svcMap, candidate);
1869
+ if (!entry) return [];
1870
+ return collectServiceRefAliases(entry.key, entry.capability);
1871
+ }
1872
+ function isSameServiceCapabilityRef(svcMap, left, right) {
1873
+ if (!isValidServiceIdRef(left) || !isValidServiceIdRef(right)) return false;
1874
+ const leftAliases = new Set(
1875
+ getServiceCapabilityAliases(svcMap, left).map((value) => String(value))
1876
+ );
1877
+ if (!leftAliases.size) {
1878
+ leftAliases.add(String(left));
1866
1879
  }
1867
- return void 0;
1880
+ const rightAliases = getServiceCapabilityAliases(svcMap, right);
1881
+ if (!rightAliases.length) {
1882
+ return leftAliases.has(String(right));
1883
+ }
1884
+ return rightAliases.some((value) => leftAliases.has(String(value)));
1868
1885
  }
1869
1886
  function normalizeRatePolicy(policy) {
1870
1887
  var _a;
@@ -1900,6 +1917,74 @@ function rateOk(svcMap, candidate, primary, policy) {
1900
1917
  if (!Number.isFinite(cRate) || !Number.isFinite(pRate)) return false;
1901
1918
  return passesRatePolicy(policy.ratePolicy, pRate, cRate);
1902
1919
  }
1920
+ function getServiceCapabilityEntry(svcMap, candidate) {
1921
+ if (candidate === void 0 || candidate === null) return void 0;
1922
+ const direct = svcMap[candidate];
1923
+ if (direct) {
1924
+ return { key: String(candidate), capability: direct };
1925
+ }
1926
+ const byString = svcMap[String(candidate)];
1927
+ if (byString) {
1928
+ return { key: String(candidate), capability: byString };
1929
+ }
1930
+ if (typeof candidate === "string") {
1931
+ const maybeNumber = Number(candidate);
1932
+ if (Number.isFinite(maybeNumber)) {
1933
+ const byNumber = svcMap[maybeNumber];
1934
+ if (byNumber) {
1935
+ return { key: String(maybeNumber), capability: byNumber };
1936
+ }
1937
+ }
1938
+ }
1939
+ const target = String(candidate);
1940
+ for (const [key, capability] of Object.entries(svcMap != null ? svcMap : {})) {
1941
+ if (collectServiceRefAliases(key, capability).some(
1942
+ (alias) => String(alias) === target
1943
+ )) {
1944
+ return { key, capability };
1945
+ }
1946
+ }
1947
+ return void 0;
1948
+ }
1949
+ function collectServiceRefAliases(key, capability) {
1950
+ const out = [];
1951
+ const seen = /* @__PURE__ */ new Set();
1952
+ const push = (value) => {
1953
+ if (!isValidServiceIdRef(value)) return;
1954
+ const normalized = normalizeServiceRef(value);
1955
+ if (!normalized) return;
1956
+ const aliasKey = String(normalized);
1957
+ if (seen.has(aliasKey)) return;
1958
+ seen.add(aliasKey);
1959
+ out.push(normalized);
1960
+ };
1961
+ push(getCanonicalServiceRef(key, capability));
1962
+ push(capability.service);
1963
+ push(capability.key);
1964
+ push(capability.id);
1965
+ return out;
1966
+ }
1967
+ function getCanonicalServiceRef(key, capability) {
1968
+ const explicitRefs = [capability.service, capability.key, capability.id];
1969
+ for (const ref of explicitRefs) {
1970
+ if (!isValidServiceIdRef(ref)) continue;
1971
+ if (String(ref) === key) {
1972
+ return ref;
1973
+ }
1974
+ }
1975
+ return normalizeServiceRef(key);
1976
+ }
1977
+ function normalizeServiceRef(value) {
1978
+ if (!isValidServiceIdRef(value)) return void 0;
1979
+ if (typeof value === "number") return value;
1980
+ const trimmed = value.trim();
1981
+ if (!trimmed) return void 0;
1982
+ const asNumber = Number(trimmed);
1983
+ if (Number.isFinite(asNumber) && String(asNumber) === trimmed) {
1984
+ return asNumber;
1985
+ }
1986
+ return trimmed;
1987
+ }
1903
1988
 
1904
1989
  // src/core/validate/steps/rates.ts
1905
1990
  function validateRates(v) {
@@ -3345,7 +3430,7 @@ function collectFailedFallbacks(props, services, settings) {
3345
3430
  const s = { ...DEFAULT_SETTINGS, ...settings != null ? settings : {} };
3346
3431
  const out = [];
3347
3432
  const fb = (_a = props.fallbacks) != null ? _a : {};
3348
- const primaryRate = (p) => rateOf(services, p);
3433
+ const primaryRate = (primary) => rateOf(services, primary);
3349
3434
  for (const [nodeId, list] of Object.entries((_b = fb.nodes) != null ? _b : {})) {
3350
3435
  const { primary, tagContexts } = primaryForNode(props, nodeId);
3351
3436
  if (!primary) {
@@ -3358,34 +3443,34 @@ function collectFailedFallbacks(props, services, settings) {
3358
3443
  });
3359
3444
  continue;
3360
3445
  }
3361
- for (const cand of list) {
3362
- const cap = getCap(services, cand);
3363
- if (!cap) {
3446
+ for (const candidate of list) {
3447
+ const capability = getCap(services, candidate);
3448
+ if (!capability) {
3364
3449
  out.push({
3365
3450
  scope: "node",
3366
3451
  nodeId,
3367
3452
  primary,
3368
- candidate: cand,
3453
+ candidate,
3369
3454
  reason: "unknown_service"
3370
3455
  });
3371
3456
  continue;
3372
3457
  }
3373
- if (String(cand) === String(primary)) {
3458
+ if (isSameServiceCapabilityRef(services, candidate, primary)) {
3374
3459
  out.push({
3375
3460
  scope: "node",
3376
3461
  nodeId,
3377
3462
  primary,
3378
- candidate: cand,
3463
+ candidate,
3379
3464
  reason: "cycle"
3380
3465
  });
3381
3466
  continue;
3382
3467
  }
3383
- if (!passesRate(s.ratePolicy, primaryRate(primary), cap.rate)) {
3468
+ if (!passesRate(s.ratePolicy, primaryRate(primary), capability.rate)) {
3384
3469
  out.push({
3385
3470
  scope: "node",
3386
3471
  nodeId,
3387
3472
  primary,
3388
- candidate: cand,
3473
+ candidate,
3389
3474
  reason: "rate_violation"
3390
3475
  });
3391
3476
  continue;
@@ -3395,58 +3480,55 @@ function collectFailedFallbacks(props, services, settings) {
3395
3480
  scope: "node",
3396
3481
  nodeId,
3397
3482
  primary,
3398
- candidate: cand,
3483
+ candidate,
3399
3484
  reason: "no_tag_context"
3400
3485
  });
3401
3486
  continue;
3402
3487
  }
3403
- let anyPass = false;
3404
- let anyFail = false;
3405
3488
  for (const tagId of tagContexts) {
3406
- const ok = s.requireConstraintFit ? satisfiesTagConstraints(tagId, { services, props }, cap) : true;
3407
- if (ok) anyPass = true;
3408
- else {
3409
- anyFail = true;
3410
- out.push({
3411
- scope: "node",
3412
- nodeId,
3413
- primary,
3414
- candidate: cand,
3415
- tagContext: tagId,
3416
- reason: "constraint_mismatch"
3417
- });
3418
- }
3489
+ const fitsConstraints = s.requireConstraintFit ? satisfiesTagConstraints(
3490
+ tagId,
3491
+ { services, props },
3492
+ capability
3493
+ ) : true;
3494
+ if (fitsConstraints) continue;
3495
+ out.push({
3496
+ scope: "node",
3497
+ nodeId,
3498
+ primary,
3499
+ candidate,
3500
+ tagContext: tagId,
3501
+ reason: "constraint_mismatch"
3502
+ });
3419
3503
  }
3420
- void anyPass;
3421
- void anyFail;
3422
3504
  }
3423
3505
  }
3424
3506
  for (const [primary, list] of Object.entries((_c = fb.global) != null ? _c : {})) {
3425
- for (const cand of list) {
3426
- const cap = getCap(services, cand);
3427
- if (!cap) {
3507
+ for (const candidate of list) {
3508
+ const capability = getCap(services, candidate);
3509
+ if (!capability) {
3428
3510
  out.push({
3429
3511
  scope: "global",
3430
3512
  primary,
3431
- candidate: cand,
3513
+ candidate,
3432
3514
  reason: "unknown_service"
3433
3515
  });
3434
3516
  continue;
3435
3517
  }
3436
- if (String(cand) === String(primary)) {
3518
+ if (isSameServiceCapabilityRef(services, candidate, primary)) {
3437
3519
  out.push({
3438
3520
  scope: "global",
3439
3521
  primary,
3440
- candidate: cand,
3522
+ candidate,
3441
3523
  reason: "cycle"
3442
3524
  });
3443
3525
  continue;
3444
3526
  }
3445
- if (!passesRate(s.ratePolicy, primaryRate(primary), cap.rate)) {
3527
+ if (!passesRate(s.ratePolicy, primaryRate(primary), capability.rate)) {
3446
3528
  out.push({
3447
3529
  scope: "global",
3448
3530
  primary,
3449
- candidate: cand,
3531
+ candidate,
3450
3532
  reason: "rate_violation"
3451
3533
  });
3452
3534
  }
@@ -3455,52 +3537,61 @@ function collectFailedFallbacks(props, services, settings) {
3455
3537
  return out;
3456
3538
  }
3457
3539
  function rateOf(map, id) {
3458
- var _a;
3540
+ var _a, _b;
3459
3541
  if (id === void 0 || id === null) return void 0;
3460
- const c = getCap(map, id);
3461
- return (_a = c == null ? void 0 : c.rate) != null ? _a : void 0;
3542
+ return (_b = (_a = getCap(map, id)) == null ? void 0 : _a.rate) != null ? _b : void 0;
3462
3543
  }
3463
- function passesRate(policy, primaryRate, candRate) {
3464
- if (typeof candRate !== "number" || !Number.isFinite(candRate))
3544
+ function passesRate(policy, primaryRate, candidateRate) {
3545
+ if (typeof candidateRate !== "number" || !Number.isFinite(candidateRate)) {
3465
3546
  return false;
3466
- if (typeof primaryRate !== "number" || !Number.isFinite(primaryRate))
3547
+ }
3548
+ if (typeof primaryRate !== "number" || !Number.isFinite(primaryRate)) {
3467
3549
  return false;
3468
- return passesRatePolicy(normalizeRatePolicy(policy), primaryRate, candRate);
3550
+ }
3551
+ return passesRatePolicy(
3552
+ normalizeRatePolicy(policy),
3553
+ primaryRate,
3554
+ candidateRate
3555
+ );
3469
3556
  }
3470
3557
  function getCap(map, id) {
3471
3558
  return getServiceCapability(map, id);
3472
3559
  }
3473
- function isCapFlagEnabled(cap, flagId) {
3560
+ function isCapFlagEnabled(capability, flagId) {
3474
3561
  var _a, _b;
3475
- const fromFlags = (_b = (_a = cap.flags) == null ? void 0 : _a[flagId]) == null ? void 0 : _b.enabled;
3562
+ const fromFlags = (_b = (_a = capability.flags) == null ? void 0 : _a[flagId]) == null ? void 0 : _b.enabled;
3476
3563
  if (fromFlags === true) return true;
3477
3564
  if (fromFlags === false) return false;
3478
- const legacy = cap[flagId];
3565
+ const legacy = capability[flagId];
3479
3566
  return legacy === true;
3480
3567
  }
3481
- function satisfiesTagConstraints(tagId, ctx, cap) {
3482
- const tag = ctx.props.filters.find((t) => t.id === tagId);
3483
- const eff = tag == null ? void 0 : tag.constraints;
3484
- if (!eff) return true;
3485
- for (const [key, value] of Object.entries(eff)) {
3486
- if (value === true && !isCapFlagEnabled(cap, key)) {
3568
+ function satisfiesTagConstraints(tagId, ctx, capability) {
3569
+ const tag = ctx.props.filters.find((item) => item.id === tagId);
3570
+ const effectiveConstraints2 = tag == null ? void 0 : tag.constraints;
3571
+ if (!effectiveConstraints2) return true;
3572
+ for (const [key, value] of Object.entries(effectiveConstraints2)) {
3573
+ if (value === true && !isCapFlagEnabled(capability, key)) {
3487
3574
  return false;
3488
3575
  }
3489
3576
  }
3490
3577
  return true;
3491
3578
  }
3492
3579
  function primaryForNode(props, nodeId) {
3493
- const tag = props.filters.find((t) => t.id === nodeId);
3580
+ const tag = props.filters.find((item) => item.id === nodeId);
3494
3581
  if (tag) {
3495
3582
  return { primary: tag.service_id, tagContexts: [tag.id] };
3496
3583
  }
3497
3584
  const field = props.fields.find(
3498
- (f) => Array.isArray(f.options) && f.options.some((o) => o.id === nodeId)
3585
+ (item) => Array.isArray(item.options) && item.options.some((option2) => option2.id === nodeId)
3499
3586
  );
3500
- if (!field) return { tagContexts: [], reasonNoPrimary: "no_parent_field" };
3501
- const opt = field.options.find((o) => o.id === nodeId);
3502
- const contexts = bindIdsToArray(field.bind_id);
3503
- return { primary: opt.service_id, tagContexts: contexts };
3587
+ if (!field) {
3588
+ return { tagContexts: [], reasonNoPrimary: "no_parent_field" };
3589
+ }
3590
+ const option = field.options.find((item) => item.id === nodeId);
3591
+ return {
3592
+ primary: option.service_id,
3593
+ tagContexts: bindIdsToArray(field.bind_id)
3594
+ };
3504
3595
  }
3505
3596
  function bindIdsToArray(bind) {
3506
3597
  if (!bind) return [];
@@ -3510,43 +3601,54 @@ function getEligibleFallbacks(params) {
3510
3601
  var _a, _b, _c, _d, _e, _f;
3511
3602
  const s = { ...DEFAULT_SETTINGS, ...(_a = params.settings) != null ? _a : {} };
3512
3603
  const { primary, nodeId, tagId, services } = params;
3513
- const fb = (_b = params.fallbacks) != null ? _b : {};
3514
- const excludes = new Set(((_c = params.exclude) != null ? _c : []).map(String));
3515
- excludes.add(String(primary));
3516
- const unique = (_d = params.unique) != null ? _d : true;
3517
- const lists = [];
3518
- if (nodeId && ((_e = fb.nodes) == null ? void 0 : _e[nodeId])) lists.push(fb.nodes[nodeId]);
3519
- if ((_f = fb.global) == null ? void 0 : _f[primary]) lists.push(fb.global[primary]);
3520
- if (!lists.length) return [];
3604
+ const excludes = /* @__PURE__ */ new Set();
3605
+ for (const ref of (_b = params.exclude) != null ? _b : []) {
3606
+ addComparableServiceRef(excludes, services, ref);
3607
+ }
3608
+ addComparableServiceRef(excludes, services, primary);
3609
+ const source = (_c = params.source) != null ? _c : "registered";
3610
+ const candidateLists = source === "all_services" ? [listServicePoolCandidates(services)] : listRegisteredFallbackCandidates(
3611
+ (_d = params.fallbacks) != null ? _d : {},
3612
+ primary,
3613
+ nodeId,
3614
+ services
3615
+ );
3616
+ if (!candidateLists.length) return [];
3521
3617
  const primaryRate = rateOf(services, primary);
3522
3618
  const seen = /* @__PURE__ */ new Set();
3523
3619
  const eligible = [];
3524
- for (const list of lists) {
3525
- for (const cand of list) {
3526
- const key = String(cand);
3527
- if (excludes.has(key)) continue;
3528
- if (unique && seen.has(key)) continue;
3529
- seen.add(key);
3530
- const cap = getCap(services, cand);
3531
- if (!cap) continue;
3532
- if (!passesRate(s.ratePolicy, primaryRate, cap.rate)) continue;
3620
+ for (const list of candidateLists) {
3621
+ for (const candidate of list) {
3622
+ if (hasComparableServiceRef(excludes, services, candidate)) continue;
3623
+ const capability = getCap(services, candidate);
3624
+ if (!capability) continue;
3625
+ const candidateId = (_e = getServiceCapabilityCanonicalRef(services, candidate)) != null ? _e : candidate;
3626
+ const candidateIdentity = getComparableServiceRefKey(
3627
+ services,
3628
+ candidateId
3629
+ );
3630
+ if (((_f = params.unique) != null ? _f : true) && seen.has(candidateIdentity)) continue;
3631
+ seen.add(candidateIdentity);
3632
+ if (!passesRate(s.ratePolicy, primaryRate, capability.rate)) {
3633
+ continue;
3634
+ }
3533
3635
  if (s.requireConstraintFit && tagId) {
3534
- const ok = satisfiesTagConstraints(
3636
+ const fitsConstraints = satisfiesTagConstraints(
3535
3637
  tagId,
3536
3638
  { props: params.props, services },
3537
- cap
3639
+ capability
3538
3640
  );
3539
- if (!ok) continue;
3641
+ if (!fitsConstraints) continue;
3540
3642
  }
3541
- eligible.push(cand);
3643
+ eligible.push(candidateId);
3542
3644
  }
3543
3645
  }
3544
3646
  if (s.selectionStrategy === "cheapest") {
3545
- eligible.sort((a, b) => {
3647
+ eligible.sort((left, right) => {
3546
3648
  var _a2, _b2;
3547
- const ra = (_a2 = rateOf(services, a)) != null ? _a2 : Infinity;
3548
- const rb = (_b2 = rateOf(services, b)) != null ? _b2 : Infinity;
3549
- return ra - rb;
3649
+ const leftRate = (_a2 = rateOf(services, left)) != null ? _a2 : Infinity;
3650
+ const rightRate = (_b2 = rateOf(services, right)) != null ? _b2 : Infinity;
3651
+ return leftRate - rightRate;
3550
3652
  });
3551
3653
  }
3552
3654
  if (typeof params.limit === "number" && params.limit >= 0) {
@@ -3554,10 +3656,104 @@ function getEligibleFallbacks(params) {
3554
3656
  }
3555
3657
  return eligible;
3556
3658
  }
3659
+ function getAssignedServiceIds(params) {
3660
+ var _a, _b, _c, _d, _e;
3661
+ const seen = /* @__PURE__ */ new Set();
3662
+ const out = [];
3663
+ const push = (value) => {
3664
+ if (!isValidServiceIdRef(value)) return;
3665
+ const key = String(value);
3666
+ if (seen.has(key)) return;
3667
+ seen.add(key);
3668
+ out.push(value);
3669
+ };
3670
+ const props = params.props;
3671
+ if (props) {
3672
+ for (const tag of (_a = props.filters) != null ? _a : []) {
3673
+ push(tag.service_id);
3674
+ }
3675
+ for (const field of (_b = props.fields) != null ? _b : []) {
3676
+ const fieldService = field.service_id;
3677
+ if (field.button === true) {
3678
+ push(fieldService);
3679
+ }
3680
+ for (const option of (_c = field.options) != null ? _c : []) {
3681
+ if (option.pricing_role === "utility") continue;
3682
+ push(option.service_id);
3683
+ }
3684
+ }
3685
+ }
3686
+ const snapshot = params.snapshot;
3687
+ if (snapshot) {
3688
+ for (const serviceId of (_d = snapshot.services) != null ? _d : []) {
3689
+ push(serviceId);
3690
+ }
3691
+ for (const list of Object.values((_e = snapshot.serviceMap) != null ? _e : {})) {
3692
+ for (const serviceId of list != null ? list : []) {
3693
+ push(serviceId);
3694
+ }
3695
+ }
3696
+ }
3697
+ return out;
3698
+ }
3557
3699
  function getFallbackRegistrationInfo(props, nodeId) {
3558
3700
  const { primary, tagContexts } = primaryForNode(props, nodeId);
3559
3701
  return { primary, tagContexts };
3560
3702
  }
3703
+ function listRegisteredFallbackCandidates(fallbacks, primary, nodeId, services) {
3704
+ var _a, _b;
3705
+ const lists = [];
3706
+ if (nodeId && ((_a = fallbacks.nodes) == null ? void 0 : _a[nodeId])) {
3707
+ lists.push(fallbacks.nodes[nodeId]);
3708
+ }
3709
+ for (const [registeredPrimary, list] of Object.entries((_b = fallbacks.global) != null ? _b : {})) {
3710
+ if (!isMatchingServiceRef(services, registeredPrimary, primary)) continue;
3711
+ lists.push(list);
3712
+ }
3713
+ return lists;
3714
+ }
3715
+ function listServicePoolCandidates(services) {
3716
+ const seen = /* @__PURE__ */ new Set();
3717
+ const out = [];
3718
+ for (const [key, capability] of Object.entries(services != null ? services : {})) {
3719
+ const candidate = getServicePoolCandidateId(key, capability);
3720
+ if (!isValidServiceIdRef(candidate)) continue;
3721
+ const identity = getComparableServiceRefKey(services, candidate);
3722
+ if (seen.has(identity)) continue;
3723
+ seen.add(identity);
3724
+ out.push(candidate);
3725
+ }
3726
+ return out;
3727
+ }
3728
+ function getServicePoolCandidateId(key, capability) {
3729
+ var _a;
3730
+ return (_a = getServiceCapabilityCanonicalRef({ [key]: capability }, key)) != null ? _a : key;
3731
+ }
3732
+ function addComparableServiceRef(target, services, value) {
3733
+ for (const ref of getComparableServiceRefs(services, value)) {
3734
+ target.add(ref);
3735
+ }
3736
+ }
3737
+ function hasComparableServiceRef(target, services, value) {
3738
+ return getComparableServiceRefs(services, value).some((ref) => target.has(ref));
3739
+ }
3740
+ function getComparableServiceRefKey(services, value) {
3741
+ if (!isValidServiceIdRef(value)) return "";
3742
+ const canonical = getServiceCapabilityCanonicalRef(services, value);
3743
+ return String(canonical != null ? canonical : value);
3744
+ }
3745
+ function getComparableServiceRefs(services, value) {
3746
+ if (!isValidServiceIdRef(value)) return [];
3747
+ const aliases = getServiceCapabilityAliases(services, value);
3748
+ if (!aliases.length) {
3749
+ return [String(value)];
3750
+ }
3751
+ return aliases.map((ref) => String(ref));
3752
+ }
3753
+ function isMatchingServiceRef(services, left, right) {
3754
+ if (!services) return String(left) === String(right);
3755
+ return isSameServiceCapabilityRef(services, left, right);
3756
+ }
3561
3757
 
3562
3758
  // src/core/tag-relations.ts
3563
3759
  var toId = (x) => typeof x === "string" ? x : x.id;
@@ -5096,6 +5292,7 @@ function createFallbackEditor(options = {}) {
5096
5292
  const original = cloneFallbacks(options.fallbacks);
5097
5293
  let current = cloneFallbacks(options.fallbacks);
5098
5294
  const props = options.props;
5295
+ const snapshot = options.snapshot;
5099
5296
  const services = (_a = options.services) != null ? _a : {};
5100
5297
  const settings = (_b = options.settings) != null ? _b : {};
5101
5298
  function state() {
@@ -5190,7 +5387,11 @@ function createFallbackEditor(options = {}) {
5190
5387
  const allowed2 = [];
5191
5388
  for (const candidate of normalized) {
5192
5389
  const reasons = [];
5193
- if (String(candidate) === String(context.primary)) {
5390
+ if (isSameServiceCapabilityRef(
5391
+ services,
5392
+ candidate,
5393
+ context.primary
5394
+ )) {
5194
5395
  reasons.push("self_reference");
5195
5396
  }
5196
5397
  if (reasons.length) {
@@ -5278,7 +5479,19 @@ function createFallbackEditor(options = {}) {
5278
5479
  return writeScope(context, []);
5279
5480
  }
5280
5481
  function eligible(context, opt) {
5482
+ var _a2, _b2;
5281
5483
  if (!props) return [];
5484
+ const source = (_a2 = opt == null ? void 0 : opt.source) != null ? _a2 : "all_services";
5485
+ const exclude2 = normalizeCandidateList(
5486
+ [
5487
+ ...(_b2 = opt == null ? void 0 : opt.exclude) != null ? _b2 : [],
5488
+ ...source === "all_services" ? [
5489
+ ...getAssignedServiceIds({ props, snapshot }),
5490
+ ...getScope(context)
5491
+ ] : []
5492
+ ],
5493
+ true
5494
+ );
5282
5495
  if (context.scope === "global") {
5283
5496
  return getEligibleFallbacks({
5284
5497
  primary: context.primary,
@@ -5286,9 +5499,10 @@ function createFallbackEditor(options = {}) {
5286
5499
  fallbacks: current,
5287
5500
  settings,
5288
5501
  props,
5289
- exclude: opt == null ? void 0 : opt.exclude,
5502
+ exclude: exclude2,
5290
5503
  unique: opt == null ? void 0 : opt.unique,
5291
- limit: opt == null ? void 0 : opt.limit
5504
+ limit: opt == null ? void 0 : opt.limit,
5505
+ source
5292
5506
  });
5293
5507
  }
5294
5508
  const info = getFallbackRegistrationInfo(props, context.nodeId);
@@ -5296,14 +5510,19 @@ function createFallbackEditor(options = {}) {
5296
5510
  return getEligibleFallbacks({
5297
5511
  primary: info.primary,
5298
5512
  nodeId: context.nodeId,
5299
- tagId: info.tagContexts[0],
5513
+ tagId: resolveNodeTagContext({
5514
+ nodeId: context.nodeId,
5515
+ snapshot,
5516
+ fallbackTagContexts: info.tagContexts
5517
+ }),
5300
5518
  services,
5301
5519
  fallbacks: current,
5302
5520
  settings,
5303
5521
  props,
5304
- exclude: opt == null ? void 0 : opt.exclude,
5522
+ exclude: exclude2,
5305
5523
  unique: opt == null ? void 0 : opt.unique,
5306
- limit: opt == null ? void 0 : opt.limit
5524
+ limit: opt == null ? void 0 : opt.limit,
5525
+ source
5307
5526
  });
5308
5527
  }
5309
5528
  function writeScope(context, nextList) {
@@ -5361,14 +5580,14 @@ function sameFallbacks(a, b) {
5361
5580
  function normalizeCandidateList(input, preserveOrder) {
5362
5581
  const out = [];
5363
5582
  for (const item of input != null ? input : []) {
5364
- if (!isValidServiceIdRef(item)) continue;
5583
+ if (!isValidServiceIdRef2(item)) continue;
5365
5584
  const exists = out.some((x) => String(x) === String(item));
5366
5585
  if (exists) continue;
5367
5586
  out.push(item);
5368
5587
  }
5369
5588
  return preserveOrder ? out : out;
5370
5589
  }
5371
- function isValidServiceIdRef(value) {
5590
+ function isValidServiceIdRef2(value) {
5372
5591
  return typeof value === "number" && Number.isFinite(value) || typeof value === "string" && value.trim().length > 0;
5373
5592
  }
5374
5593
  function clamp(n, min, max) {
@@ -5377,7 +5596,7 @@ function clamp(n, min, max) {
5377
5596
  function getNodeRegistrationInfo(props, nodeId) {
5378
5597
  const tag = props.filters.find((t) => t.id === nodeId);
5379
5598
  if (tag) {
5380
- if (!isValidServiceIdRef(tag.service_id)) {
5599
+ if (!isValidServiceIdRef2(tag.service_id)) {
5381
5600
  return { ok: false, reasons: ["no_primary"] };
5382
5601
  }
5383
5602
  return {
@@ -5390,7 +5609,7 @@ function getNodeRegistrationInfo(props, nodeId) {
5390
5609
  if (!hit) {
5391
5610
  return { ok: false, reasons: ["node_not_found"] };
5392
5611
  }
5393
- if (!isValidServiceIdRef(hit.option.service_id)) {
5612
+ if (!isValidServiceIdRef2(hit.option.service_id)) {
5394
5613
  return { ok: false, reasons: ["no_primary"] };
5395
5614
  }
5396
5615
  return {
@@ -5412,6 +5631,15 @@ function bindIdsToArray2(v) {
5412
5631
  if (Array.isArray(v)) return v.filter(Boolean);
5413
5632
  return v ? [v] : [];
5414
5633
  }
5634
+ function resolveNodeTagContext(params) {
5635
+ var _a, _b, _c;
5636
+ const nodeContexts = (_c = (_b = (_a = params.snapshot) == null ? void 0 : _a.meta) == null ? void 0 : _b.context) == null ? void 0 : _c.nodeContexts;
5637
+ if (nodeContexts && Object.prototype.hasOwnProperty.call(nodeContexts, params.nodeId)) {
5638
+ const tagId = nodeContexts[params.nodeId];
5639
+ return typeof tagId === "string" && tagId.trim().length > 0 ? tagId : void 0;
5640
+ }
5641
+ return params.fallbackTagContexts[0];
5642
+ }
5415
5643
  function mapDiagReason(reason) {
5416
5644
  switch (String(reason)) {
5417
5645
  case "unknown_service":
@@ -12113,10 +12341,10 @@ function FallbackEditorInner({ className }) {
12113
12341
  "div",
12114
12342
  {
12115
12343
  className: [
12116
- "min-h-screen bg-zinc-100 p-4 text-zinc-900 dark:bg-zinc-950 dark:text-zinc-100",
12344
+ "h-full min-h-0 overflow-hidden bg-zinc-100 p-4 text-zinc-900 dark:bg-zinc-950 dark:text-zinc-100",
12117
12345
  className
12118
12346
  ].filter(Boolean).join(" "),
12119
- children: /* @__PURE__ */ jsxs2("div", { className: "mx-auto flex max-w-7xl flex-col gap-4", children: [
12347
+ children: /* @__PURE__ */ jsxs2("div", { className: "flex h-full min-h-0 flex-col gap-4", children: [
12120
12348
  /* @__PURE__ */ jsx6(
12121
12349
  FallbackEditorHeader,
12122
12350
  {
@@ -12128,9 +12356,9 @@ function FallbackEditorInner({ className }) {
12128
12356
  saving: headerSaving
12129
12357
  }
12130
12358
  ),
12131
- /* @__PURE__ */ jsxs2("div", { className: "grid gap-4 xl:grid-cols-[300px_minmax(0,1fr)_360px]", children: [
12359
+ /* @__PURE__ */ jsxs2("div", { className: "grid min-h-0 flex-1 gap-4 overflow-hidden xl:grid-cols-[300px_minmax(0,1fr)_360px]", children: [
12132
12360
  /* @__PURE__ */ jsx6(FallbackServiceSidebar, {}),
12133
- /* @__PURE__ */ jsxs2("div", { className: "flex min-h-0 flex-col gap-4", children: [
12361
+ /* @__PURE__ */ jsxs2("div", { className: "flex min-h-0 flex-col gap-4 overflow-y-auto", children: [
12134
12362
  /* @__PURE__ */ jsx6("section", { className: "rounded-2xl border border-zinc-200 bg-white p-4 shadow-sm dark:border-zinc-800 dark:bg-zinc-900", children: /* @__PURE__ */ jsxs2("div", { className: "flex flex-wrap items-start justify-between gap-4", children: [
12135
12363
  /* @__PURE__ */ jsxs2("div", { children: [
12136
12364
  /* @__PURE__ */ jsx6("h2", { className: "text-lg font-semibold text-zinc-900 dark:text-zinc-100", children: activeServiceId !== void 0 ? `Service #${String(activeServiceId)}` : "No service selected" }),
@@ -12184,6 +12412,7 @@ function TabButton({
12184
12412
 
12185
12413
  // src/react/fallback-editor/VirtualServiceList.tsx
12186
12414
  import React7 from "react";
12415
+ import { Check } from "lucide-react";
12187
12416
  import { jsx as jsx7, jsxs as jsxs3 } from "react/jsx-runtime";
12188
12417
  function VirtualServiceList({
12189
12418
  items,
@@ -12226,7 +12455,7 @@ function VirtualServiceList({
12226
12455
  {
12227
12456
  type: "button",
12228
12457
  onClick: () => onToggle(item.id),
12229
- className: "absolute left-0 right-0 flex items-center justify-between border-b border-zinc-100 bg-white px-3 text-left hover:bg-zinc-50 dark:border-zinc-800 dark:bg-zinc-900 dark:hover:bg-zinc-800",
12458
+ className: "absolute left-0 right-0 flex items-center justify-between border-b border-zinc-100 bg-white px-3 text-left transition hover:bg-zinc-50 dark:border-zinc-800 dark:bg-zinc-900 dark:hover:bg-zinc-800",
12230
12459
  style: {
12231
12460
  top: index * rowHeight,
12232
12461
  height: rowHeight
@@ -12246,12 +12475,13 @@ function VirtualServiceList({
12246
12475
  ] })
12247
12476
  ] }),
12248
12477
  /* @__PURE__ */ jsx7(
12249
- "input",
12478
+ "span",
12250
12479
  {
12251
- type: "checkbox",
12252
- checked,
12253
- readOnly: true,
12254
- className: "h-4 w-4 rounded border-zinc-300"
12480
+ className: [
12481
+ "inline-flex h-4 w-4 items-center justify-center rounded border transition",
12482
+ checked ? "border-blue-600 bg-blue-600 text-white dark:border-blue-500 dark:bg-blue-500" : "border-zinc-300 bg-white text-transparent dark:border-zinc-700 dark:bg-zinc-800"
12483
+ ].join(" "),
12484
+ children: /* @__PURE__ */ jsx7(Check, { className: "h-3 w-3" })
12255
12485
  }
12256
12486
  )
12257
12487
  ]
@@ -12274,7 +12504,7 @@ function FallbackDetailsPanel() {
12274
12504
  () => services.find((s) => String(s.id) === String(activeServiceId)),
12275
12505
  [services, activeServiceId]
12276
12506
  );
12277
- return /* @__PURE__ */ jsxs4("aside", { className: "flex min-h-0 flex-col gap-4", children: [
12507
+ return /* @__PURE__ */ jsxs4("aside", { className: "flex min-h-0 flex-col gap-4 overflow-y-auto", children: [
12278
12508
  /* @__PURE__ */ jsxs4("section", { className: "rounded-2xl border border-zinc-200 bg-white p-4 shadow-sm dark:border-zinc-800 dark:bg-zinc-900", children: [
12279
12509
  /* @__PURE__ */ jsx8("h3", { className: "text-sm font-semibold text-zinc-900 dark:text-zinc-100", children: "Primary service info" }),
12280
12510
  !service ? /* @__PURE__ */ jsx8("p", { className: "mt-3 text-sm text-zinc-500 dark:text-zinc-400", children: "No service selected." }) : /* @__PURE__ */ jsxs4("div", { className: "mt-3 space-y-2 text-sm", children: [
@@ -12401,6 +12631,7 @@ function FallbackEditorHeader({
12401
12631
 
12402
12632
  // src/react/fallback-editor/FallbackSettingsPanel.tsx
12403
12633
  import React9 from "react";
12634
+ import { InputField as InputField21 } from "@timeax/form-palette";
12404
12635
  import { jsx as jsx10, jsxs as jsxs6 } from "react/jsx-runtime";
12405
12636
  function FallbackSettingsPanel() {
12406
12637
  var _a, _b, _c;
@@ -12436,7 +12667,7 @@ function FallbackSettingsPanel() {
12436
12667
  kind: "lte_primary",
12437
12668
  pct: 5
12438
12669
  };
12439
- return /* @__PURE__ */ jsxs6("section", { className: "rounded-2xl border border-zinc-200 bg-white p-4 shadow-sm dark:border-zinc-800 dark:bg-zinc-900", children: [
12670
+ return /* @__PURE__ */ jsxs6("section", { className: "overflow-y-auto rounded-2xl border border-zinc-200 bg-white p-4 shadow-sm dark:border-zinc-800 dark:bg-zinc-900", children: [
12440
12671
  /* @__PURE__ */ jsxs6("div", { className: "mb-4 flex items-start justify-between gap-3", children: [
12441
12672
  /* @__PURE__ */ jsxs6("div", { children: [
12442
12673
  /* @__PURE__ */ jsx10("h3", { className: "text-sm font-semibold text-zinc-900 dark:text-zinc-100", children: "Fallback settings" }),
@@ -12462,30 +12693,17 @@ function FallbackSettingsPanel() {
12462
12693
  {
12463
12694
  title: "Require constraint fit",
12464
12695
  hint: "Reject or warn when a candidate does not match effective tag constraints.",
12465
- children: /* @__PURE__ */ jsxs6(
12466
- "button",
12696
+ children: /* @__PURE__ */ jsx10(
12697
+ InputField21,
12467
12698
  {
12468
- type: "button",
12469
- onClick: () => setDraft((prev) => ({
12699
+ variant: "toggle",
12700
+ value: Boolean(draft.requireConstraintFit),
12701
+ onChange: ({ value }) => setDraft((prev) => ({
12470
12702
  ...prev,
12471
- requireConstraintFit: !(prev == null ? void 0 : prev.requireConstraintFit)
12703
+ requireConstraintFit: Boolean(value)
12472
12704
  })),
12473
- className: [
12474
- "inline-flex items-center gap-2 rounded-full border px-3 py-2 text-sm",
12475
- draft.requireConstraintFit ? "border-green-300 bg-green-50 text-green-700 dark:border-green-900/50 dark:bg-green-950/30 dark:text-green-300" : "border-zinc-300 bg-white text-zinc-600 dark:border-zinc-700 dark:bg-zinc-900 dark:text-zinc-300"
12476
- ].join(" "),
12477
- children: [
12478
- /* @__PURE__ */ jsx10("span", { children: draft.requireConstraintFit ? "Enabled" : "Disabled" }),
12479
- /* @__PURE__ */ jsx10(
12480
- "span",
12481
- {
12482
- className: [
12483
- "h-2.5 w-2.5 rounded-full",
12484
- draft.requireConstraintFit ? "bg-green-500" : "bg-zinc-400"
12485
- ].join(" ")
12486
- }
12487
- )
12488
- ]
12705
+ onText: "Enabled",
12706
+ offText: "Disabled"
12489
12707
  }
12490
12708
  )
12491
12709
  }
@@ -12496,12 +12714,13 @@ function FallbackSettingsPanel() {
12496
12714
  title: "Rate policy",
12497
12715
  hint: "Controls how fallback service rates are compared against the primary service.",
12498
12716
  children: /* @__PURE__ */ jsxs6("div", { className: "flex flex-col gap-2 md:items-end", children: [
12499
- /* @__PURE__ */ jsxs6(
12500
- "select",
12717
+ /* @__PURE__ */ jsx10(
12718
+ InputField21,
12501
12719
  {
12720
+ variant: "select",
12502
12721
  value: ratePolicy.kind,
12503
- onChange: (e) => {
12504
- const kind = e.target.value;
12722
+ onChange: ({ value }) => {
12723
+ const kind = value;
12505
12724
  if (kind === "eq_primary") {
12506
12725
  setRatePolicy({ kind: "eq_primary" });
12507
12726
  return;
@@ -12526,30 +12745,39 @@ function FallbackSettingsPanel() {
12526
12745
  pct: currentPct
12527
12746
  });
12528
12747
  },
12529
- className: "w-56 rounded-xl border border-zinc-300 bg-white px-3 py-2 text-sm dark:border-zinc-700 dark:bg-zinc-900 dark:text-zinc-100",
12530
- children: [
12531
- /* @__PURE__ */ jsx10("option", { value: "eq_primary", children: "eq_primary" }),
12532
- /* @__PURE__ */ jsx10("option", { value: "lte_primary", children: "lte_primary" }),
12533
- /* @__PURE__ */ jsx10("option", { value: "within_pct", children: "within_pct" }),
12534
- /* @__PURE__ */ jsx10("option", { value: "at_least_pct_lower", children: "at_least_pct_lower" })
12535
- ]
12748
+ options: [
12749
+ { value: "eq_primary", label: "eq_primary" },
12750
+ {
12751
+ value: "lte_primary",
12752
+ label: "lte_primary"
12753
+ },
12754
+ { value: "within_pct", label: "within_pct" },
12755
+ {
12756
+ value: "at_least_pct_lower",
12757
+ label: "at_least_pct_lower"
12758
+ }
12759
+ ],
12760
+ clearable: false
12536
12761
  }
12537
12762
  ),
12538
12763
  ratePolicy.kind !== "eq_primary" && /* @__PURE__ */ jsxs6("div", { className: "flex items-center gap-2", children: [
12539
- /* @__PURE__ */ jsx10(
12540
- "input",
12764
+ /* @__PURE__ */ jsx10("div", { className: "w-32", children: /* @__PURE__ */ jsx10(
12765
+ InputField21,
12541
12766
  {
12542
- type: "number",
12543
- min: 0,
12544
- step: "0.01",
12767
+ variant: "number",
12545
12768
  value: ratePolicy.pct,
12546
- onChange: (e) => {
12547
- const pct = Number(e.target.value || 0);
12548
- setRatePolicy({ ...ratePolicy, pct });
12769
+ onChange: ({ value }) => {
12770
+ const pct = typeof value === "number" ? value : Number(value != null ? value : 0);
12771
+ setRatePolicy({
12772
+ ...ratePolicy,
12773
+ pct
12774
+ });
12549
12775
  },
12550
- className: "w-32 rounded-xl border border-zinc-300 bg-white px-3 py-2 text-sm dark:border-zinc-700 dark:bg-zinc-900 dark:text-zinc-100"
12776
+ min: 0,
12777
+ step: 0.01,
12778
+ fullWidth: true
12551
12779
  }
12552
- ),
12780
+ ) }),
12553
12781
  /* @__PURE__ */ jsx10("span", { className: "text-sm text-zinc-500 dark:text-zinc-400", children: "%" })
12554
12782
  ] })
12555
12783
  ] })
@@ -12560,19 +12788,20 @@ function FallbackSettingsPanel() {
12560
12788
  {
12561
12789
  title: "Selection strategy",
12562
12790
  hint: "How valid fallback candidates are ordered in previews.",
12563
- children: /* @__PURE__ */ jsxs6(
12564
- "select",
12791
+ children: /* @__PURE__ */ jsx10(
12792
+ InputField21,
12565
12793
  {
12794
+ variant: "select",
12566
12795
  value: (_b = draft.selectionStrategy) != null ? _b : "priority",
12567
- onChange: (e) => setDraft((prev) => ({
12796
+ onChange: ({ value }) => setDraft((prev) => ({
12568
12797
  ...prev,
12569
- selectionStrategy: e.target.value
12798
+ selectionStrategy: value
12570
12799
  })),
12571
- className: "w-48 rounded-xl border border-zinc-300 bg-white px-3 py-2 text-sm dark:border-zinc-700 dark:bg-zinc-900 dark:text-zinc-100",
12572
- children: [
12573
- /* @__PURE__ */ jsx10("option", { value: "priority", children: "priority" }),
12574
- /* @__PURE__ */ jsx10("option", { value: "cheapest", children: "cheapest" })
12575
- ]
12800
+ options: [
12801
+ { value: "priority", label: "priority" },
12802
+ { value: "cheapest", label: "cheapest" }
12803
+ ],
12804
+ clearable: false
12576
12805
  }
12577
12806
  )
12578
12807
  }
@@ -12582,19 +12811,20 @@ function FallbackSettingsPanel() {
12582
12811
  {
12583
12812
  title: "Mode",
12584
12813
  hint: "Use strict for enforced filtering, dev for advisory feedback.",
12585
- children: /* @__PURE__ */ jsxs6(
12586
- "select",
12814
+ children: /* @__PURE__ */ jsx10(
12815
+ InputField21,
12587
12816
  {
12817
+ variant: "select",
12588
12818
  value: (_c = draft.mode) != null ? _c : "strict",
12589
- onChange: (e) => setDraft((prev) => ({
12819
+ onChange: ({ value }) => setDraft((prev) => ({
12590
12820
  ...prev,
12591
- mode: e.target.value
12821
+ mode: value
12592
12822
  })),
12593
- className: "w-48 rounded-xl border border-zinc-300 bg-white px-3 py-2 text-sm dark:border-zinc-700 dark:bg-zinc-900 dark:text-zinc-100",
12594
- children: [
12595
- /* @__PURE__ */ jsx10("option", { value: "strict", children: "strict" }),
12596
- /* @__PURE__ */ jsx10("option", { value: "dev", children: "dev" })
12597
- ]
12823
+ options: [
12824
+ { value: "strict", label: "strict" },
12825
+ { value: "dev", label: "dev" }
12826
+ ],
12827
+ clearable: false
12598
12828
  }
12599
12829
  )
12600
12830
  }
@@ -12620,6 +12850,8 @@ function SettingRow({
12620
12850
 
12621
12851
  // src/react/fallback-editor/FallbackServiceSidebar.tsx
12622
12852
  import { useMemo as useMemo7, useState as useState4 } from "react";
12853
+ import { InputField as InputField22 } from "@timeax/form-palette";
12854
+ import { Search } from "lucide-react";
12623
12855
  import { jsx as jsx11, jsxs as jsxs7 } from "react/jsx-runtime";
12624
12856
  function FallbackServiceSidebar() {
12625
12857
  const { activeServiceId, setActiveServiceId, get } = useFallbackEditor();
@@ -12635,22 +12867,26 @@ function FallbackServiceSidebar() {
12635
12867
  }
12636
12868
  );
12637
12869
  }, [query, services]);
12638
- return /* @__PURE__ */ jsxs7("aside", { className: "flex min-h-0 flex-col rounded-2xl border border-zinc-200 bg-white shadow-sm dark:border-zinc-800 dark:bg-zinc-900", children: [
12870
+ return /* @__PURE__ */ jsxs7("aside", { className: "flex h-full min-h-0 flex-col overflow-hidden rounded-2xl border border-zinc-200 bg-white shadow-sm dark:border-zinc-800 dark:bg-zinc-900", children: [
12639
12871
  /* @__PURE__ */ jsxs7("div", { className: "border-b border-zinc-200 p-4 dark:border-zinc-800", children: [
12640
12872
  /* @__PURE__ */ jsx11("h2", { className: "text-sm font-semibold text-zinc-900 dark:text-zinc-100", children: "Primary services" }),
12641
12873
  /* @__PURE__ */ jsx11("p", { className: "mt-1 text-xs text-zinc-500 dark:text-zinc-400", children: "Services currently active in the builder/runtime context." })
12642
12874
  ] }),
12643
12875
  /* @__PURE__ */ jsxs7("div", { className: "flex min-h-0 flex-1 flex-col p-4", children: [
12644
- /* @__PURE__ */ jsx11(
12645
- "input",
12876
+ /* @__PURE__ */ jsx11("div", { className: "rounded-xl border border-zinc-200 bg-zinc-50/80 p-2 dark:border-zinc-800 dark:bg-zinc-950/80", children: /* @__PURE__ */ jsx11(
12877
+ InputField22,
12646
12878
  {
12879
+ variant: "text",
12647
12880
  value: query,
12648
- onChange: (e) => setQuery(e.target.value),
12881
+ onChange: ({ value }) => setQuery(String(value != null ? value : "")),
12649
12882
  placeholder: "Search primary service...",
12650
- className: "rounded-xl border border-zinc-300 bg-white px-3 py-2 text-sm outline-none focus:border-blue-500 dark:border-zinc-700 dark:bg-zinc-950 dark:text-zinc-100"
12883
+ leadingControl: /* @__PURE__ */ jsx11(Search, { className: "h-4 w-4 text-zinc-400" }),
12884
+ joinControls: true,
12885
+ extendBoxToControls: true,
12886
+ fullWidth: true
12651
12887
  }
12652
- ),
12653
- /* @__PURE__ */ jsx11("div", { className: "mt-3 flex-1 space-y-2 overflow-auto", children: filtered.map((service) => {
12888
+ ) }),
12889
+ /* @__PURE__ */ jsx11("div", { className: "mt-3 flex-1 space-y-2 overflow-y-auto", children: filtered.map((service) => {
12654
12890
  var _a, _b;
12655
12891
  const active = String(service.id) === String(activeServiceId);
12656
12892
  const count = get(service.id).length;
@@ -12661,7 +12897,7 @@ function FallbackServiceSidebar() {
12661
12897
  onClick: () => setActiveServiceId(service.id),
12662
12898
  className: [
12663
12899
  "w-full rounded-2xl border p-3 text-left transition",
12664
- active ? "border-blue-500 bg-blue-50 dark:bg-blue-950/30" : "border-zinc-200 bg-zinc-50 hover:border-zinc-300 dark:border-zinc-800 dark:bg-zinc-950 dark:hover:border-zinc-700"
12900
+ active ? "border-blue-500 bg-blue-50 dark:border-blue-500/70 dark:bg-blue-950/30" : "border-zinc-200 bg-zinc-50 hover:border-zinc-300 dark:border-zinc-800 dark:bg-zinc-950 dark:hover:border-zinc-700 dark:hover:bg-zinc-900"
12665
12901
  ].join(" "),
12666
12902
  children: /* @__PURE__ */ jsxs7("div", { className: "flex items-start justify-between gap-3", children: [
12667
12903
  /* @__PURE__ */ jsxs7("div", { className: "min-w-0", children: [
@@ -12691,11 +12927,174 @@ function FallbackServiceSidebar() {
12691
12927
  }
12692
12928
 
12693
12929
  // src/react/fallback-editor/FallbackRegistrationsPanel.tsx
12694
- import React13 from "react";
12930
+ import React11 from "react";
12931
+ import { Fragment, jsx as jsx12, jsxs as jsxs8 } from "react/jsx-runtime";
12932
+ function FallbackRegistrationsPanel() {
12933
+ const { activeServiceId, remove: remove2, clear, check } = useFallbackEditor();
12934
+ const registrations = useActiveFallbackRegistrations();
12935
+ const eligibleServices = useEligibleServiceList();
12936
+ const [candidatePickerOpen, setCandidatePickerOpen] = React11.useState(false);
12937
+ const [candidateContext, setCandidateContext] = React11.useState(null);
12938
+ const [candidatePrimaryId, setCandidatePrimaryId] = React11.useState(void 0);
12939
+ const [registrationDialogOpen, setRegistrationDialogOpen] = React11.useState(false);
12940
+ const makeContext = React11.useCallback(
12941
+ (registration) => {
12942
+ if (registration.scope === "global") {
12943
+ return {
12944
+ scope: "global",
12945
+ primary: registration.primary
12946
+ };
12947
+ }
12948
+ return {
12949
+ scope: "node",
12950
+ nodeId: registration.scopeId
12951
+ };
12952
+ },
12953
+ []
12954
+ );
12955
+ const openCandidatePicker = React11.useCallback(
12956
+ (context, primaryId) => {
12957
+ setCandidateContext(context);
12958
+ setCandidatePrimaryId(primaryId);
12959
+ setCandidatePickerOpen(true);
12960
+ },
12961
+ []
12962
+ );
12963
+ if (activeServiceId === void 0 || activeServiceId === null) {
12964
+ return /* @__PURE__ */ jsx12("section", { className: "rounded-2xl border border-zinc-200 bg-white p-4 shadow-sm dark:border-zinc-800 dark:bg-zinc-900 ", children: /* @__PURE__ */ jsx12("div", { className: "rounded-2xl border border-dashed border-zinc-300 p-6 text-sm text-zinc-500 dark:border-zinc-700 dark:text-zinc-400", children: "Select a primary service to start editing." }) });
12965
+ }
12966
+ return /* @__PURE__ */ jsxs8(Fragment, { children: [
12967
+ /* @__PURE__ */ jsxs8("section", { className: "rounded-2xl border border-zinc-200 bg-white p-4 shadow-sm dark:border-zinc-800 dark:bg-zinc-900 overflow-y-auto", children: [
12968
+ /* @__PURE__ */ jsxs8("div", { className: "mb-4 flex items-start justify-between gap-3", children: [
12969
+ /* @__PURE__ */ jsxs8("div", { children: [
12970
+ /* @__PURE__ */ jsx12("h3", { className: "text-sm font-semibold text-zinc-900 dark:text-zinc-100", children: "Registered fallbacks" }),
12971
+ /* @__PURE__ */ jsx12("p", { className: "mt-1 text-xs text-zinc-500 dark:text-zinc-400", children: "Use eligible services as fallback candidates for the selected primary." })
12972
+ ] }),
12973
+ /* @__PURE__ */ jsx12(
12974
+ "button",
12975
+ {
12976
+ type: "button",
12977
+ onClick: () => setRegistrationDialogOpen(true),
12978
+ className: "rounded-xl border border-zinc-300 bg-white px-3 py-2 text-sm font-medium text-zinc-700 hover:bg-zinc-50 dark:border-zinc-700 dark:bg-zinc-950 dark:text-zinc-200 dark:hover:bg-zinc-800",
12979
+ children: "Add registration"
12980
+ }
12981
+ )
12982
+ ] }),
12983
+ /* @__PURE__ */ jsx12("div", { className: "space-y-4", children: registrations.length === 0 ? /* @__PURE__ */ jsx12("div", { className: "rounded-2xl border border-dashed border-zinc-300 p-6 text-sm text-zinc-500 dark:border-zinc-700 dark:text-zinc-400", children: "No registrations yet for this primary service." }) : registrations.map((reg, index) => {
12984
+ var _a;
12985
+ const context = makeContext(reg);
12986
+ const candidates = reg.services;
12987
+ return /* @__PURE__ */ jsxs8(
12988
+ "div",
12989
+ {
12990
+ className: "rounded-2xl border border-zinc-200 bg-zinc-50 p-4 dark:border-zinc-800 dark:bg-zinc-950",
12991
+ children: [
12992
+ /* @__PURE__ */ jsxs8("div", { className: "flex flex-wrap items-start justify-between gap-3", children: [
12993
+ /* @__PURE__ */ jsxs8("div", { children: [
12994
+ /* @__PURE__ */ jsx12("div", { className: "text-sm font-semibold text-zinc-900 dark:text-zinc-100", children: reg.scope === "global" ? "Global registration" : `Node \xB7 ${reg.scopeId}` }),
12995
+ /* @__PURE__ */ jsxs8("div", { className: "mt-1 text-xs text-zinc-500 dark:text-zinc-400", children: [
12996
+ "Primary #",
12997
+ String(reg.primary)
12998
+ ] })
12999
+ ] }),
13000
+ /* @__PURE__ */ jsxs8("span", { className: "rounded-full border border-zinc-200 bg-white px-2 py-1 text-[11px] text-zinc-600 dark:border-zinc-700 dark:bg-zinc-900 dark:text-zinc-300", children: [
13001
+ reg.scope,
13002
+ reg.scopeId ? ` \xB7 ${reg.scopeId}` : ""
13003
+ ] })
13004
+ ] }),
13005
+ /* @__PURE__ */ jsx12("div", { className: "mt-4 flex flex-wrap gap-2", children: candidates.length === 0 ? /* @__PURE__ */ jsx12("span", { className: "text-xs text-zinc-500 dark:text-zinc-400", children: "No fallback services yet." }) : candidates.map((candidate) => {
13006
+ var _a2;
13007
+ const preview = check(context, [
13008
+ candidate
13009
+ ]);
13010
+ const rejected = preview.rejected[0];
13011
+ const tone = rejected ? "border-red-200 bg-red-50 text-red-700 dark:border-red-900/50 dark:bg-red-950/30 dark:text-red-300" : "border-zinc-200 bg-white text-zinc-700 dark:border-zinc-700 dark:bg-zinc-900 dark:text-zinc-200";
13012
+ const service = eligibleServices.find(
13013
+ (s) => String(s.id) === String(candidate)
13014
+ );
13015
+ return /* @__PURE__ */ jsxs8(
13016
+ "div",
13017
+ {
13018
+ className: `inline-flex items-center gap-2 rounded-xl border px-3 py-2 text-xs ${tone}`,
13019
+ children: [
13020
+ /* @__PURE__ */ jsx12("span", { children: service ? `#${String(service.id)} \xB7 ${(_a2 = service.name) != null ? _a2 : "Unnamed"}` : `#${String(candidate)}` }),
13021
+ rejected ? /* @__PURE__ */ jsx12("span", { className: "rounded-full border border-current/20 px-2 py-0.5 text-[10px]", children: rejected.reasons.join(
13022
+ ", "
13023
+ ) }) : /* @__PURE__ */ jsx12("span", { className: "rounded-full border border-current/20 px-2 py-0.5 text-[10px]", children: "valid" }),
13024
+ /* @__PURE__ */ jsx12(
13025
+ "button",
13026
+ {
13027
+ type: "button",
13028
+ onClick: () => remove2(
13029
+ context,
13030
+ candidate
13031
+ ),
13032
+ className: "text-current/70 hover:text-current",
13033
+ children: "\xD7"
13034
+ }
13035
+ )
13036
+ ]
13037
+ },
13038
+ String(candidate)
13039
+ );
13040
+ }) }),
13041
+ /* @__PURE__ */ jsxs8("div", { className: "mt-4 flex gap-2", children: [
13042
+ /* @__PURE__ */ jsx12(
13043
+ "button",
13044
+ {
13045
+ type: "button",
13046
+ onClick: () => openCandidatePicker(
13047
+ context,
13048
+ reg.primary
13049
+ ),
13050
+ className: "rounded-xl border border-zinc-300 bg-white px-3 py-2 text-sm font-medium text-zinc-700 hover:bg-zinc-50 dark:border-zinc-700 dark:bg-zinc-900 dark:text-zinc-200 dark:hover:bg-zinc-800",
13051
+ children: "Add fallback"
13052
+ }
13053
+ ),
13054
+ /* @__PURE__ */ jsx12(
13055
+ "button",
13056
+ {
13057
+ type: "button",
13058
+ onClick: () => clear(context),
13059
+ className: "rounded-xl border border-red-300 bg-white px-3 py-2 text-sm font-medium text-red-600 hover:bg-red-50 dark:border-red-900/50 dark:bg-zinc-900 dark:text-red-300 dark:hover:bg-red-950/20",
13060
+ children: "Clear"
13061
+ }
13062
+ )
13063
+ ] })
13064
+ ]
13065
+ },
13066
+ `${reg.scope}:${String((_a = reg.scopeId) != null ? _a : "global")}:${index}`
13067
+ );
13068
+ }) })
13069
+ ] }),
13070
+ /* @__PURE__ */ jsx12(
13071
+ FallbackAddRegistrationDialog,
13072
+ {
13073
+ open: registrationDialogOpen,
13074
+ onClose: () => setRegistrationDialogOpen(false),
13075
+ onSelect: (context, primaryId) => {
13076
+ setRegistrationDialogOpen(false);
13077
+ openCandidatePicker(context, primaryId);
13078
+ }
13079
+ }
13080
+ ),
13081
+ /* @__PURE__ */ jsx12(
13082
+ FallbackAddCandidatesDialog,
13083
+ {
13084
+ open: candidatePickerOpen,
13085
+ onClose: () => setCandidatePickerOpen(false),
13086
+ context: candidateContext,
13087
+ primaryId: candidatePrimaryId
13088
+ }
13089
+ )
13090
+ ] });
13091
+ }
12695
13092
 
12696
13093
  // src/react/fallback-editor/FallbackAddCandidatesDialog.tsx
12697
- import React11 from "react";
12698
- import { jsx as jsx12, jsxs as jsxs8 } from "react/jsx-runtime";
13094
+ import React12 from "react";
13095
+ import { InputField as InputField23 } from "@timeax/form-palette";
13096
+ import { Search as Search2 } from "lucide-react";
13097
+ import { jsx as jsx13, jsxs as jsxs9 } from "react/jsx-runtime";
12699
13098
  function FallbackAddCandidatesDialog({
12700
13099
  open,
12701
13100
  onClose,
@@ -12704,23 +13103,23 @@ function FallbackAddCandidatesDialog({
12704
13103
  }) {
12705
13104
  const { eligible, addMany } = useFallbackEditor();
12706
13105
  const eligibleServices = useEligibleServiceList();
12707
- const [query, setQuery] = React11.useState("");
12708
- const [filterEligibleOnly, setFilterEligibleOnly] = React11.useState(true);
12709
- const [selected, setSelected] = React11.useState(/* @__PURE__ */ new Set());
12710
- const [submitting, setSubmitting] = React11.useState(false);
12711
- React11.useEffect(() => {
13106
+ const [query, setQuery] = React12.useState("");
13107
+ const [filterEligibleOnly, setFilterEligibleOnly] = React12.useState(true);
13108
+ const [selected, setSelected] = React12.useState(/* @__PURE__ */ new Set());
13109
+ const [submitting, setSubmitting] = React12.useState(false);
13110
+ React12.useEffect(() => {
12712
13111
  if (!open) {
12713
13112
  setQuery("");
12714
13113
  setFilterEligibleOnly(true);
12715
13114
  setSelected(/* @__PURE__ */ new Set());
12716
13115
  }
12717
13116
  }, [open]);
12718
- const allowedIds = React11.useMemo(() => {
13117
+ const allowedIds = React12.useMemo(() => {
12719
13118
  if (!context) return null;
12720
13119
  if (!filterEligibleOnly) return null;
12721
13120
  return new Set(eligible(context).map((id) => String(id)));
12722
13121
  }, [context, filterEligibleOnly, eligible]);
12723
- const items = React11.useMemo(() => {
13122
+ const items = React12.useMemo(() => {
12724
13123
  const q = query.trim().toLowerCase();
12725
13124
  return eligibleServices.filter((service) => {
12726
13125
  var _a, _b;
@@ -12755,34 +13154,37 @@ function FallbackAddCandidatesDialog({
12755
13154
  }
12756
13155
  }
12757
13156
  if (!open || !context) return null;
12758
- return /* @__PURE__ */ jsx12("div", { className: "fixed inset-0 z-50 flex items-center justify-center bg-black/50 p-4", children: /* @__PURE__ */ jsxs8("div", { className: "flex max-h-[85vh] w-full max-w-3xl flex-col rounded-2xl border border-zinc-200 bg-white shadow-2xl dark:border-zinc-800 dark:bg-zinc-900", children: [
12759
- /* @__PURE__ */ jsxs8("div", { className: "border-b border-zinc-200 p-4 dark:border-zinc-800", children: [
12760
- /* @__PURE__ */ jsx12("h3", { className: "text-base font-semibold text-zinc-900 dark:text-zinc-100", children: "Add fallback services" }),
12761
- /* @__PURE__ */ jsx12("p", { className: "mt-1 text-sm text-zinc-500 dark:text-zinc-400", children: "Search and select one or more eligible fallback candidates." })
13157
+ return /* @__PURE__ */ jsx13("div", { className: "fixed inset-0 z-50 flex items-center justify-center bg-black/60 p-4", children: /* @__PURE__ */ jsxs9("div", { className: "flex max-h-[85vh] w-full max-w-3xl flex-col rounded-2xl border border-zinc-200 bg-white shadow-2xl dark:border-zinc-800 dark:bg-zinc-900", children: [
13158
+ /* @__PURE__ */ jsxs9("div", { className: "border-b border-zinc-200 p-4 dark:border-zinc-800", children: [
13159
+ /* @__PURE__ */ jsx13("h3", { className: "text-base font-semibold text-zinc-900 dark:text-zinc-100", children: "Add fallback services" }),
13160
+ /* @__PURE__ */ jsx13("p", { className: "mt-1 text-sm text-zinc-500 dark:text-zinc-400", children: "Search and select one or more eligible fallback candidates." })
12762
13161
  ] }),
12763
- /* @__PURE__ */ jsxs8("div", { className: "flex flex-col gap-3 p-4", children: [
12764
- /* @__PURE__ */ jsx12(
12765
- "input",
13162
+ /* @__PURE__ */ jsxs9("div", { className: "flex flex-col gap-3 p-4", children: [
13163
+ /* @__PURE__ */ jsx13(
13164
+ InputField23,
12766
13165
  {
13166
+ variant: "text",
12767
13167
  value: query,
12768
- onChange: (e) => setQuery(e.target.value),
13168
+ onChange: ({ value }) => setQuery(String(value != null ? value : "")),
12769
13169
  placeholder: "Search eligible services...",
12770
- className: "rounded-xl border border-zinc-300 bg-white px-3 py-2 text-sm outline-none focus:border-blue-500 dark:border-zinc-700 dark:bg-zinc-950 dark:text-zinc-100"
13170
+ leadingControl: /* @__PURE__ */ jsx13(Search2, { className: "h-4 w-4 text-zinc-400" }),
13171
+ joinControls: true,
13172
+ extendBoxToControls: true,
13173
+ fullWidth: true
12771
13174
  }
12772
13175
  ),
12773
- /* @__PURE__ */ jsxs8("label", { className: "inline-flex items-center gap-2 text-sm text-zinc-700 dark:text-zinc-300", children: [
12774
- /* @__PURE__ */ jsx12(
12775
- "input",
12776
- {
12777
- type: "checkbox",
12778
- checked: filterEligibleOnly,
12779
- onChange: (e) => setFilterEligibleOnly(e.target.checked),
12780
- className: "h-4 w-4 rounded border-zinc-300"
12781
- }
12782
- ),
12783
- "Filter eligible only"
12784
- ] }),
12785
- /* @__PURE__ */ jsx12(
13176
+ /* @__PURE__ */ jsx13(
13177
+ InputField23,
13178
+ {
13179
+ variant: "toggle",
13180
+ value: filterEligibleOnly,
13181
+ onChange: ({ value }) => setFilterEligibleOnly(Boolean(value)),
13182
+ label: "Filter eligible only",
13183
+ onText: "On",
13184
+ offText: "Off"
13185
+ }
13186
+ ),
13187
+ /* @__PURE__ */ jsx13(
12786
13188
  VirtualServiceList,
12787
13189
  {
12788
13190
  items,
@@ -12792,28 +13194,28 @@ function FallbackAddCandidatesDialog({
12792
13194
  }
12793
13195
  )
12794
13196
  ] }),
12795
- /* @__PURE__ */ jsxs8("div", { className: "flex items-center justify-between border-t border-zinc-200 p-4 dark:border-zinc-800", children: [
12796
- /* @__PURE__ */ jsxs8("div", { className: "text-sm text-zinc-500 dark:text-zinc-400", children: [
13197
+ /* @__PURE__ */ jsxs9("div", { className: "flex items-center justify-between border-t border-zinc-200 p-4 dark:border-zinc-800", children: [
13198
+ /* @__PURE__ */ jsxs9("div", { className: "text-sm text-zinc-500 dark:text-zinc-400", children: [
12797
13199
  selected.size,
12798
13200
  " selected"
12799
13201
  ] }),
12800
- /* @__PURE__ */ jsxs8("div", { className: "flex gap-2", children: [
12801
- /* @__PURE__ */ jsx12(
13202
+ /* @__PURE__ */ jsxs9("div", { className: "flex gap-2", children: [
13203
+ /* @__PURE__ */ jsx13(
12802
13204
  "button",
12803
13205
  {
12804
13206
  type: "button",
12805
13207
  onClick: onClose,
12806
- className: "rounded-xl border border-zinc-300 bg-white px-3 py-2 text-sm font-medium text-zinc-700 hover:bg-zinc-50 dark:border-zinc-700 dark:bg-zinc-950 dark:text-zinc-200 dark:hover:bg-zinc-800",
13208
+ className: "rounded-xl border border-zinc-300 bg-white px-3 py-2 text-sm font-medium text-zinc-700 transition hover:bg-zinc-50 dark:border-zinc-700 dark:bg-zinc-950 dark:text-zinc-200 dark:hover:bg-zinc-800",
12807
13209
  children: "Cancel"
12808
13210
  }
12809
13211
  ),
12810
- /* @__PURE__ */ jsx12(
13212
+ /* @__PURE__ */ jsx13(
12811
13213
  "button",
12812
13214
  {
12813
13215
  type: "button",
12814
13216
  onClick: handleAdd,
12815
13217
  disabled: selected.size === 0 || submitting,
12816
- className: "rounded-xl bg-blue-600 px-3 py-2 text-sm font-medium text-white hover:bg-blue-700 disabled:cursor-not-allowed disabled:opacity-60",
13218
+ className: "rounded-xl bg-blue-600 px-3 py-2 text-sm font-medium text-white transition hover:bg-blue-700 disabled:cursor-not-allowed disabled:opacity-60",
12817
13219
  children: submitting ? "Adding..." : "Add selected"
12818
13220
  }
12819
13221
  )
@@ -12823,8 +13225,9 @@ function FallbackAddCandidatesDialog({
12823
13225
  }
12824
13226
 
12825
13227
  // src/react/fallback-editor/FallbackAddRegistrationDialog.tsx
12826
- import React12 from "react";
12827
- import { jsx as jsx13, jsxs as jsxs9 } from "react/jsx-runtime";
13228
+ import React13 from "react";
13229
+ import { InputField as InputField24 } from "@timeax/form-palette";
13230
+ import { jsx as jsx14, jsxs as jsxs10 } from "react/jsx-runtime";
12828
13231
  function FallbackAddRegistrationDialog({
12829
13232
  open,
12830
13233
  onClose,
@@ -12832,23 +13235,23 @@ function FallbackAddRegistrationDialog({
12832
13235
  }) {
12833
13236
  const { activeServiceId, serviceProps, snapshot } = useFallbackEditor();
12834
13237
  const registrations = useActiveFallbackRegistrations();
12835
- const [scope, setScope] = React12.useState("global");
12836
- const [nodeId, setNodeId] = React12.useState("");
12837
- const mode = React12.useMemo(() => {
13238
+ const [scope, setScope] = React13.useState("global");
13239
+ const [nodeId, setNodeId] = React13.useState("");
13240
+ const mode = React13.useMemo(() => {
12838
13241
  if (snapshot) return "snapshot";
12839
13242
  if (serviceProps) return "props";
12840
13243
  return "none";
12841
13244
  }, [snapshot, serviceProps]);
12842
- React12.useEffect(() => {
13245
+ React13.useEffect(() => {
12843
13246
  if (open) {
12844
13247
  setScope("global");
12845
13248
  setNodeId("");
12846
13249
  }
12847
13250
  }, [open]);
12848
- const hasGlobal = React12.useMemo(() => {
13251
+ const hasGlobal = React13.useMemo(() => {
12849
13252
  return registrations.some((r) => r.scope === "global");
12850
13253
  }, [registrations]);
12851
- const nodeTargets = React12.useMemo(() => {
13254
+ const nodeTargets = React13.useMemo(() => {
12852
13255
  var _a, _b, _c, _d, _e, _f, _g, _h, _i, _j, _k;
12853
13256
  if (activeServiceId === void 0 || activeServiceId === null) {
12854
13257
  return [];
@@ -12938,12 +13341,12 @@ function FallbackAddRegistrationDialog({
12938
13341
  }
12939
13342
  return [];
12940
13343
  }, [mode, snapshot, serviceProps, activeServiceId]);
12941
- React12.useEffect(() => {
13344
+ React13.useEffect(() => {
12942
13345
  if (hasGlobal && scope === "global") {
12943
13346
  setScope("node");
12944
13347
  }
12945
13348
  }, [hasGlobal, scope]);
12946
- React12.useEffect(() => {
13349
+ React13.useEffect(() => {
12947
13350
  if (scope === "node" && nodeId) {
12948
13351
  const exists = nodeTargets.some((node) => node.id === nodeId);
12949
13352
  if (!exists) setNodeId("");
@@ -12974,100 +13377,74 @@ function FallbackAddRegistrationDialog({
12974
13377
  }
12975
13378
  if (!open) return null;
12976
13379
  const nodeScopeDisabled = nodeTargets.length === 0;
12977
- return /* @__PURE__ */ jsx13("div", { className: "fixed inset-0 z-50 flex items-center justify-center bg-black/50 p-4", children: /* @__PURE__ */ jsxs9("div", { className: "w-full max-w-lg rounded-2xl border border-zinc-200 bg-white shadow-2xl dark:border-zinc-800 dark:bg-zinc-900", children: [
12978
- /* @__PURE__ */ jsxs9("div", { className: "border-b border-zinc-200 p-4 dark:border-zinc-800", children: [
12979
- /* @__PURE__ */ jsx13("h3", { className: "text-base font-semibold text-zinc-900 dark:text-zinc-100", children: "Add registration" }),
12980
- /* @__PURE__ */ jsx13("p", { className: "mt-1 text-sm text-zinc-500 dark:text-zinc-400", children: "Choose the registration scope before selecting fallback candidates." })
13380
+ return /* @__PURE__ */ jsx14("div", { className: "fixed inset-0 z-50 flex items-center justify-center bg-black/60 p-4", children: /* @__PURE__ */ jsxs10("div", { className: "w-full max-w-lg rounded-2xl border border-zinc-200 bg-white shadow-2xl dark:border-zinc-800 dark:bg-zinc-900", children: [
13381
+ /* @__PURE__ */ jsxs10("div", { className: "border-b border-zinc-200 p-4 dark:border-zinc-800", children: [
13382
+ /* @__PURE__ */ jsx14("h3", { className: "text-base font-semibold text-zinc-900 dark:text-zinc-100", children: "Add registration" }),
13383
+ /* @__PURE__ */ jsx14("p", { className: "mt-1 text-sm text-zinc-500 dark:text-zinc-400", children: "Choose the registration scope before selecting fallback candidates." })
12981
13384
  ] }),
12982
- /* @__PURE__ */ jsxs9("div", { className: "space-y-4 p-4", children: [
12983
- /* @__PURE__ */ jsxs9("div", { className: "space-y-2", children: [
12984
- /* @__PURE__ */ jsx13("div", { className: "text-sm font-medium text-zinc-900 dark:text-zinc-100", children: "Scope" }),
12985
- /* @__PURE__ */ jsxs9("div", { className: "grid gap-2", children: [
12986
- !hasGlobal && /* @__PURE__ */ jsxs9("label", { className: "flex cursor-pointer items-start gap-3 rounded-xl border border-zinc-200 p-3 dark:border-zinc-800", children: [
12987
- /* @__PURE__ */ jsx13(
12988
- "input",
13385
+ /* @__PURE__ */ jsxs10("div", { className: "space-y-4 p-4", children: [
13386
+ /* @__PURE__ */ jsxs10("div", { className: "space-y-2", children: [
13387
+ /* @__PURE__ */ jsx14("div", { className: "text-sm font-medium text-zinc-900 dark:text-zinc-100", children: "Scope" }),
13388
+ /* @__PURE__ */ jsx14(
13389
+ InputField24,
13390
+ {
13391
+ variant: "radio",
13392
+ value: scope,
13393
+ onChange: ({ value }) => setScope(value),
13394
+ options: [
13395
+ ...!hasGlobal ? [
13396
+ {
13397
+ value: "global",
13398
+ label: "Global"
13399
+ }
13400
+ ] : [],
12989
13401
  {
12990
- type: "radio",
12991
- name: "scope",
12992
- checked: scope === "global",
12993
- onChange: () => setScope("global"),
12994
- className: "mt-1 h-4 w-4"
13402
+ value: "node",
13403
+ label: nodeScopeDisabled ? "Node (Unavailable)" : "Node"
12995
13404
  }
12996
- ),
12997
- /* @__PURE__ */ jsxs9("div", { children: [
12998
- /* @__PURE__ */ jsx13("div", { className: "text-sm font-medium text-zinc-900 dark:text-zinc-100", children: "Global" }),
12999
- /* @__PURE__ */ jsx13("div", { className: "mt-1 text-xs text-zinc-500 dark:text-zinc-400", children: "Use one global registration for this primary service." })
13000
- ] })
13001
- ] }),
13002
- /* @__PURE__ */ jsxs9(
13003
- "label",
13004
- {
13005
- className: [
13006
- "flex items-start gap-3 rounded-xl border p-3",
13007
- nodeScopeDisabled ? "cursor-not-allowed border-zinc-200 opacity-60 dark:border-zinc-800" : "cursor-pointer border-zinc-200 dark:border-zinc-800"
13008
- ].join(" "),
13009
- children: [
13010
- /* @__PURE__ */ jsx13(
13011
- "input",
13012
- {
13013
- type: "radio",
13014
- name: "scope",
13015
- checked: scope === "node",
13016
- onChange: () => setScope("node"),
13017
- disabled: nodeScopeDisabled,
13018
- className: "mt-1 h-4 w-4"
13019
- }
13020
- ),
13021
- /* @__PURE__ */ jsxs9("div", { children: [
13022
- /* @__PURE__ */ jsx13("div", { className: "text-sm font-medium text-zinc-900 dark:text-zinc-100", children: "Node" }),
13023
- /* @__PURE__ */ jsx13("div", { className: "mt-1 text-xs text-zinc-500 dark:text-zinc-400", children: mode === "snapshot" ? "Pick a node currently active in the order snapshot for this primary service." : mode === "props" ? "Pick a tag, field, or option from ServiceProps that maps to this primary service." : "Node-scoped registration is unavailable without OrderSnapshot or ServiceProps." })
13024
- ] })
13025
- ]
13026
- }
13027
- )
13028
- ] })
13405
+ ]
13406
+ }
13407
+ ),
13408
+ /* @__PURE__ */ jsx14("div", { className: "text-xs text-zinc-500 dark:text-zinc-400", children: scope === "global" ? "Use one global registration for this primary service." : mode === "snapshot" ? "Pick a node currently active in the order snapshot for this primary service." : mode === "props" ? "Pick a tag, field, or option from ServiceProps that maps to this primary service." : "Node-scoped registration is unavailable without OrderSnapshot or ServiceProps." })
13029
13409
  ] }),
13030
- scope === "node" && /* @__PURE__ */ jsxs9("div", { className: "space-y-2", children: [
13031
- /* @__PURE__ */ jsx13("div", { className: "text-sm font-medium text-zinc-900 dark:text-zinc-100", children: "Node id" }),
13032
- /* @__PURE__ */ jsxs9(
13033
- "select",
13410
+ scope === "node" && /* @__PURE__ */ jsxs10("div", { className: "space-y-2", children: [
13411
+ /* @__PURE__ */ jsx14("div", { className: "text-sm font-medium text-zinc-900 dark:text-zinc-100", children: "Node id" }),
13412
+ /* @__PURE__ */ jsx14(
13413
+ InputField24,
13034
13414
  {
13035
- value: nodeId,
13036
- onChange: (e) => setNodeId(e.target.value),
13037
- className: "w-full rounded-xl border border-zinc-300 bg-white px-3 py-2 text-sm outline-none focus:border-blue-500 dark:border-zinc-700 dark:bg-zinc-950 dark:text-zinc-100",
13038
- children: [
13039
- /* @__PURE__ */ jsx13("option", { value: "", children: "Select node\u2026" }),
13040
- nodeTargets.map((node) => /* @__PURE__ */ jsxs9("option", { value: node.id, children: [
13041
- "[",
13042
- node.kind,
13043
- "] ",
13044
- node.label,
13045
- " \xB7 #",
13046
- String(node.serviceId)
13047
- ] }, node.id))
13048
- ]
13415
+ variant: "select",
13416
+ value: nodeId || void 0,
13417
+ onChange: ({ value }) => setNodeId(String(value != null ? value : "")),
13418
+ options: nodeTargets.map((node) => ({
13419
+ value: node.id,
13420
+ label: `[${node.kind}] ${node.label} \xB7 #${String(node.serviceId)}`
13421
+ })),
13422
+ placeholder: "Select node...",
13423
+ searchable: true,
13424
+ clearable: false,
13425
+ fullWidth: true
13049
13426
  }
13050
13427
  ),
13051
- nodeScopeDisabled ? /* @__PURE__ */ jsx13("div", { className: "text-xs text-zinc-500 dark:text-zinc-400", children: mode === "snapshot" ? "No active snapshot nodes were found for this primary service." : mode === "props" ? "No ServiceProps nodes were found for this primary service." : "Node-scoped registration requires either OrderSnapshot or ServiceProps." }) : null
13428
+ nodeScopeDisabled ? /* @__PURE__ */ jsx14("div", { className: "text-xs text-zinc-500 dark:text-zinc-400", children: mode === "snapshot" ? "No active snapshot nodes were found for this primary service." : mode === "props" ? "No ServiceProps nodes were found for this primary service." : "Node-scoped registration requires either OrderSnapshot or ServiceProps." }) : null
13052
13429
  ] })
13053
13430
  ] }),
13054
- /* @__PURE__ */ jsxs9("div", { className: "flex items-center justify-end gap-2 border-t border-zinc-200 p-4 dark:border-zinc-800", children: [
13055
- /* @__PURE__ */ jsx13(
13431
+ /* @__PURE__ */ jsxs10("div", { className: "flex items-center justify-end gap-2 border-t border-zinc-200 p-4 dark:border-zinc-800", children: [
13432
+ /* @__PURE__ */ jsx14(
13056
13433
  "button",
13057
13434
  {
13058
13435
  type: "button",
13059
13436
  onClick: onClose,
13060
- className: "rounded-xl border border-zinc-300 bg-white px-3 py-2 text-sm font-medium text-zinc-700 hover:bg-zinc-50 dark:border-zinc-700 dark:bg-zinc-950 dark:text-zinc-200 dark:hover:bg-zinc-800",
13437
+ className: "rounded-xl border border-zinc-300 bg-white px-3 py-2 text-sm font-medium text-zinc-700 transition hover:bg-zinc-50 dark:border-zinc-700 dark:bg-zinc-950 dark:text-zinc-200 dark:hover:bg-zinc-800",
13061
13438
  children: "Cancel"
13062
13439
  }
13063
13440
  ),
13064
- /* @__PURE__ */ jsx13(
13441
+ /* @__PURE__ */ jsx14(
13065
13442
  "button",
13066
13443
  {
13067
13444
  type: "button",
13068
13445
  onClick: handleContinue,
13069
13446
  disabled: activeServiceId === void 0 || scope === "node" && !nodeId,
13070
- className: "rounded-xl bg-blue-600 px-3 py-2 text-sm font-medium text-white hover:bg-blue-700 disabled:cursor-not-allowed disabled:opacity-60",
13447
+ className: "rounded-xl bg-blue-600 px-3 py-2 text-sm font-medium text-white transition hover:bg-blue-700 disabled:cursor-not-allowed disabled:opacity-60",
13071
13448
  children: "Continue"
13072
13449
  }
13073
13450
  )
@@ -13104,169 +13481,6 @@ function resolveNodeMeta(props, nodeId) {
13104
13481
  }
13105
13482
  return { kind: "node", label: nodeId };
13106
13483
  }
13107
-
13108
- // src/react/fallback-editor/FallbackRegistrationsPanel.tsx
13109
- import { Fragment, jsx as jsx14, jsxs as jsxs10 } from "react/jsx-runtime";
13110
- function FallbackRegistrationsPanel() {
13111
- const { activeServiceId, remove: remove2, clear, check } = useFallbackEditor();
13112
- const registrations = useActiveFallbackRegistrations();
13113
- const eligibleServices = useEligibleServiceList();
13114
- const [candidatePickerOpen, setCandidatePickerOpen] = React13.useState(false);
13115
- const [candidateContext, setCandidateContext] = React13.useState(null);
13116
- const [candidatePrimaryId, setCandidatePrimaryId] = React13.useState(void 0);
13117
- const [registrationDialogOpen, setRegistrationDialogOpen] = React13.useState(false);
13118
- const makeContext = React13.useCallback(
13119
- (registration) => {
13120
- if (registration.scope === "global") {
13121
- return {
13122
- scope: "global",
13123
- primary: registration.primary
13124
- };
13125
- }
13126
- return {
13127
- scope: "node",
13128
- nodeId: registration.scopeId
13129
- };
13130
- },
13131
- []
13132
- );
13133
- const openCandidatePicker = React13.useCallback(
13134
- (context, primaryId) => {
13135
- setCandidateContext(context);
13136
- setCandidatePrimaryId(primaryId);
13137
- setCandidatePickerOpen(true);
13138
- },
13139
- []
13140
- );
13141
- if (activeServiceId === void 0 || activeServiceId === null) {
13142
- return /* @__PURE__ */ jsx14("section", { className: "rounded-2xl border border-zinc-200 bg-white p-4 shadow-sm dark:border-zinc-800 dark:bg-zinc-900", children: /* @__PURE__ */ jsx14("div", { className: "rounded-2xl border border-dashed border-zinc-300 p-6 text-sm text-zinc-500 dark:border-zinc-700 dark:text-zinc-400", children: "Select a primary service to start editing." }) });
13143
- }
13144
- return /* @__PURE__ */ jsxs10(Fragment, { children: [
13145
- /* @__PURE__ */ jsxs10("section", { className: "rounded-2xl border border-zinc-200 bg-white p-4 shadow-sm dark:border-zinc-800 dark:bg-zinc-900", children: [
13146
- /* @__PURE__ */ jsxs10("div", { className: "mb-4 flex items-start justify-between gap-3", children: [
13147
- /* @__PURE__ */ jsxs10("div", { children: [
13148
- /* @__PURE__ */ jsx14("h3", { className: "text-sm font-semibold text-zinc-900 dark:text-zinc-100", children: "Registered fallbacks" }),
13149
- /* @__PURE__ */ jsx14("p", { className: "mt-1 text-xs text-zinc-500 dark:text-zinc-400", children: "Use eligible services as fallback candidates for the selected primary." })
13150
- ] }),
13151
- /* @__PURE__ */ jsx14(
13152
- "button",
13153
- {
13154
- type: "button",
13155
- onClick: () => setRegistrationDialogOpen(true),
13156
- className: "rounded-xl border border-zinc-300 bg-white px-3 py-2 text-sm font-medium text-zinc-700 hover:bg-zinc-50 dark:border-zinc-700 dark:bg-zinc-950 dark:text-zinc-200 dark:hover:bg-zinc-800",
13157
- children: "Add registration"
13158
- }
13159
- )
13160
- ] }),
13161
- /* @__PURE__ */ jsx14("div", { className: "space-y-4", children: registrations.length === 0 ? /* @__PURE__ */ jsx14("div", { className: "rounded-2xl border border-dashed border-zinc-300 p-6 text-sm text-zinc-500 dark:border-zinc-700 dark:text-zinc-400", children: "No registrations yet for this primary service." }) : registrations.map((reg, index) => {
13162
- var _a;
13163
- const context = makeContext(reg);
13164
- const candidates = reg.services;
13165
- return /* @__PURE__ */ jsxs10(
13166
- "div",
13167
- {
13168
- className: "rounded-2xl border border-zinc-200 bg-zinc-50 p-4 dark:border-zinc-800 dark:bg-zinc-950",
13169
- children: [
13170
- /* @__PURE__ */ jsxs10("div", { className: "flex flex-wrap items-start justify-between gap-3", children: [
13171
- /* @__PURE__ */ jsxs10("div", { children: [
13172
- /* @__PURE__ */ jsx14("div", { className: "text-sm font-semibold text-zinc-900 dark:text-zinc-100", children: reg.scope === "global" ? "Global registration" : `Node \xB7 ${reg.scopeId}` }),
13173
- /* @__PURE__ */ jsxs10("div", { className: "mt-1 text-xs text-zinc-500 dark:text-zinc-400", children: [
13174
- "Primary #",
13175
- String(reg.primary)
13176
- ] })
13177
- ] }),
13178
- /* @__PURE__ */ jsxs10("span", { className: "rounded-full border border-zinc-200 bg-white px-2 py-1 text-[11px] text-zinc-600 dark:border-zinc-700 dark:bg-zinc-900 dark:text-zinc-300", children: [
13179
- reg.scope,
13180
- reg.scopeId ? ` \xB7 ${reg.scopeId}` : ""
13181
- ] })
13182
- ] }),
13183
- /* @__PURE__ */ jsx14("div", { className: "mt-4 flex flex-wrap gap-2", children: candidates.length === 0 ? /* @__PURE__ */ jsx14("span", { className: "text-xs text-zinc-500 dark:text-zinc-400", children: "No fallback services yet." }) : candidates.map((candidate) => {
13184
- var _a2;
13185
- const preview = check(context, [
13186
- candidate
13187
- ]);
13188
- const rejected = preview.rejected[0];
13189
- const tone = rejected ? "border-red-200 bg-red-50 text-red-700 dark:border-red-900/50 dark:bg-red-950/30 dark:text-red-300" : "border-zinc-200 bg-white text-zinc-700 dark:border-zinc-700 dark:bg-zinc-900 dark:text-zinc-200";
13190
- const service = eligibleServices.find(
13191
- (s) => String(s.id) === String(candidate)
13192
- );
13193
- return /* @__PURE__ */ jsxs10(
13194
- "div",
13195
- {
13196
- className: `inline-flex items-center gap-2 rounded-xl border px-3 py-2 text-xs ${tone}`,
13197
- children: [
13198
- /* @__PURE__ */ jsx14("span", { children: service ? `#${String(service.id)} \xB7 ${(_a2 = service.name) != null ? _a2 : "Unnamed"}` : `#${String(candidate)}` }),
13199
- rejected ? /* @__PURE__ */ jsx14("span", { className: "rounded-full border border-current/20 px-2 py-0.5 text-[10px]", children: rejected.reasons.join(
13200
- ", "
13201
- ) }) : /* @__PURE__ */ jsx14("span", { className: "rounded-full border border-current/20 px-2 py-0.5 text-[10px]", children: "valid" }),
13202
- /* @__PURE__ */ jsx14(
13203
- "button",
13204
- {
13205
- type: "button",
13206
- onClick: () => remove2(
13207
- context,
13208
- candidate
13209
- ),
13210
- className: "text-current/70 hover:text-current",
13211
- children: "\xD7"
13212
- }
13213
- )
13214
- ]
13215
- },
13216
- String(candidate)
13217
- );
13218
- }) }),
13219
- /* @__PURE__ */ jsxs10("div", { className: "mt-4 flex gap-2", children: [
13220
- /* @__PURE__ */ jsx14(
13221
- "button",
13222
- {
13223
- type: "button",
13224
- onClick: () => openCandidatePicker(
13225
- context,
13226
- reg.primary
13227
- ),
13228
- className: "rounded-xl border border-zinc-300 bg-white px-3 py-2 text-sm font-medium text-zinc-700 hover:bg-zinc-50 dark:border-zinc-700 dark:bg-zinc-900 dark:text-zinc-200 dark:hover:bg-zinc-800",
13229
- children: "Add fallback"
13230
- }
13231
- ),
13232
- /* @__PURE__ */ jsx14(
13233
- "button",
13234
- {
13235
- type: "button",
13236
- onClick: () => clear(context),
13237
- className: "rounded-xl border border-red-300 bg-white px-3 py-2 text-sm font-medium text-red-600 hover:bg-red-50 dark:border-red-900/50 dark:bg-zinc-900 dark:text-red-300 dark:hover:bg-red-950/20",
13238
- children: "Clear"
13239
- }
13240
- )
13241
- ] })
13242
- ]
13243
- },
13244
- `${reg.scope}:${String((_a = reg.scopeId) != null ? _a : "global")}:${index}`
13245
- );
13246
- }) })
13247
- ] }),
13248
- /* @__PURE__ */ jsx14(
13249
- FallbackAddRegistrationDialog,
13250
- {
13251
- open: registrationDialogOpen,
13252
- onClose: () => setRegistrationDialogOpen(false),
13253
- onSelect: (context, primaryId) => {
13254
- setRegistrationDialogOpen(false);
13255
- openCandidatePicker(context, primaryId);
13256
- }
13257
- }
13258
- ),
13259
- /* @__PURE__ */ jsx14(
13260
- FallbackAddCandidatesDialog,
13261
- {
13262
- open: candidatePickerOpen,
13263
- onClose: () => setCandidatePickerOpen(false),
13264
- context: candidateContext,
13265
- primaryId: candidatePrimaryId
13266
- }
13267
- )
13268
- ] });
13269
- }
13270
13484
  export {
13271
13485
  CanvasAPI,
13272
13486
  EventBus,