@timeax/digital-service-engine 0.2.0 → 0.2.2

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,18 +1917,84 @@ 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) {
1906
- var _a, _b, _c, _d;
1907
- const ratePolicy = normalizeRatePolicy(
1908
- (_a = v.options.fallbackSettings) == null ? void 0 : _a.ratePolicy
1909
- );
1991
+ var _a, _b, _c;
1992
+ const ratePolicy = normalizeRatePolicy(v.options.ratePolicy);
1910
1993
  for (const f of v.fields) {
1911
1994
  if (!isMultiField(f)) continue;
1912
1995
  const baseRates = [];
1913
- for (const o of (_b = f.options) != null ? _b : []) {
1914
- const role = (_d = (_c = o.pricing_role) != null ? _c : f.pricing_role) != null ? _d : "base";
1996
+ for (const o of (_a = f.options) != null ? _a : []) {
1997
+ const role = (_c = (_b = o.pricing_role) != null ? _b : f.pricing_role) != null ? _c : "base";
1915
1998
  if (role !== "base") continue;
1916
1999
  const sid = o.service_id;
1917
2000
  if (!isServiceIdRef(sid)) continue;
@@ -2685,6 +2768,38 @@ function applyPolicies(errors, props, serviceMap, policies, fieldsVisibleUnder,
2685
2768
  }
2686
2769
  }
2687
2770
 
2771
+ // src/core/governance.ts
2772
+ var DEFAULT_FALLBACK_SETTINGS = {
2773
+ requireConstraintFit: true,
2774
+ ratePolicy: { kind: "lte_primary", pct: 5 },
2775
+ selectionStrategy: "priority",
2776
+ mode: "strict"
2777
+ };
2778
+ function resolveGlobalRatePolicy(options) {
2779
+ return normalizeRatePolicy(options.ratePolicy);
2780
+ }
2781
+ function resolveFallbackSettings(options) {
2782
+ var _a;
2783
+ return {
2784
+ ...DEFAULT_FALLBACK_SETTINGS,
2785
+ ...(_a = options.fallbackSettings) != null ? _a : {}
2786
+ };
2787
+ }
2788
+ function mergeValidatorOptions(defaults = {}, overrides = {}) {
2789
+ var _a, _b, _c, _d;
2790
+ const mergedFallbackSettings = {
2791
+ ...(_a = defaults.fallbackSettings) != null ? _a : {},
2792
+ ...(_b = overrides.fallbackSettings) != null ? _b : {}
2793
+ };
2794
+ return {
2795
+ ...defaults,
2796
+ ...overrides,
2797
+ policies: (_c = overrides.policies) != null ? _c : defaults.policies,
2798
+ ratePolicy: (_d = overrides.ratePolicy) != null ? _d : defaults.ratePolicy,
2799
+ fallbackSettings: Object.keys(mergedFallbackSettings).length > 0 ? mergedFallbackSettings : void 0
2800
+ };
2801
+ }
2802
+
2688
2803
  // src/core/builder.ts
2689
2804
  import { cloneDeep as cloneDeep2 } from "lodash-es";
2690
2805
  function createBuilder(opts = {}) {
@@ -2927,7 +3042,7 @@ var BuilderImpl = class {
2927
3042
  return out;
2928
3043
  }
2929
3044
  errors() {
2930
- return validate(this.props, this.options);
3045
+ return validate(this.props, mergeValidatorOptions({}, this.options));
2931
3046
  }
2932
3047
  getOptions() {
2933
3048
  return cloneDeep2(this.options);
@@ -3214,11 +3329,14 @@ function readVisibilitySimOpts(ctx) {
3214
3329
  };
3215
3330
  }
3216
3331
  function validate(props, ctx = {}) {
3217
- var _a, _b, _c, _d;
3332
+ var _a, _b, _c;
3333
+ const options = mergeValidatorOptions({}, ctx);
3334
+ const fallbackSettings = resolveFallbackSettings(options);
3335
+ const ratePolicy = resolveGlobalRatePolicy(options);
3218
3336
  const errors = [];
3219
- const serviceMap = (_a = ctx.serviceMap) != null ? _a : {};
3337
+ const serviceMap = (_a = options.serviceMap) != null ? _a : {};
3220
3338
  const selectedKeys = new Set(
3221
- (_b = ctx.selectedOptionKeys) != null ? _b : []
3339
+ (_b = options.selectedOptionKeys) != null ? _b : []
3222
3340
  );
3223
3341
  const tags = Array.isArray(props.filters) ? props.filters : [];
3224
3342
  const fields = Array.isArray(props.fields) ? props.fields : [];
@@ -3228,8 +3346,12 @@ function validate(props, ctx = {}) {
3228
3346
  for (const f of fields) fieldById.set(f.id, f);
3229
3347
  const v = {
3230
3348
  props,
3231
- nodeMap: (_c = ctx.nodeMap) != null ? _c : buildNodeMap(props),
3232
- options: ctx,
3349
+ nodeMap: (_c = options.nodeMap) != null ? _c : buildNodeMap(props),
3350
+ options: {
3351
+ ...options,
3352
+ ratePolicy,
3353
+ fallbackSettings
3354
+ },
3233
3355
  errors,
3234
3356
  serviceMap,
3235
3357
  selectedKeys,
@@ -3244,7 +3366,7 @@ function validate(props, ctx = {}) {
3244
3366
  validateIdentity(v);
3245
3367
  validateOptionMaps(v);
3246
3368
  v.fieldsVisibleUnder = createFieldsVisibleUnder(v);
3247
- const visSim = readVisibilitySimOpts(ctx);
3369
+ const visSim = readVisibilitySimOpts(options);
3248
3370
  validateVisibility(v, visSim);
3249
3371
  applyPolicies(
3250
3372
  v.errors,
@@ -3265,7 +3387,7 @@ function validate(props, ctx = {}) {
3265
3387
  builder,
3266
3388
  services: serviceMap,
3267
3389
  tagId: tag.id,
3268
- ratePolicy: (_d = ctx.fallbackSettings) == null ? void 0 : _d.ratePolicy,
3390
+ ratePolicy,
3269
3391
  invalidFieldIds: v.invalidRateFieldIds
3270
3392
  });
3271
3393
  for (const diag of diags) {
@@ -3308,7 +3430,7 @@ function collectFailedFallbacks(props, services, settings) {
3308
3430
  const s = { ...DEFAULT_SETTINGS, ...settings != null ? settings : {} };
3309
3431
  const out = [];
3310
3432
  const fb = (_a = props.fallbacks) != null ? _a : {};
3311
- const primaryRate = (p) => rateOf(services, p);
3433
+ const primaryRate = (primary) => rateOf(services, primary);
3312
3434
  for (const [nodeId, list] of Object.entries((_b = fb.nodes) != null ? _b : {})) {
3313
3435
  const { primary, tagContexts } = primaryForNode(props, nodeId);
3314
3436
  if (!primary) {
@@ -3321,34 +3443,34 @@ function collectFailedFallbacks(props, services, settings) {
3321
3443
  });
3322
3444
  continue;
3323
3445
  }
3324
- for (const cand of list) {
3325
- const cap = getCap(services, cand);
3326
- if (!cap) {
3446
+ for (const candidate of list) {
3447
+ const capability = getCap(services, candidate);
3448
+ if (!capability) {
3327
3449
  out.push({
3328
3450
  scope: "node",
3329
3451
  nodeId,
3330
3452
  primary,
3331
- candidate: cand,
3453
+ candidate,
3332
3454
  reason: "unknown_service"
3333
3455
  });
3334
3456
  continue;
3335
3457
  }
3336
- if (String(cand) === String(primary)) {
3458
+ if (isSameServiceCapabilityRef(services, candidate, primary)) {
3337
3459
  out.push({
3338
3460
  scope: "node",
3339
3461
  nodeId,
3340
3462
  primary,
3341
- candidate: cand,
3463
+ candidate,
3342
3464
  reason: "cycle"
3343
3465
  });
3344
3466
  continue;
3345
3467
  }
3346
- if (!passesRate(s.ratePolicy, primaryRate(primary), cap.rate)) {
3468
+ if (!passesRate(s.ratePolicy, primaryRate(primary), capability.rate)) {
3347
3469
  out.push({
3348
3470
  scope: "node",
3349
3471
  nodeId,
3350
3472
  primary,
3351
- candidate: cand,
3473
+ candidate,
3352
3474
  reason: "rate_violation"
3353
3475
  });
3354
3476
  continue;
@@ -3358,58 +3480,55 @@ function collectFailedFallbacks(props, services, settings) {
3358
3480
  scope: "node",
3359
3481
  nodeId,
3360
3482
  primary,
3361
- candidate: cand,
3483
+ candidate,
3362
3484
  reason: "no_tag_context"
3363
3485
  });
3364
3486
  continue;
3365
3487
  }
3366
- let anyPass = false;
3367
- let anyFail = false;
3368
3488
  for (const tagId of tagContexts) {
3369
- const ok = s.requireConstraintFit ? satisfiesTagConstraints(tagId, { services, props }, cap) : true;
3370
- if (ok) anyPass = true;
3371
- else {
3372
- anyFail = true;
3373
- out.push({
3374
- scope: "node",
3375
- nodeId,
3376
- primary,
3377
- candidate: cand,
3378
- tagContext: tagId,
3379
- reason: "constraint_mismatch"
3380
- });
3381
- }
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
+ });
3382
3503
  }
3383
- void anyPass;
3384
- void anyFail;
3385
3504
  }
3386
3505
  }
3387
3506
  for (const [primary, list] of Object.entries((_c = fb.global) != null ? _c : {})) {
3388
- for (const cand of list) {
3389
- const cap = getCap(services, cand);
3390
- if (!cap) {
3507
+ for (const candidate of list) {
3508
+ const capability = getCap(services, candidate);
3509
+ if (!capability) {
3391
3510
  out.push({
3392
3511
  scope: "global",
3393
3512
  primary,
3394
- candidate: cand,
3513
+ candidate,
3395
3514
  reason: "unknown_service"
3396
3515
  });
3397
3516
  continue;
3398
3517
  }
3399
- if (String(cand) === String(primary)) {
3518
+ if (isSameServiceCapabilityRef(services, candidate, primary)) {
3400
3519
  out.push({
3401
3520
  scope: "global",
3402
3521
  primary,
3403
- candidate: cand,
3522
+ candidate,
3404
3523
  reason: "cycle"
3405
3524
  });
3406
3525
  continue;
3407
3526
  }
3408
- if (!passesRate(s.ratePolicy, primaryRate(primary), cap.rate)) {
3527
+ if (!passesRate(s.ratePolicy, primaryRate(primary), capability.rate)) {
3409
3528
  out.push({
3410
3529
  scope: "global",
3411
3530
  primary,
3412
- candidate: cand,
3531
+ candidate,
3413
3532
  reason: "rate_violation"
3414
3533
  });
3415
3534
  }
@@ -3418,52 +3537,61 @@ function collectFailedFallbacks(props, services, settings) {
3418
3537
  return out;
3419
3538
  }
3420
3539
  function rateOf(map, id) {
3421
- var _a;
3540
+ var _a, _b;
3422
3541
  if (id === void 0 || id === null) return void 0;
3423
- const c = getCap(map, id);
3424
- 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;
3425
3543
  }
3426
- function passesRate(policy, primaryRate, candRate) {
3427
- if (typeof candRate !== "number" || !Number.isFinite(candRate))
3544
+ function passesRate(policy, primaryRate, candidateRate) {
3545
+ if (typeof candidateRate !== "number" || !Number.isFinite(candidateRate)) {
3428
3546
  return false;
3429
- if (typeof primaryRate !== "number" || !Number.isFinite(primaryRate))
3547
+ }
3548
+ if (typeof primaryRate !== "number" || !Number.isFinite(primaryRate)) {
3430
3549
  return false;
3431
- return passesRatePolicy(normalizeRatePolicy(policy), primaryRate, candRate);
3550
+ }
3551
+ return passesRatePolicy(
3552
+ normalizeRatePolicy(policy),
3553
+ primaryRate,
3554
+ candidateRate
3555
+ );
3432
3556
  }
3433
3557
  function getCap(map, id) {
3434
3558
  return getServiceCapability(map, id);
3435
3559
  }
3436
- function isCapFlagEnabled(cap, flagId) {
3560
+ function isCapFlagEnabled(capability, flagId) {
3437
3561
  var _a, _b;
3438
- 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;
3439
3563
  if (fromFlags === true) return true;
3440
3564
  if (fromFlags === false) return false;
3441
- const legacy = cap[flagId];
3565
+ const legacy = capability[flagId];
3442
3566
  return legacy === true;
3443
3567
  }
3444
- function satisfiesTagConstraints(tagId, ctx, cap) {
3445
- const tag = ctx.props.filters.find((t) => t.id === tagId);
3446
- const eff = tag == null ? void 0 : tag.constraints;
3447
- if (!eff) return true;
3448
- for (const [key, value] of Object.entries(eff)) {
3449
- 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)) {
3450
3574
  return false;
3451
3575
  }
3452
3576
  }
3453
3577
  return true;
3454
3578
  }
3455
3579
  function primaryForNode(props, nodeId) {
3456
- const tag = props.filters.find((t) => t.id === nodeId);
3580
+ const tag = props.filters.find((item) => item.id === nodeId);
3457
3581
  if (tag) {
3458
3582
  return { primary: tag.service_id, tagContexts: [tag.id] };
3459
3583
  }
3460
3584
  const field = props.fields.find(
3461
- (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)
3462
3586
  );
3463
- if (!field) return { tagContexts: [], reasonNoPrimary: "no_parent_field" };
3464
- const opt = field.options.find((o) => o.id === nodeId);
3465
- const contexts = bindIdsToArray(field.bind_id);
3466
- 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
+ };
3467
3595
  }
3468
3596
  function bindIdsToArray(bind) {
3469
3597
  if (!bind) return [];
@@ -3473,43 +3601,54 @@ function getEligibleFallbacks(params) {
3473
3601
  var _a, _b, _c, _d, _e, _f;
3474
3602
  const s = { ...DEFAULT_SETTINGS, ...(_a = params.settings) != null ? _a : {} };
3475
3603
  const { primary, nodeId, tagId, services } = params;
3476
- const fb = (_b = params.fallbacks) != null ? _b : {};
3477
- const excludes = new Set(((_c = params.exclude) != null ? _c : []).map(String));
3478
- excludes.add(String(primary));
3479
- const unique = (_d = params.unique) != null ? _d : true;
3480
- const lists = [];
3481
- if (nodeId && ((_e = fb.nodes) == null ? void 0 : _e[nodeId])) lists.push(fb.nodes[nodeId]);
3482
- if ((_f = fb.global) == null ? void 0 : _f[primary]) lists.push(fb.global[primary]);
3483
- 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 [];
3484
3617
  const primaryRate = rateOf(services, primary);
3485
3618
  const seen = /* @__PURE__ */ new Set();
3486
3619
  const eligible = [];
3487
- for (const list of lists) {
3488
- for (const cand of list) {
3489
- const key = String(cand);
3490
- if (excludes.has(key)) continue;
3491
- if (unique && seen.has(key)) continue;
3492
- seen.add(key);
3493
- const cap = getCap(services, cand);
3494
- if (!cap) continue;
3495
- 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
+ }
3496
3635
  if (s.requireConstraintFit && tagId) {
3497
- const ok = satisfiesTagConstraints(
3636
+ const fitsConstraints = satisfiesTagConstraints(
3498
3637
  tagId,
3499
3638
  { props: params.props, services },
3500
- cap
3639
+ capability
3501
3640
  );
3502
- if (!ok) continue;
3641
+ if (!fitsConstraints) continue;
3503
3642
  }
3504
- eligible.push(cand);
3643
+ eligible.push(candidateId);
3505
3644
  }
3506
3645
  }
3507
3646
  if (s.selectionStrategy === "cheapest") {
3508
- eligible.sort((a, b) => {
3647
+ eligible.sort((left, right) => {
3509
3648
  var _a2, _b2;
3510
- const ra = (_a2 = rateOf(services, a)) != null ? _a2 : Infinity;
3511
- const rb = (_b2 = rateOf(services, b)) != null ? _b2 : Infinity;
3512
- 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;
3513
3652
  });
3514
3653
  }
3515
3654
  if (typeof params.limit === "number" && params.limit >= 0) {
@@ -3517,10 +3656,104 @@ function getEligibleFallbacks(params) {
3517
3656
  }
3518
3657
  return eligible;
3519
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
+ }
3520
3699
  function getFallbackRegistrationInfo(props, nodeId) {
3521
3700
  const { primary, tagContexts } = primaryForNode(props, nodeId);
3522
3701
  return { primary, tagContexts };
3523
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
+ }
3524
3757
 
3525
3758
  // src/core/tag-relations.ts
3526
3759
  var toId = (x) => typeof x === "string" ? x : x.id;
@@ -4139,18 +4372,23 @@ function compilePolicies(raw) {
4139
4372
 
4140
4373
  // src/core/service-filter.ts
4141
4374
  function filterServicesForVisibleGroup(input, deps) {
4142
- var _a, _b, _c, _d, _e, _f;
4375
+ var _a, _b, _c, _d, _e, _f, _g, _h, _i, _j, _k;
4143
4376
  const svcMap = (_c = (_b = (_a = deps.builder).getServiceMap) == null ? void 0 : _b.call(_a)) != null ? _c : {};
4377
+ const builderOptions = (_e = (_d = deps.builder).getOptions) == null ? void 0 : _e.call(_d);
4144
4378
  const { context } = input;
4145
4379
  const usedSet = new Set(context.usedServiceIds.map(String));
4146
4380
  const primary = context.usedServiceIds[0];
4381
+ const explicitFallbackSettings = (_f = context.fallbackSettings) != null ? _f : context.fallback;
4382
+ const resolvedRatePolicy = normalizeRatePolicy(
4383
+ (_h = (_g = context.ratePolicy) != null ? _g : explicitFallbackSettings == null ? void 0 : explicitFallbackSettings.ratePolicy) != null ? _h : builderOptions == null ? void 0 : builderOptions.ratePolicy
4384
+ );
4385
+ const fallbackSettingsSource = explicitFallbackSettings != null ? explicitFallbackSettings : builderOptions == null ? void 0 : builderOptions.fallbackSettings;
4147
4386
  const fb = {
4148
- requireConstraintFit: true,
4149
- ratePolicy: { kind: "lte_primary", pct: 5 },
4150
- selectionStrategy: "priority",
4151
- mode: "strict",
4152
- ...(_d = context.fallback) != null ? _d : {}
4387
+ ...DEFAULT_FALLBACK_SETTINGS,
4388
+ ...fallbackSettingsSource != null ? fallbackSettingsSource : {},
4389
+ ratePolicy: resolvedRatePolicy
4153
4390
  };
4391
+ const policySource = (_j = (_i = context.policies) != null ? _i : builderOptions == null ? void 0 : builderOptions.policies) != null ? _j : [];
4154
4392
  const visibleServiceIds = context.selectedButtons === void 0 ? void 0 : collectVisibleServiceIds(
4155
4393
  deps.builder,
4156
4394
  context.tagId,
@@ -4175,11 +4413,11 @@ function filterServicesForVisibleGroup(input, deps) {
4175
4413
  const fitsConstraints = constraintFitOk(
4176
4414
  svcMap,
4177
4415
  cap.id,
4178
- (_e = context.effectiveConstraints) != null ? _e : {}
4416
+ (_k = context.effectiveConstraints) != null ? _k : {}
4179
4417
  );
4180
4418
  const passesRate2 = primary == null ? true : rateOk(svcMap, id, primary, fb);
4181
4419
  const polRes = evaluatePoliciesRaw(
4182
- (_f = context.policies) != null ? _f : [],
4420
+ policySource,
4183
4421
  [...context.usedServiceIds, id],
4184
4422
  svcMap,
4185
4423
  context.tagId,
@@ -5054,6 +5292,7 @@ function createFallbackEditor(options = {}) {
5054
5292
  const original = cloneFallbacks(options.fallbacks);
5055
5293
  let current = cloneFallbacks(options.fallbacks);
5056
5294
  const props = options.props;
5295
+ const snapshot = options.snapshot;
5057
5296
  const services = (_a = options.services) != null ? _a : {};
5058
5297
  const settings = (_b = options.settings) != null ? _b : {};
5059
5298
  function state() {
@@ -5148,7 +5387,11 @@ function createFallbackEditor(options = {}) {
5148
5387
  const allowed2 = [];
5149
5388
  for (const candidate of normalized) {
5150
5389
  const reasons = [];
5151
- if (String(candidate) === String(context.primary)) {
5390
+ if (isSameServiceCapabilityRef(
5391
+ services,
5392
+ candidate,
5393
+ context.primary
5394
+ )) {
5152
5395
  reasons.push("self_reference");
5153
5396
  }
5154
5397
  if (reasons.length) {
@@ -5236,7 +5479,19 @@ function createFallbackEditor(options = {}) {
5236
5479
  return writeScope(context, []);
5237
5480
  }
5238
5481
  function eligible(context, opt) {
5482
+ var _a2, _b2;
5239
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
+ );
5240
5495
  if (context.scope === "global") {
5241
5496
  return getEligibleFallbacks({
5242
5497
  primary: context.primary,
@@ -5244,9 +5499,10 @@ function createFallbackEditor(options = {}) {
5244
5499
  fallbacks: current,
5245
5500
  settings,
5246
5501
  props,
5247
- exclude: opt == null ? void 0 : opt.exclude,
5502
+ exclude: exclude2,
5248
5503
  unique: opt == null ? void 0 : opt.unique,
5249
- limit: opt == null ? void 0 : opt.limit
5504
+ limit: opt == null ? void 0 : opt.limit,
5505
+ source
5250
5506
  });
5251
5507
  }
5252
5508
  const info = getFallbackRegistrationInfo(props, context.nodeId);
@@ -5254,14 +5510,19 @@ function createFallbackEditor(options = {}) {
5254
5510
  return getEligibleFallbacks({
5255
5511
  primary: info.primary,
5256
5512
  nodeId: context.nodeId,
5257
- tagId: info.tagContexts[0],
5513
+ tagId: resolveNodeTagContext({
5514
+ nodeId: context.nodeId,
5515
+ snapshot,
5516
+ fallbackTagContexts: info.tagContexts
5517
+ }),
5258
5518
  services,
5259
5519
  fallbacks: current,
5260
5520
  settings,
5261
5521
  props,
5262
- exclude: opt == null ? void 0 : opt.exclude,
5522
+ exclude: exclude2,
5263
5523
  unique: opt == null ? void 0 : opt.unique,
5264
- limit: opt == null ? void 0 : opt.limit
5524
+ limit: opt == null ? void 0 : opt.limit,
5525
+ source
5265
5526
  });
5266
5527
  }
5267
5528
  function writeScope(context, nextList) {
@@ -5319,14 +5580,14 @@ function sameFallbacks(a, b) {
5319
5580
  function normalizeCandidateList(input, preserveOrder) {
5320
5581
  const out = [];
5321
5582
  for (const item of input != null ? input : []) {
5322
- if (!isValidServiceIdRef(item)) continue;
5583
+ if (!isValidServiceIdRef2(item)) continue;
5323
5584
  const exists = out.some((x) => String(x) === String(item));
5324
5585
  if (exists) continue;
5325
5586
  out.push(item);
5326
5587
  }
5327
5588
  return preserveOrder ? out : out;
5328
5589
  }
5329
- function isValidServiceIdRef(value) {
5590
+ function isValidServiceIdRef2(value) {
5330
5591
  return typeof value === "number" && Number.isFinite(value) || typeof value === "string" && value.trim().length > 0;
5331
5592
  }
5332
5593
  function clamp(n, min, max) {
@@ -5335,7 +5596,7 @@ function clamp(n, min, max) {
5335
5596
  function getNodeRegistrationInfo(props, nodeId) {
5336
5597
  const tag = props.filters.find((t) => t.id === nodeId);
5337
5598
  if (tag) {
5338
- if (!isValidServiceIdRef(tag.service_id)) {
5599
+ if (!isValidServiceIdRef2(tag.service_id)) {
5339
5600
  return { ok: false, reasons: ["no_primary"] };
5340
5601
  }
5341
5602
  return {
@@ -5348,7 +5609,7 @@ function getNodeRegistrationInfo(props, nodeId) {
5348
5609
  if (!hit) {
5349
5610
  return { ok: false, reasons: ["node_not_found"] };
5350
5611
  }
5351
- if (!isValidServiceIdRef(hit.option.service_id)) {
5612
+ if (!isValidServiceIdRef2(hit.option.service_id)) {
5352
5613
  return { ok: false, reasons: ["no_primary"] };
5353
5614
  }
5354
5615
  return {
@@ -5370,6 +5631,15 @@ function bindIdsToArray2(v) {
5370
5631
  if (Array.isArray(v)) return v.filter(Boolean);
5371
5632
  return v ? [v] : [];
5372
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
+ }
5373
5643
  function mapDiagReason(reason) {
5374
5644
  switch (String(reason)) {
5375
5645
  case "unknown_service":
@@ -6923,6 +7193,8 @@ function filterServicesForVisibleGroup2(ctx, candidates, input) {
6923
7193
  usedServiceIds: input.usedServiceIds,
6924
7194
  effectiveConstraints: input.effectiveConstraints,
6925
7195
  policies: input.policies,
7196
+ ratePolicy: input.ratePolicy,
7197
+ fallbackSettings: input.fallbackSettings,
6926
7198
  fallback: input.fallback
6927
7199
  }
6928
7200
  };
@@ -12069,10 +12341,10 @@ function FallbackEditorInner({ className }) {
12069
12341
  "div",
12070
12342
  {
12071
12343
  className: [
12072
- "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",
12073
12345
  className
12074
12346
  ].filter(Boolean).join(" "),
12075
- 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: [
12076
12348
  /* @__PURE__ */ jsx6(
12077
12349
  FallbackEditorHeader,
12078
12350
  {
@@ -12084,9 +12356,9 @@ function FallbackEditorInner({ className }) {
12084
12356
  saving: headerSaving
12085
12357
  }
12086
12358
  ),
12087
- /* @__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: [
12088
12360
  /* @__PURE__ */ jsx6(FallbackServiceSidebar, {}),
12089
- /* @__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: [
12090
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: [
12091
12363
  /* @__PURE__ */ jsxs2("div", { children: [
12092
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" }),
@@ -12230,7 +12502,7 @@ function FallbackDetailsPanel() {
12230
12502
  () => services.find((s) => String(s.id) === String(activeServiceId)),
12231
12503
  [services, activeServiceId]
12232
12504
  );
12233
- return /* @__PURE__ */ jsxs4("aside", { className: "flex min-h-0 flex-col gap-4", children: [
12505
+ return /* @__PURE__ */ jsxs4("aside", { className: "flex min-h-0 flex-col gap-4 overflow-y-auto", children: [
12234
12506
  /* @__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: [
12235
12507
  /* @__PURE__ */ jsx8("h3", { className: "text-sm font-semibold text-zinc-900 dark:text-zinc-100", children: "Primary service info" }),
12236
12508
  !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: [
@@ -12392,7 +12664,7 @@ function FallbackSettingsPanel() {
12392
12664
  kind: "lte_primary",
12393
12665
  pct: 5
12394
12666
  };
12395
- 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: [
12667
+ 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 overflow-y-auto", children: [
12396
12668
  /* @__PURE__ */ jsxs6("div", { className: "mb-4 flex items-start justify-between gap-3", children: [
12397
12669
  /* @__PURE__ */ jsxs6("div", { children: [
12398
12670
  /* @__PURE__ */ jsx10("h3", { className: "text-sm font-semibold text-zinc-900 dark:text-zinc-100", children: "Fallback settings" }),
@@ -12591,7 +12863,7 @@ function FallbackServiceSidebar() {
12591
12863
  }
12592
12864
  );
12593
12865
  }, [query, services]);
12594
- 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: [
12866
+ 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: [
12595
12867
  /* @__PURE__ */ jsxs7("div", { className: "border-b border-zinc-200 p-4 dark:border-zinc-800", children: [
12596
12868
  /* @__PURE__ */ jsx11("h2", { className: "text-sm font-semibold text-zinc-900 dark:text-zinc-100", children: "Primary services" }),
12597
12869
  /* @__PURE__ */ jsx11("p", { className: "mt-1 text-xs text-zinc-500 dark:text-zinc-400", children: "Services currently active in the builder/runtime context." })
@@ -12606,7 +12878,7 @@ function FallbackServiceSidebar() {
12606
12878
  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"
12607
12879
  }
12608
12880
  ),
12609
- /* @__PURE__ */ jsx11("div", { className: "mt-3 flex-1 space-y-2 overflow-auto", children: filtered.map((service) => {
12881
+ /* @__PURE__ */ jsx11("div", { className: "mt-3 flex-1 space-y-2 overflow-y-auto", children: filtered.map((service) => {
12610
12882
  var _a, _b;
12611
12883
  const active = String(service.id) === String(activeServiceId);
12612
12884
  const count = get(service.id).length;
@@ -12647,11 +12919,172 @@ function FallbackServiceSidebar() {
12647
12919
  }
12648
12920
 
12649
12921
  // src/react/fallback-editor/FallbackRegistrationsPanel.tsx
12650
- import React13 from "react";
12922
+ import React11 from "react";
12923
+ import { Fragment, jsx as jsx12, jsxs as jsxs8 } from "react/jsx-runtime";
12924
+ function FallbackRegistrationsPanel() {
12925
+ const { activeServiceId, remove: remove2, clear, check } = useFallbackEditor();
12926
+ const registrations = useActiveFallbackRegistrations();
12927
+ const eligibleServices = useEligibleServiceList();
12928
+ const [candidatePickerOpen, setCandidatePickerOpen] = React11.useState(false);
12929
+ const [candidateContext, setCandidateContext] = React11.useState(null);
12930
+ const [candidatePrimaryId, setCandidatePrimaryId] = React11.useState(void 0);
12931
+ const [registrationDialogOpen, setRegistrationDialogOpen] = React11.useState(false);
12932
+ const makeContext = React11.useCallback(
12933
+ (registration) => {
12934
+ if (registration.scope === "global") {
12935
+ return {
12936
+ scope: "global",
12937
+ primary: registration.primary
12938
+ };
12939
+ }
12940
+ return {
12941
+ scope: "node",
12942
+ nodeId: registration.scopeId
12943
+ };
12944
+ },
12945
+ []
12946
+ );
12947
+ const openCandidatePicker = React11.useCallback(
12948
+ (context, primaryId) => {
12949
+ setCandidateContext(context);
12950
+ setCandidatePrimaryId(primaryId);
12951
+ setCandidatePickerOpen(true);
12952
+ },
12953
+ []
12954
+ );
12955
+ if (activeServiceId === void 0 || activeServiceId === null) {
12956
+ 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." }) });
12957
+ }
12958
+ return /* @__PURE__ */ jsxs8(Fragment, { children: [
12959
+ /* @__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: [
12960
+ /* @__PURE__ */ jsxs8("div", { className: "mb-4 flex items-start justify-between gap-3", children: [
12961
+ /* @__PURE__ */ jsxs8("div", { children: [
12962
+ /* @__PURE__ */ jsx12("h3", { className: "text-sm font-semibold text-zinc-900 dark:text-zinc-100", children: "Registered fallbacks" }),
12963
+ /* @__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." })
12964
+ ] }),
12965
+ /* @__PURE__ */ jsx12(
12966
+ "button",
12967
+ {
12968
+ type: "button",
12969
+ onClick: () => setRegistrationDialogOpen(true),
12970
+ 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",
12971
+ children: "Add registration"
12972
+ }
12973
+ )
12974
+ ] }),
12975
+ /* @__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) => {
12976
+ var _a;
12977
+ const context = makeContext(reg);
12978
+ const candidates = reg.services;
12979
+ return /* @__PURE__ */ jsxs8(
12980
+ "div",
12981
+ {
12982
+ className: "rounded-2xl border border-zinc-200 bg-zinc-50 p-4 dark:border-zinc-800 dark:bg-zinc-950",
12983
+ children: [
12984
+ /* @__PURE__ */ jsxs8("div", { className: "flex flex-wrap items-start justify-between gap-3", children: [
12985
+ /* @__PURE__ */ jsxs8("div", { children: [
12986
+ /* @__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}` }),
12987
+ /* @__PURE__ */ jsxs8("div", { className: "mt-1 text-xs text-zinc-500 dark:text-zinc-400", children: [
12988
+ "Primary #",
12989
+ String(reg.primary)
12990
+ ] })
12991
+ ] }),
12992
+ /* @__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: [
12993
+ reg.scope,
12994
+ reg.scopeId ? ` \xB7 ${reg.scopeId}` : ""
12995
+ ] })
12996
+ ] }),
12997
+ /* @__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) => {
12998
+ var _a2;
12999
+ const preview = check(context, [
13000
+ candidate
13001
+ ]);
13002
+ const rejected = preview.rejected[0];
13003
+ 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";
13004
+ const service = eligibleServices.find(
13005
+ (s) => String(s.id) === String(candidate)
13006
+ );
13007
+ return /* @__PURE__ */ jsxs8(
13008
+ "div",
13009
+ {
13010
+ className: `inline-flex items-center gap-2 rounded-xl border px-3 py-2 text-xs ${tone}`,
13011
+ children: [
13012
+ /* @__PURE__ */ jsx12("span", { children: service ? `#${String(service.id)} \xB7 ${(_a2 = service.name) != null ? _a2 : "Unnamed"}` : `#${String(candidate)}` }),
13013
+ rejected ? /* @__PURE__ */ jsx12("span", { className: "rounded-full border border-current/20 px-2 py-0.5 text-[10px]", children: rejected.reasons.join(
13014
+ ", "
13015
+ ) }) : /* @__PURE__ */ jsx12("span", { className: "rounded-full border border-current/20 px-2 py-0.5 text-[10px]", children: "valid" }),
13016
+ /* @__PURE__ */ jsx12(
13017
+ "button",
13018
+ {
13019
+ type: "button",
13020
+ onClick: () => remove2(
13021
+ context,
13022
+ candidate
13023
+ ),
13024
+ className: "text-current/70 hover:text-current",
13025
+ children: "\xD7"
13026
+ }
13027
+ )
13028
+ ]
13029
+ },
13030
+ String(candidate)
13031
+ );
13032
+ }) }),
13033
+ /* @__PURE__ */ jsxs8("div", { className: "mt-4 flex gap-2", children: [
13034
+ /* @__PURE__ */ jsx12(
13035
+ "button",
13036
+ {
13037
+ type: "button",
13038
+ onClick: () => openCandidatePicker(
13039
+ context,
13040
+ reg.primary
13041
+ ),
13042
+ 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",
13043
+ children: "Add fallback"
13044
+ }
13045
+ ),
13046
+ /* @__PURE__ */ jsx12(
13047
+ "button",
13048
+ {
13049
+ type: "button",
13050
+ onClick: () => clear(context),
13051
+ 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",
13052
+ children: "Clear"
13053
+ }
13054
+ )
13055
+ ] })
13056
+ ]
13057
+ },
13058
+ `${reg.scope}:${String((_a = reg.scopeId) != null ? _a : "global")}:${index}`
13059
+ );
13060
+ }) })
13061
+ ] }),
13062
+ /* @__PURE__ */ jsx12(
13063
+ FallbackAddRegistrationDialog,
13064
+ {
13065
+ open: registrationDialogOpen,
13066
+ onClose: () => setRegistrationDialogOpen(false),
13067
+ onSelect: (context, primaryId) => {
13068
+ setRegistrationDialogOpen(false);
13069
+ openCandidatePicker(context, primaryId);
13070
+ }
13071
+ }
13072
+ ),
13073
+ /* @__PURE__ */ jsx12(
13074
+ FallbackAddCandidatesDialog,
13075
+ {
13076
+ open: candidatePickerOpen,
13077
+ onClose: () => setCandidatePickerOpen(false),
13078
+ context: candidateContext,
13079
+ primaryId: candidatePrimaryId
13080
+ }
13081
+ )
13082
+ ] });
13083
+ }
12651
13084
 
12652
13085
  // src/react/fallback-editor/FallbackAddCandidatesDialog.tsx
12653
- import React11 from "react";
12654
- import { jsx as jsx12, jsxs as jsxs8 } from "react/jsx-runtime";
13086
+ import React12 from "react";
13087
+ import { jsx as jsx13, jsxs as jsxs9 } from "react/jsx-runtime";
12655
13088
  function FallbackAddCandidatesDialog({
12656
13089
  open,
12657
13090
  onClose,
@@ -12660,23 +13093,23 @@ function FallbackAddCandidatesDialog({
12660
13093
  }) {
12661
13094
  const { eligible, addMany } = useFallbackEditor();
12662
13095
  const eligibleServices = useEligibleServiceList();
12663
- const [query, setQuery] = React11.useState("");
12664
- const [filterEligibleOnly, setFilterEligibleOnly] = React11.useState(true);
12665
- const [selected, setSelected] = React11.useState(/* @__PURE__ */ new Set());
12666
- const [submitting, setSubmitting] = React11.useState(false);
12667
- React11.useEffect(() => {
13096
+ const [query, setQuery] = React12.useState("");
13097
+ const [filterEligibleOnly, setFilterEligibleOnly] = React12.useState(true);
13098
+ const [selected, setSelected] = React12.useState(/* @__PURE__ */ new Set());
13099
+ const [submitting, setSubmitting] = React12.useState(false);
13100
+ React12.useEffect(() => {
12668
13101
  if (!open) {
12669
13102
  setQuery("");
12670
13103
  setFilterEligibleOnly(true);
12671
13104
  setSelected(/* @__PURE__ */ new Set());
12672
13105
  }
12673
13106
  }, [open]);
12674
- const allowedIds = React11.useMemo(() => {
13107
+ const allowedIds = React12.useMemo(() => {
12675
13108
  if (!context) return null;
12676
13109
  if (!filterEligibleOnly) return null;
12677
13110
  return new Set(eligible(context).map((id) => String(id)));
12678
13111
  }, [context, filterEligibleOnly, eligible]);
12679
- const items = React11.useMemo(() => {
13112
+ const items = React12.useMemo(() => {
12680
13113
  const q = query.trim().toLowerCase();
12681
13114
  return eligibleServices.filter((service) => {
12682
13115
  var _a, _b;
@@ -12711,13 +13144,13 @@ function FallbackAddCandidatesDialog({
12711
13144
  }
12712
13145
  }
12713
13146
  if (!open || !context) return null;
12714
- 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: [
12715
- /* @__PURE__ */ jsxs8("div", { className: "border-b border-zinc-200 p-4 dark:border-zinc-800", children: [
12716
- /* @__PURE__ */ jsx12("h3", { className: "text-base font-semibold text-zinc-900 dark:text-zinc-100", children: "Add fallback services" }),
12717
- /* @__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." })
13147
+ 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: "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: [
13148
+ /* @__PURE__ */ jsxs9("div", { className: "border-b border-zinc-200 p-4 dark:border-zinc-800", children: [
13149
+ /* @__PURE__ */ jsx13("h3", { className: "text-base font-semibold text-zinc-900 dark:text-zinc-100", children: "Add fallback services" }),
13150
+ /* @__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." })
12718
13151
  ] }),
12719
- /* @__PURE__ */ jsxs8("div", { className: "flex flex-col gap-3 p-4", children: [
12720
- /* @__PURE__ */ jsx12(
13152
+ /* @__PURE__ */ jsxs9("div", { className: "flex flex-col gap-3 p-4", children: [
13153
+ /* @__PURE__ */ jsx13(
12721
13154
  "input",
12722
13155
  {
12723
13156
  value: query,
@@ -12726,8 +13159,8 @@ function FallbackAddCandidatesDialog({
12726
13159
  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"
12727
13160
  }
12728
13161
  ),
12729
- /* @__PURE__ */ jsxs8("label", { className: "inline-flex items-center gap-2 text-sm text-zinc-700 dark:text-zinc-300", children: [
12730
- /* @__PURE__ */ jsx12(
13162
+ /* @__PURE__ */ jsxs9("label", { className: "inline-flex items-center gap-2 text-sm text-zinc-700 dark:text-zinc-300", children: [
13163
+ /* @__PURE__ */ jsx13(
12731
13164
  "input",
12732
13165
  {
12733
13166
  type: "checkbox",
@@ -12738,7 +13171,7 @@ function FallbackAddCandidatesDialog({
12738
13171
  ),
12739
13172
  "Filter eligible only"
12740
13173
  ] }),
12741
- /* @__PURE__ */ jsx12(
13174
+ /* @__PURE__ */ jsx13(
12742
13175
  VirtualServiceList,
12743
13176
  {
12744
13177
  items,
@@ -12748,13 +13181,13 @@ function FallbackAddCandidatesDialog({
12748
13181
  }
12749
13182
  )
12750
13183
  ] }),
12751
- /* @__PURE__ */ jsxs8("div", { className: "flex items-center justify-between border-t border-zinc-200 p-4 dark:border-zinc-800", children: [
12752
- /* @__PURE__ */ jsxs8("div", { className: "text-sm text-zinc-500 dark:text-zinc-400", children: [
13184
+ /* @__PURE__ */ jsxs9("div", { className: "flex items-center justify-between border-t border-zinc-200 p-4 dark:border-zinc-800", children: [
13185
+ /* @__PURE__ */ jsxs9("div", { className: "text-sm text-zinc-500 dark:text-zinc-400", children: [
12753
13186
  selected.size,
12754
13187
  " selected"
12755
13188
  ] }),
12756
- /* @__PURE__ */ jsxs8("div", { className: "flex gap-2", children: [
12757
- /* @__PURE__ */ jsx12(
13189
+ /* @__PURE__ */ jsxs9("div", { className: "flex gap-2", children: [
13190
+ /* @__PURE__ */ jsx13(
12758
13191
  "button",
12759
13192
  {
12760
13193
  type: "button",
@@ -12763,7 +13196,7 @@ function FallbackAddCandidatesDialog({
12763
13196
  children: "Cancel"
12764
13197
  }
12765
13198
  ),
12766
- /* @__PURE__ */ jsx12(
13199
+ /* @__PURE__ */ jsx13(
12767
13200
  "button",
12768
13201
  {
12769
13202
  type: "button",
@@ -12779,8 +13212,8 @@ function FallbackAddCandidatesDialog({
12779
13212
  }
12780
13213
 
12781
13214
  // src/react/fallback-editor/FallbackAddRegistrationDialog.tsx
12782
- import React12 from "react";
12783
- import { jsx as jsx13, jsxs as jsxs9 } from "react/jsx-runtime";
13215
+ import React13 from "react";
13216
+ import { jsx as jsx14, jsxs as jsxs10 } from "react/jsx-runtime";
12784
13217
  function FallbackAddRegistrationDialog({
12785
13218
  open,
12786
13219
  onClose,
@@ -12788,23 +13221,23 @@ function FallbackAddRegistrationDialog({
12788
13221
  }) {
12789
13222
  const { activeServiceId, serviceProps, snapshot } = useFallbackEditor();
12790
13223
  const registrations = useActiveFallbackRegistrations();
12791
- const [scope, setScope] = React12.useState("global");
12792
- const [nodeId, setNodeId] = React12.useState("");
12793
- const mode = React12.useMemo(() => {
13224
+ const [scope, setScope] = React13.useState("global");
13225
+ const [nodeId, setNodeId] = React13.useState("");
13226
+ const mode = React13.useMemo(() => {
12794
13227
  if (snapshot) return "snapshot";
12795
13228
  if (serviceProps) return "props";
12796
13229
  return "none";
12797
13230
  }, [snapshot, serviceProps]);
12798
- React12.useEffect(() => {
13231
+ React13.useEffect(() => {
12799
13232
  if (open) {
12800
13233
  setScope("global");
12801
13234
  setNodeId("");
12802
13235
  }
12803
13236
  }, [open]);
12804
- const hasGlobal = React12.useMemo(() => {
13237
+ const hasGlobal = React13.useMemo(() => {
12805
13238
  return registrations.some((r) => r.scope === "global");
12806
13239
  }, [registrations]);
12807
- const nodeTargets = React12.useMemo(() => {
13240
+ const nodeTargets = React13.useMemo(() => {
12808
13241
  var _a, _b, _c, _d, _e, _f, _g, _h, _i, _j, _k;
12809
13242
  if (activeServiceId === void 0 || activeServiceId === null) {
12810
13243
  return [];
@@ -12894,12 +13327,12 @@ function FallbackAddRegistrationDialog({
12894
13327
  }
12895
13328
  return [];
12896
13329
  }, [mode, snapshot, serviceProps, activeServiceId]);
12897
- React12.useEffect(() => {
13330
+ React13.useEffect(() => {
12898
13331
  if (hasGlobal && scope === "global") {
12899
13332
  setScope("node");
12900
13333
  }
12901
13334
  }, [hasGlobal, scope]);
12902
- React12.useEffect(() => {
13335
+ React13.useEffect(() => {
12903
13336
  if (scope === "node" && nodeId) {
12904
13337
  const exists = nodeTargets.some((node) => node.id === nodeId);
12905
13338
  if (!exists) setNodeId("");
@@ -12930,17 +13363,17 @@ function FallbackAddRegistrationDialog({
12930
13363
  }
12931
13364
  if (!open) return null;
12932
13365
  const nodeScopeDisabled = nodeTargets.length === 0;
12933
- 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: [
12934
- /* @__PURE__ */ jsxs9("div", { className: "border-b border-zinc-200 p-4 dark:border-zinc-800", children: [
12935
- /* @__PURE__ */ jsx13("h3", { className: "text-base font-semibold text-zinc-900 dark:text-zinc-100", children: "Add registration" }),
12936
- /* @__PURE__ */ jsx13("p", { className: "mt-1 text-sm text-zinc-500 dark:text-zinc-400", children: "Choose the registration scope before selecting fallback candidates." })
13366
+ return /* @__PURE__ */ jsx14("div", { className: "fixed inset-0 z-50 flex items-center justify-center bg-black/50 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: [
13367
+ /* @__PURE__ */ jsxs10("div", { className: "border-b border-zinc-200 p-4 dark:border-zinc-800", children: [
13368
+ /* @__PURE__ */ jsx14("h3", { className: "text-base font-semibold text-zinc-900 dark:text-zinc-100", children: "Add registration" }),
13369
+ /* @__PURE__ */ jsx14("p", { className: "mt-1 text-sm text-zinc-500 dark:text-zinc-400", children: "Choose the registration scope before selecting fallback candidates." })
12937
13370
  ] }),
12938
- /* @__PURE__ */ jsxs9("div", { className: "space-y-4 p-4", children: [
12939
- /* @__PURE__ */ jsxs9("div", { className: "space-y-2", children: [
12940
- /* @__PURE__ */ jsx13("div", { className: "text-sm font-medium text-zinc-900 dark:text-zinc-100", children: "Scope" }),
12941
- /* @__PURE__ */ jsxs9("div", { className: "grid gap-2", children: [
12942
- !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: [
12943
- /* @__PURE__ */ jsx13(
13371
+ /* @__PURE__ */ jsxs10("div", { className: "space-y-4 p-4", children: [
13372
+ /* @__PURE__ */ jsxs10("div", { className: "space-y-2", children: [
13373
+ /* @__PURE__ */ jsx14("div", { className: "text-sm font-medium text-zinc-900 dark:text-zinc-100", children: "Scope" }),
13374
+ /* @__PURE__ */ jsxs10("div", { className: "grid gap-2", children: [
13375
+ !hasGlobal && /* @__PURE__ */ jsxs10("label", { className: "flex cursor-pointer items-start gap-3 rounded-xl border border-zinc-200 p-3 dark:border-zinc-800", children: [
13376
+ /* @__PURE__ */ jsx14(
12944
13377
  "input",
12945
13378
  {
12946
13379
  type: "radio",
@@ -12950,12 +13383,12 @@ function FallbackAddRegistrationDialog({
12950
13383
  className: "mt-1 h-4 w-4"
12951
13384
  }
12952
13385
  ),
12953
- /* @__PURE__ */ jsxs9("div", { children: [
12954
- /* @__PURE__ */ jsx13("div", { className: "text-sm font-medium text-zinc-900 dark:text-zinc-100", children: "Global" }),
12955
- /* @__PURE__ */ jsx13("div", { className: "mt-1 text-xs text-zinc-500 dark:text-zinc-400", children: "Use one global registration for this primary service." })
13386
+ /* @__PURE__ */ jsxs10("div", { children: [
13387
+ /* @__PURE__ */ jsx14("div", { className: "text-sm font-medium text-zinc-900 dark:text-zinc-100", children: "Global" }),
13388
+ /* @__PURE__ */ jsx14("div", { className: "mt-1 text-xs text-zinc-500 dark:text-zinc-400", children: "Use one global registration for this primary service." })
12956
13389
  ] })
12957
13390
  ] }),
12958
- /* @__PURE__ */ jsxs9(
13391
+ /* @__PURE__ */ jsxs10(
12959
13392
  "label",
12960
13393
  {
12961
13394
  className: [
@@ -12963,7 +13396,7 @@ function FallbackAddRegistrationDialog({
12963
13396
  nodeScopeDisabled ? "cursor-not-allowed border-zinc-200 opacity-60 dark:border-zinc-800" : "cursor-pointer border-zinc-200 dark:border-zinc-800"
12964
13397
  ].join(" "),
12965
13398
  children: [
12966
- /* @__PURE__ */ jsx13(
13399
+ /* @__PURE__ */ jsx14(
12967
13400
  "input",
12968
13401
  {
12969
13402
  type: "radio",
@@ -12974,26 +13407,26 @@ function FallbackAddRegistrationDialog({
12974
13407
  className: "mt-1 h-4 w-4"
12975
13408
  }
12976
13409
  ),
12977
- /* @__PURE__ */ jsxs9("div", { children: [
12978
- /* @__PURE__ */ jsx13("div", { className: "text-sm font-medium text-zinc-900 dark:text-zinc-100", children: "Node" }),
12979
- /* @__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." })
13410
+ /* @__PURE__ */ jsxs10("div", { children: [
13411
+ /* @__PURE__ */ jsx14("div", { className: "text-sm font-medium text-zinc-900 dark:text-zinc-100", children: "Node" }),
13412
+ /* @__PURE__ */ jsx14("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." })
12980
13413
  ] })
12981
13414
  ]
12982
13415
  }
12983
13416
  )
12984
13417
  ] })
12985
13418
  ] }),
12986
- scope === "node" && /* @__PURE__ */ jsxs9("div", { className: "space-y-2", children: [
12987
- /* @__PURE__ */ jsx13("div", { className: "text-sm font-medium text-zinc-900 dark:text-zinc-100", children: "Node id" }),
12988
- /* @__PURE__ */ jsxs9(
13419
+ scope === "node" && /* @__PURE__ */ jsxs10("div", { className: "space-y-2", children: [
13420
+ /* @__PURE__ */ jsx14("div", { className: "text-sm font-medium text-zinc-900 dark:text-zinc-100", children: "Node id" }),
13421
+ /* @__PURE__ */ jsxs10(
12989
13422
  "select",
12990
13423
  {
12991
13424
  value: nodeId,
12992
13425
  onChange: (e) => setNodeId(e.target.value),
12993
13426
  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",
12994
13427
  children: [
12995
- /* @__PURE__ */ jsx13("option", { value: "", children: "Select node\u2026" }),
12996
- nodeTargets.map((node) => /* @__PURE__ */ jsxs9("option", { value: node.id, children: [
13428
+ /* @__PURE__ */ jsx14("option", { value: "", children: "Select node\u2026" }),
13429
+ nodeTargets.map((node) => /* @__PURE__ */ jsxs10("option", { value: node.id, children: [
12997
13430
  "[",
12998
13431
  node.kind,
12999
13432
  "] ",
@@ -13004,11 +13437,11 @@ function FallbackAddRegistrationDialog({
13004
13437
  ]
13005
13438
  }
13006
13439
  ),
13007
- 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
13440
+ 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
13008
13441
  ] })
13009
13442
  ] }),
13010
- /* @__PURE__ */ jsxs9("div", { className: "flex items-center justify-end gap-2 border-t border-zinc-200 p-4 dark:border-zinc-800", children: [
13011
- /* @__PURE__ */ jsx13(
13443
+ /* @__PURE__ */ jsxs10("div", { className: "flex items-center justify-end gap-2 border-t border-zinc-200 p-4 dark:border-zinc-800", children: [
13444
+ /* @__PURE__ */ jsx14(
13012
13445
  "button",
13013
13446
  {
13014
13447
  type: "button",
@@ -13017,7 +13450,7 @@ function FallbackAddRegistrationDialog({
13017
13450
  children: "Cancel"
13018
13451
  }
13019
13452
  ),
13020
- /* @__PURE__ */ jsx13(
13453
+ /* @__PURE__ */ jsx14(
13021
13454
  "button",
13022
13455
  {
13023
13456
  type: "button",
@@ -13060,169 +13493,6 @@ function resolveNodeMeta(props, nodeId) {
13060
13493
  }
13061
13494
  return { kind: "node", label: nodeId };
13062
13495
  }
13063
-
13064
- // src/react/fallback-editor/FallbackRegistrationsPanel.tsx
13065
- import { Fragment, jsx as jsx14, jsxs as jsxs10 } from "react/jsx-runtime";
13066
- function FallbackRegistrationsPanel() {
13067
- const { activeServiceId, remove: remove2, clear, check } = useFallbackEditor();
13068
- const registrations = useActiveFallbackRegistrations();
13069
- const eligibleServices = useEligibleServiceList();
13070
- const [candidatePickerOpen, setCandidatePickerOpen] = React13.useState(false);
13071
- const [candidateContext, setCandidateContext] = React13.useState(null);
13072
- const [candidatePrimaryId, setCandidatePrimaryId] = React13.useState(void 0);
13073
- const [registrationDialogOpen, setRegistrationDialogOpen] = React13.useState(false);
13074
- const makeContext = React13.useCallback(
13075
- (registration) => {
13076
- if (registration.scope === "global") {
13077
- return {
13078
- scope: "global",
13079
- primary: registration.primary
13080
- };
13081
- }
13082
- return {
13083
- scope: "node",
13084
- nodeId: registration.scopeId
13085
- };
13086
- },
13087
- []
13088
- );
13089
- const openCandidatePicker = React13.useCallback(
13090
- (context, primaryId) => {
13091
- setCandidateContext(context);
13092
- setCandidatePrimaryId(primaryId);
13093
- setCandidatePickerOpen(true);
13094
- },
13095
- []
13096
- );
13097
- if (activeServiceId === void 0 || activeServiceId === null) {
13098
- 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." }) });
13099
- }
13100
- return /* @__PURE__ */ jsxs10(Fragment, { children: [
13101
- /* @__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: [
13102
- /* @__PURE__ */ jsxs10("div", { className: "mb-4 flex items-start justify-between gap-3", children: [
13103
- /* @__PURE__ */ jsxs10("div", { children: [
13104
- /* @__PURE__ */ jsx14("h3", { className: "text-sm font-semibold text-zinc-900 dark:text-zinc-100", children: "Registered fallbacks" }),
13105
- /* @__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." })
13106
- ] }),
13107
- /* @__PURE__ */ jsx14(
13108
- "button",
13109
- {
13110
- type: "button",
13111
- onClick: () => setRegistrationDialogOpen(true),
13112
- 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",
13113
- children: "Add registration"
13114
- }
13115
- )
13116
- ] }),
13117
- /* @__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) => {
13118
- var _a;
13119
- const context = makeContext(reg);
13120
- const candidates = reg.services;
13121
- return /* @__PURE__ */ jsxs10(
13122
- "div",
13123
- {
13124
- className: "rounded-2xl border border-zinc-200 bg-zinc-50 p-4 dark:border-zinc-800 dark:bg-zinc-950",
13125
- children: [
13126
- /* @__PURE__ */ jsxs10("div", { className: "flex flex-wrap items-start justify-between gap-3", children: [
13127
- /* @__PURE__ */ jsxs10("div", { children: [
13128
- /* @__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}` }),
13129
- /* @__PURE__ */ jsxs10("div", { className: "mt-1 text-xs text-zinc-500 dark:text-zinc-400", children: [
13130
- "Primary #",
13131
- String(reg.primary)
13132
- ] })
13133
- ] }),
13134
- /* @__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: [
13135
- reg.scope,
13136
- reg.scopeId ? ` \xB7 ${reg.scopeId}` : ""
13137
- ] })
13138
- ] }),
13139
- /* @__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) => {
13140
- var _a2;
13141
- const preview = check(context, [
13142
- candidate
13143
- ]);
13144
- const rejected = preview.rejected[0];
13145
- 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";
13146
- const service = eligibleServices.find(
13147
- (s) => String(s.id) === String(candidate)
13148
- );
13149
- return /* @__PURE__ */ jsxs10(
13150
- "div",
13151
- {
13152
- className: `inline-flex items-center gap-2 rounded-xl border px-3 py-2 text-xs ${tone}`,
13153
- children: [
13154
- /* @__PURE__ */ jsx14("span", { children: service ? `#${String(service.id)} \xB7 ${(_a2 = service.name) != null ? _a2 : "Unnamed"}` : `#${String(candidate)}` }),
13155
- rejected ? /* @__PURE__ */ jsx14("span", { className: "rounded-full border border-current/20 px-2 py-0.5 text-[10px]", children: rejected.reasons.join(
13156
- ", "
13157
- ) }) : /* @__PURE__ */ jsx14("span", { className: "rounded-full border border-current/20 px-2 py-0.5 text-[10px]", children: "valid" }),
13158
- /* @__PURE__ */ jsx14(
13159
- "button",
13160
- {
13161
- type: "button",
13162
- onClick: () => remove2(
13163
- context,
13164
- candidate
13165
- ),
13166
- className: "text-current/70 hover:text-current",
13167
- children: "\xD7"
13168
- }
13169
- )
13170
- ]
13171
- },
13172
- String(candidate)
13173
- );
13174
- }) }),
13175
- /* @__PURE__ */ jsxs10("div", { className: "mt-4 flex gap-2", children: [
13176
- /* @__PURE__ */ jsx14(
13177
- "button",
13178
- {
13179
- type: "button",
13180
- onClick: () => openCandidatePicker(
13181
- context,
13182
- reg.primary
13183
- ),
13184
- 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",
13185
- children: "Add fallback"
13186
- }
13187
- ),
13188
- /* @__PURE__ */ jsx14(
13189
- "button",
13190
- {
13191
- type: "button",
13192
- onClick: () => clear(context),
13193
- 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",
13194
- children: "Clear"
13195
- }
13196
- )
13197
- ] })
13198
- ]
13199
- },
13200
- `${reg.scope}:${String((_a = reg.scopeId) != null ? _a : "global")}:${index}`
13201
- );
13202
- }) })
13203
- ] }),
13204
- /* @__PURE__ */ jsx14(
13205
- FallbackAddRegistrationDialog,
13206
- {
13207
- open: registrationDialogOpen,
13208
- onClose: () => setRegistrationDialogOpen(false),
13209
- onSelect: (context, primaryId) => {
13210
- setRegistrationDialogOpen(false);
13211
- openCandidatePicker(context, primaryId);
13212
- }
13213
- }
13214
- ),
13215
- /* @__PURE__ */ jsx14(
13216
- FallbackAddCandidatesDialog,
13217
- {
13218
- open: candidatePickerOpen,
13219
- onClose: () => setCandidatePickerOpen(false),
13220
- context: candidateContext,
13221
- primaryId: candidatePrimaryId
13222
- }
13223
- )
13224
- ] });
13225
- }
13226
13496
  export {
13227
13497
  CanvasAPI,
13228
13498
  EventBus,