@timeax/digital-service-engine 0.0.4 → 0.0.6

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.
@@ -3124,6 +3124,58 @@ function bindIdsToArray(bind) {
3124
3124
  if (!bind) return [];
3125
3125
  return Array.isArray(bind) ? bind.slice() : [bind];
3126
3126
  }
3127
+ function getEligibleFallbacks(params) {
3128
+ var _a, _b, _c, _d, _e, _f;
3129
+ const s = { ...DEFAULT_SETTINGS, ...(_a = params.settings) != null ? _a : {} };
3130
+ const { primary, nodeId, tagId, services } = params;
3131
+ const fb = (_b = params.fallbacks) != null ? _b : {};
3132
+ const excludes = new Set(((_c = params.exclude) != null ? _c : []).map(String));
3133
+ excludes.add(String(primary));
3134
+ const unique = (_d = params.unique) != null ? _d : true;
3135
+ const lists = [];
3136
+ if (nodeId && ((_e = fb.nodes) == null ? void 0 : _e[nodeId])) lists.push(fb.nodes[nodeId]);
3137
+ if ((_f = fb.global) == null ? void 0 : _f[primary]) lists.push(fb.global[primary]);
3138
+ if (!lists.length) return [];
3139
+ const primaryRate = rateOf(services, primary);
3140
+ const seen = /* @__PURE__ */ new Set();
3141
+ const eligible = [];
3142
+ for (const list of lists) {
3143
+ for (const cand of list) {
3144
+ const key = String(cand);
3145
+ if (excludes.has(key)) continue;
3146
+ if (unique && seen.has(key)) continue;
3147
+ seen.add(key);
3148
+ const cap = getCap(services, cand);
3149
+ if (!cap) continue;
3150
+ if (!passesRate(s.ratePolicy, primaryRate, cap.rate)) continue;
3151
+ if (s.requireConstraintFit && tagId) {
3152
+ const ok = satisfiesTagConstraints(
3153
+ tagId,
3154
+ { props: params.props, services },
3155
+ cap
3156
+ );
3157
+ if (!ok) continue;
3158
+ }
3159
+ eligible.push(cand);
3160
+ }
3161
+ }
3162
+ if (s.selectionStrategy === "cheapest") {
3163
+ eligible.sort((a, b) => {
3164
+ var _a2, _b2;
3165
+ const ra = (_a2 = rateOf(services, a)) != null ? _a2 : Infinity;
3166
+ const rb = (_b2 = rateOf(services, b)) != null ? _b2 : Infinity;
3167
+ return ra - rb;
3168
+ });
3169
+ }
3170
+ if (typeof params.limit === "number" && params.limit >= 0) {
3171
+ return eligible.slice(0, params.limit);
3172
+ }
3173
+ return eligible;
3174
+ }
3175
+ function getFallbackRegistrationInfo(props, nodeId) {
3176
+ const { primary, tagContexts } = primaryForNode(props, nodeId);
3177
+ return { primary, tagContexts };
3178
+ }
3127
3179
 
3128
3180
  // src/utils/prune-fallbacks.ts
3129
3181
  function pruneInvalidNodeFallbacks(props, services, settings) {
@@ -3812,6 +3864,347 @@ function resolveMinMax(servicesList, services) {
3812
3864
  return { min: min != null ? min : 1, ...max !== void 0 ? { max } : {} };
3813
3865
  }
3814
3866
 
3867
+ // src/core/fallback-editor.ts
3868
+ function createFallbackEditor(options = {}) {
3869
+ var _a, _b;
3870
+ const original = cloneFallbacks(options.fallbacks);
3871
+ let current = cloneFallbacks(options.fallbacks);
3872
+ const props = options.props;
3873
+ const services = (_a = options.services) != null ? _a : {};
3874
+ const settings = (_b = options.settings) != null ? _b : {};
3875
+ function state() {
3876
+ return {
3877
+ original: cloneFallbacks(original),
3878
+ current: cloneFallbacks(current),
3879
+ changed: !sameFallbacks(original, current)
3880
+ };
3881
+ }
3882
+ function value() {
3883
+ return cloneFallbacks(current);
3884
+ }
3885
+ function reset() {
3886
+ current = cloneFallbacks(original);
3887
+ return state();
3888
+ }
3889
+ function get(serviceId) {
3890
+ var _a2, _b2;
3891
+ const out = [];
3892
+ for (const [primary, list] of Object.entries((_a2 = current.global) != null ? _a2 : {})) {
3893
+ if (String(primary) !== String(serviceId)) continue;
3894
+ out.push({
3895
+ scope: "global",
3896
+ primary,
3897
+ services: [...list != null ? list : []]
3898
+ });
3899
+ }
3900
+ if (!props) return out;
3901
+ for (const [nodeId, list] of Object.entries((_b2 = current.nodes) != null ? _b2 : {})) {
3902
+ const info = getFallbackRegistrationInfo(props, nodeId);
3903
+ if (String(info.primary) !== String(serviceId)) continue;
3904
+ out.push({
3905
+ scope: "node",
3906
+ scopeId: nodeId,
3907
+ primary: info.primary,
3908
+ services: [...list != null ? list : []]
3909
+ });
3910
+ }
3911
+ return out;
3912
+ }
3913
+ function getScope(context) {
3914
+ var _a2, _b2, _c, _d;
3915
+ if (context.scope === "global") {
3916
+ return [...(_b2 = (_a2 = current.global) == null ? void 0 : _a2[context.primary]) != null ? _b2 : []];
3917
+ }
3918
+ return [...(_d = (_c = current.nodes) == null ? void 0 : _c[context.nodeId]) != null ? _d : []];
3919
+ }
3920
+ function check(context, candidates) {
3921
+ var _a2, _b2;
3922
+ const normalized = normalizeCandidateList(
3923
+ candidates != null ? candidates : getScope(context),
3924
+ true
3925
+ );
3926
+ if (context.scope === "node" && !props) {
3927
+ return {
3928
+ context,
3929
+ allowed: [],
3930
+ rejected: normalized.map((candidate) => ({
3931
+ candidate,
3932
+ ok: false,
3933
+ reasons: ["missing_service_props"]
3934
+ })),
3935
+ warnings: ["missing_service_props"]
3936
+ };
3937
+ }
3938
+ const tempFallbacks = cloneFallbacks(current);
3939
+ if (context.scope === "global") {
3940
+ (_a2 = tempFallbacks.global) != null ? _a2 : tempFallbacks.global = {};
3941
+ if (normalized.length)
3942
+ tempFallbacks.global[context.primary] = normalized;
3943
+ else delete tempFallbacks.global[context.primary];
3944
+ } else {
3945
+ (_b2 = tempFallbacks.nodes) != null ? _b2 : tempFallbacks.nodes = {};
3946
+ if (normalized.length)
3947
+ tempFallbacks.nodes[context.nodeId] = normalized;
3948
+ else delete tempFallbacks.nodes[context.nodeId];
3949
+ }
3950
+ if (!props) {
3951
+ if (context.scope !== "global") {
3952
+ return {
3953
+ context,
3954
+ allowed: [],
3955
+ rejected: normalized.map((candidate) => ({
3956
+ candidate,
3957
+ ok: false,
3958
+ reasons: ["missing_service_props"]
3959
+ })),
3960
+ warnings: ["missing_service_props"]
3961
+ };
3962
+ }
3963
+ const rejected2 = [];
3964
+ const allowed2 = [];
3965
+ for (const candidate of normalized) {
3966
+ const reasons = [];
3967
+ if (String(candidate) === String(context.primary)) {
3968
+ reasons.push("self_reference");
3969
+ }
3970
+ if (reasons.length) {
3971
+ rejected2.push({ candidate, ok: false, reasons });
3972
+ } else {
3973
+ allowed2.push(candidate);
3974
+ }
3975
+ }
3976
+ return {
3977
+ context,
3978
+ primary: context.primary,
3979
+ allowed: allowed2,
3980
+ rejected: rejected2,
3981
+ warnings: []
3982
+ };
3983
+ }
3984
+ const fakeProps = {
3985
+ ...props,
3986
+ fallbacks: tempFallbacks
3987
+ };
3988
+ const diags = collectFailedFallbacks(fakeProps, services, {
3989
+ ...settings,
3990
+ mode: "dev"
3991
+ });
3992
+ const scoped = diags.filter((d) => {
3993
+ if (context.scope === "global") {
3994
+ return d.scope === "global" && String(d.primary) === String(context.primary);
3995
+ }
3996
+ return d.scope === "node" && String(d.nodeId) === String(context.nodeId);
3997
+ });
3998
+ const rejected = normalized.map((candidate) => {
3999
+ const reasons = scoped.filter((d) => String(d.candidate) === String(candidate)).map((d) => mapDiagReason(d.reason));
4000
+ return {
4001
+ candidate,
4002
+ ok: reasons.length === 0,
4003
+ reasons
4004
+ };
4005
+ }).filter((row) => !row.ok);
4006
+ const allowed = normalized.filter(
4007
+ (candidate) => !rejected.some(
4008
+ (r) => String(r.candidate) === String(candidate)
4009
+ )
4010
+ );
4011
+ const info = context.scope === "global" ? { ok: true, primary: context.primary } : getNodeRegistrationInfo(props, context.nodeId);
4012
+ const primary = (info == null ? void 0 : info.ok) ? info.primary : void 0;
4013
+ return {
4014
+ context,
4015
+ primary,
4016
+ allowed,
4017
+ rejected,
4018
+ warnings: []
4019
+ };
4020
+ }
4021
+ function add(context, candidate, options2) {
4022
+ return addMany(context, [candidate], options2);
4023
+ }
4024
+ function addMany(context, candidates, options2) {
4025
+ const existing = getScope(context);
4026
+ const merged = [...existing];
4027
+ const insertAt = typeof (options2 == null ? void 0 : options2.index) === "number" ? clamp(options2.index, 0, merged.length) : void 0;
4028
+ const incoming = normalizeCandidateList(candidates, true).filter(
4029
+ (id) => !merged.some((x) => String(x) === String(id))
4030
+ );
4031
+ if (insertAt === void 0) {
4032
+ merged.push(...incoming);
4033
+ } else {
4034
+ merged.splice(insertAt, 0, ...incoming);
4035
+ }
4036
+ return replace(context, merged, options2);
4037
+ }
4038
+ function remove(context, candidate) {
4039
+ const next = getScope(context).filter(
4040
+ (id) => String(id) !== String(candidate)
4041
+ );
4042
+ return writeScope(context, next);
4043
+ }
4044
+ function replace(context, candidates, options2) {
4045
+ const strict = !!(options2 == null ? void 0 : options2.strict);
4046
+ const normalized = normalizeCandidateList(candidates, true);
4047
+ const checked = check(context, normalized);
4048
+ const next = strict ? checked.allowed : normalized;
4049
+ return writeScope(context, next);
4050
+ }
4051
+ function clear(context) {
4052
+ return writeScope(context, []);
4053
+ }
4054
+ function eligible(context, opt) {
4055
+ if (!props) return [];
4056
+ if (context.scope === "global") {
4057
+ return getEligibleFallbacks({
4058
+ primary: context.primary,
4059
+ services,
4060
+ fallbacks: current,
4061
+ settings,
4062
+ props,
4063
+ exclude: opt == null ? void 0 : opt.exclude,
4064
+ unique: opt == null ? void 0 : opt.unique,
4065
+ limit: opt == null ? void 0 : opt.limit
4066
+ });
4067
+ }
4068
+ const info = getFallbackRegistrationInfo(props, context.nodeId);
4069
+ if (!info.primary) return [];
4070
+ return getEligibleFallbacks({
4071
+ primary: info.primary,
4072
+ nodeId: context.nodeId,
4073
+ tagId: info.tagContexts[0],
4074
+ services,
4075
+ fallbacks: current,
4076
+ settings,
4077
+ props,
4078
+ exclude: opt == null ? void 0 : opt.exclude,
4079
+ unique: opt == null ? void 0 : opt.unique,
4080
+ limit: opt == null ? void 0 : opt.limit
4081
+ });
4082
+ }
4083
+ function writeScope(context, nextList) {
4084
+ var _a2, _b2;
4085
+ const next = cloneFallbacks(current);
4086
+ if (context.scope === "global") {
4087
+ (_a2 = next.global) != null ? _a2 : next.global = {};
4088
+ if (nextList.length) {
4089
+ next.global[context.primary] = [...nextList];
4090
+ } else {
4091
+ delete next.global[context.primary];
4092
+ if (!Object.keys(next.global).length) delete next.global;
4093
+ }
4094
+ } else {
4095
+ (_b2 = next.nodes) != null ? _b2 : next.nodes = {};
4096
+ if (nextList.length) {
4097
+ next.nodes[context.nodeId] = [...nextList];
4098
+ } else {
4099
+ delete next.nodes[context.nodeId];
4100
+ if (!Object.keys(next.nodes).length) delete next.nodes;
4101
+ }
4102
+ }
4103
+ current = next;
4104
+ return state();
4105
+ }
4106
+ return {
4107
+ state,
4108
+ value,
4109
+ reset,
4110
+ get,
4111
+ getScope,
4112
+ check,
4113
+ add,
4114
+ addMany,
4115
+ remove,
4116
+ replace,
4117
+ clear,
4118
+ eligible
4119
+ };
4120
+ }
4121
+ function cloneFallbacks(input) {
4122
+ return {
4123
+ ...(input == null ? void 0 : input.nodes) ? { nodes: cloneRecordArray(input.nodes) } : {},
4124
+ ...(input == null ? void 0 : input.global) ? { global: cloneRecordArray(input.global) } : {}
4125
+ };
4126
+ }
4127
+ function cloneRecordArray(input) {
4128
+ const out = {};
4129
+ for (const [k, v] of Object.entries(input)) out[k] = [...v != null ? v : []];
4130
+ return out;
4131
+ }
4132
+ function sameFallbacks(a, b) {
4133
+ return JSON.stringify(a != null ? a : {}) === JSON.stringify(b != null ? b : {});
4134
+ }
4135
+ function normalizeCandidateList(input, preserveOrder) {
4136
+ const out = [];
4137
+ for (const item of input != null ? input : []) {
4138
+ if (!isValidServiceIdRef(item)) continue;
4139
+ const exists = out.some((x) => String(x) === String(item));
4140
+ if (exists) continue;
4141
+ out.push(item);
4142
+ }
4143
+ return preserveOrder ? out : out;
4144
+ }
4145
+ function isValidServiceIdRef(value) {
4146
+ return typeof value === "number" && Number.isFinite(value) || typeof value === "string" && value.trim().length > 0;
4147
+ }
4148
+ function clamp(n, min, max) {
4149
+ return Math.max(min, Math.min(max, n));
4150
+ }
4151
+ function getNodeRegistrationInfo(props, nodeId) {
4152
+ const tag = props.filters.find((t) => t.id === nodeId);
4153
+ if (tag) {
4154
+ if (!isValidServiceIdRef(tag.service_id)) {
4155
+ return { ok: false, reasons: ["no_primary"] };
4156
+ }
4157
+ return {
4158
+ ok: true,
4159
+ primary: tag.service_id,
4160
+ tagContexts: [tag.id]
4161
+ };
4162
+ }
4163
+ const hit = findOptionOwner(props.fields, nodeId);
4164
+ if (!hit) {
4165
+ return { ok: false, reasons: ["node_not_found"] };
4166
+ }
4167
+ if (!isValidServiceIdRef(hit.option.service_id)) {
4168
+ return { ok: false, reasons: ["no_primary"] };
4169
+ }
4170
+ return {
4171
+ ok: true,
4172
+ primary: hit.option.service_id,
4173
+ tagContexts: bindIdsToArray2(hit.field.bind_id)
4174
+ };
4175
+ }
4176
+ function findOptionOwner(fields, optionId) {
4177
+ var _a;
4178
+ for (const field of fields) {
4179
+ for (const option of (_a = field.options) != null ? _a : []) {
4180
+ if (option.id === optionId) return { field, option };
4181
+ }
4182
+ }
4183
+ return null;
4184
+ }
4185
+ function bindIdsToArray2(v) {
4186
+ if (Array.isArray(v)) return v.filter(Boolean);
4187
+ return v ? [v] : [];
4188
+ }
4189
+ function mapDiagReason(reason) {
4190
+ switch (String(reason)) {
4191
+ case "unknown_service":
4192
+ return "unknown_service";
4193
+ case "no_primary":
4194
+ return "no_primary";
4195
+ case "rate_violation":
4196
+ return "rate_violation";
4197
+ case "constraint_mismatch":
4198
+ return "constraint_mismatch";
4199
+ case "cycle":
4200
+ return "cycle";
4201
+ case "no_tag_context":
4202
+ return "no_tag_context";
4203
+ default:
4204
+ return "node_not_found";
4205
+ }
4206
+ }
4207
+
3815
4208
  // src/core/policy.ts
3816
4209
  var ALLOWED_SCOPES = /* @__PURE__ */ new Set([
3817
4210
  "global",
@@ -9816,20 +10209,1524 @@ function registerEntries(registry) {
9816
10209
  })
9817
10210
  );
9818
10211
  }
10212
+
10213
+ // src/react/fallback-editor/useFallbackEditor.ts
10214
+ import React6 from "react";
10215
+
10216
+ // src/react/fallback-editor/FallbackEditorProvider.tsx
10217
+ import React5 from "react";
10218
+ import { jsx as jsx5 } from "react/jsx-runtime";
10219
+ var FallbackEditorContext = React5.createContext(null);
10220
+ function FallbackEditorProvider({
10221
+ children,
10222
+ fallbacks,
10223
+ props,
10224
+ snapshot,
10225
+ primaryServices,
10226
+ eligibleServices,
10227
+ settings: initialSettings,
10228
+ initialServiceId,
10229
+ initialTab = "registrations",
10230
+ onSettingsChange,
10231
+ onSave,
10232
+ onValidate,
10233
+ onReset
10234
+ }) {
10235
+ const [settings, setSettings] = React5.useState(
10236
+ initialSettings != null ? initialSettings : {}
10237
+ );
10238
+ const [settingsSaving, setSettingsSaving] = React5.useState(false);
10239
+ const [headerSaving, setHeaderSaving] = React5.useState(false);
10240
+ const [headerValidating, setHeaderValidating] = React5.useState(false);
10241
+ const [headerResetting, setHeaderResetting] = React5.useState(false);
10242
+ const [version, setVersion] = React5.useState(0);
10243
+ const [activeServiceId, setActiveServiceId] = React5.useState(initialServiceId);
10244
+ const [activeTab, setActiveTab] = React5.useState(initialTab);
10245
+ React5.useEffect(() => {
10246
+ setSettings(initialSettings != null ? initialSettings : {});
10247
+ }, [initialSettings]);
10248
+ const resolvedPrimaryServices = React5.useMemo(
10249
+ () => primaryServices != null ? primaryServices : {},
10250
+ [primaryServices]
10251
+ );
10252
+ const resolvedEligibleServices = React5.useMemo(
10253
+ () => {
10254
+ var _a;
10255
+ return (_a = eligibleServices != null ? eligibleServices : primaryServices) != null ? _a : {};
10256
+ },
10257
+ [eligibleServices, primaryServices]
10258
+ );
10259
+ const editorRef = React5.useRef(null);
10260
+ const buildEditor = React5.useCallback(
10261
+ (next) => {
10262
+ var _a, _b, _c, _d, _e, _f, _g, _h;
10263
+ const currentValue = (_a = editorRef.current) == null ? void 0 : _a.value();
10264
+ editorRef.current = createFallbackEditor({
10265
+ fallbacks: (_d = (_c = (_b = next == null ? void 0 : next.fallbacks) != null ? _b : currentValue) != null ? _c : fallbacks) != null ? _d : {},
10266
+ props: (_e = next == null ? void 0 : next.props) != null ? _e : props,
10267
+ snapshot: (_f = next == null ? void 0 : next.snapshot) != null ? _f : snapshot,
10268
+ services: (_g = next == null ? void 0 : next.services) != null ? _g : resolvedEligibleServices,
10269
+ settings: (_h = next == null ? void 0 : next.settings) != null ? _h : settings
10270
+ });
10271
+ setVersion((v) => v + 1);
10272
+ },
10273
+ [fallbacks, props, snapshot, resolvedEligibleServices, settings]
10274
+ );
10275
+ if (!editorRef.current) {
10276
+ editorRef.current = createFallbackEditor({
10277
+ fallbacks: fallbacks != null ? fallbacks : {},
10278
+ props,
10279
+ snapshot,
10280
+ services: resolvedEligibleServices,
10281
+ settings
10282
+ });
10283
+ }
10284
+ React5.useEffect(() => {
10285
+ buildEditor({
10286
+ fallbacks: fallbacks != null ? fallbacks : {},
10287
+ props,
10288
+ snapshot,
10289
+ services: resolvedEligibleServices,
10290
+ settings
10291
+ });
10292
+ }, [fallbacks, props, snapshot, resolvedEligibleServices, buildEditor]);
10293
+ const editor = editorRef.current;
10294
+ const bump = React5.useCallback(() => {
10295
+ setVersion((v) => v + 1);
10296
+ }, []);
10297
+ const syncAfterMutation = React5.useCallback(() => {
10298
+ bump();
10299
+ }, [bump]);
10300
+ const reset = React5.useCallback(() => {
10301
+ editor.reset();
10302
+ syncAfterMutation();
10303
+ }, [editor, syncAfterMutation]);
10304
+ const add = React5.useCallback(
10305
+ (context, candidate, options) => {
10306
+ const next = editor.add(context, candidate, options);
10307
+ syncAfterMutation();
10308
+ return next;
10309
+ },
10310
+ [editor, syncAfterMutation]
10311
+ );
10312
+ const addMany = React5.useCallback(
10313
+ (context, candidates, options) => {
10314
+ const next = editor.addMany(context, candidates, options);
10315
+ syncAfterMutation();
10316
+ return next;
10317
+ },
10318
+ [editor, syncAfterMutation]
10319
+ );
10320
+ const remove = React5.useCallback(
10321
+ (context, candidate) => {
10322
+ const next = editor.remove(context, candidate);
10323
+ syncAfterMutation();
10324
+ return next;
10325
+ },
10326
+ [editor, syncAfterMutation]
10327
+ );
10328
+ const replace = React5.useCallback(
10329
+ (context, candidates, options) => {
10330
+ const next = editor.replace(context, candidates, options);
10331
+ syncAfterMutation();
10332
+ return next;
10333
+ },
10334
+ [editor, syncAfterMutation]
10335
+ );
10336
+ const clear = React5.useCallback(
10337
+ (context) => {
10338
+ const next = editor.clear(context);
10339
+ syncAfterMutation();
10340
+ return next;
10341
+ },
10342
+ [editor, syncAfterMutation]
10343
+ );
10344
+ const saveSettings = React5.useCallback(
10345
+ async (next) => {
10346
+ setSettingsSaving(true);
10347
+ try {
10348
+ const resolved = onSettingsChange ? await onSettingsChange(next) : next;
10349
+ const finalSettings = resolved != null ? resolved : next;
10350
+ setSettings(finalSettings);
10351
+ buildEditor({
10352
+ settings: finalSettings,
10353
+ fallbacks: editor.value()
10354
+ });
10355
+ return finalSettings;
10356
+ } finally {
10357
+ setSettingsSaving(false);
10358
+ }
10359
+ },
10360
+ [onSettingsChange, buildEditor, editor]
10361
+ );
10362
+ const saveFallbacks = React5.useCallback(async () => {
10363
+ const next = editor.value();
10364
+ setHeaderSaving(true);
10365
+ try {
10366
+ const resolved = onSave ? await onSave(next) : next;
10367
+ if (resolved) {
10368
+ buildEditor({ fallbacks: resolved });
10369
+ }
10370
+ return resolved;
10371
+ } finally {
10372
+ setHeaderSaving(false);
10373
+ }
10374
+ }, [editor, onSave, buildEditor]);
10375
+ const validateFallbacks2 = React5.useCallback(async () => {
10376
+ const next = editor.value();
10377
+ setHeaderValidating(true);
10378
+ try {
10379
+ if (onValidate) {
10380
+ await onValidate(next);
10381
+ }
10382
+ } finally {
10383
+ setHeaderValidating(false);
10384
+ }
10385
+ }, [editor, onValidate]);
10386
+ const resetEditor = React5.useCallback(async () => {
10387
+ setHeaderResetting(true);
10388
+ try {
10389
+ editor.reset();
10390
+ syncAfterMutation();
10391
+ if (onReset) {
10392
+ await onReset();
10393
+ }
10394
+ } finally {
10395
+ setHeaderResetting(false);
10396
+ }
10397
+ }, [editor, syncAfterMutation, onReset]);
10398
+ const value = React5.useMemo(() => editor.value(), [editor, version]);
10399
+ const state = React5.useMemo(() => editor.state(), [editor, version]);
10400
+ const ctx = React5.useMemo(
10401
+ () => ({
10402
+ editor,
10403
+ version,
10404
+ serviceProps: props,
10405
+ snapshot,
10406
+ primaryServices: resolvedPrimaryServices,
10407
+ eligibleServices: resolvedEligibleServices,
10408
+ activeServiceId,
10409
+ setActiveServiceId,
10410
+ activeTab,
10411
+ setActiveTab,
10412
+ state,
10413
+ value,
10414
+ settings,
10415
+ settingsSaving,
10416
+ headerSaving,
10417
+ headerValidating,
10418
+ headerResetting,
10419
+ saveSettings,
10420
+ saveFallbacks,
10421
+ validateFallbacks: validateFallbacks2,
10422
+ resetEditor,
10423
+ reset,
10424
+ get: editor.get,
10425
+ getScope: editor.getScope,
10426
+ check: editor.check,
10427
+ eligible: editor.eligible,
10428
+ add,
10429
+ addMany,
10430
+ remove,
10431
+ replace,
10432
+ clear
10433
+ }),
10434
+ [
10435
+ editor,
10436
+ version,
10437
+ props,
10438
+ snapshot,
10439
+ resolvedPrimaryServices,
10440
+ resolvedEligibleServices,
10441
+ activeServiceId,
10442
+ activeTab,
10443
+ state,
10444
+ value,
10445
+ settings,
10446
+ settingsSaving,
10447
+ headerSaving,
10448
+ headerValidating,
10449
+ headerResetting,
10450
+ saveSettings,
10451
+ saveFallbacks,
10452
+ validateFallbacks2,
10453
+ resetEditor,
10454
+ reset,
10455
+ add,
10456
+ addMany,
10457
+ remove,
10458
+ replace,
10459
+ clear
10460
+ ]
10461
+ );
10462
+ return /* @__PURE__ */ jsx5(FallbackEditorContext.Provider, { value: ctx, children });
10463
+ }
10464
+ function useFallbackEditorContext() {
10465
+ const ctx = React5.useContext(FallbackEditorContext);
10466
+ if (!ctx) {
10467
+ throw new Error(
10468
+ "useFallbackEditorContext must be used inside FallbackEditorProvider"
10469
+ );
10470
+ }
10471
+ return ctx;
10472
+ }
10473
+
10474
+ // src/react/fallback-editor/useFallbackEditor.ts
10475
+ function useFallbackEditor() {
10476
+ return useFallbackEditorContext();
10477
+ }
10478
+ function useActiveFallbackRegistrations() {
10479
+ const { activeServiceId, get, version } = useFallbackEditorContext();
10480
+ return React6.useMemo(() => {
10481
+ if (activeServiceId === void 0 || activeServiceId === null)
10482
+ return [];
10483
+ return get(activeServiceId);
10484
+ }, [activeServiceId, get, version]);
10485
+ }
10486
+ function usePrimaryServiceList() {
10487
+ const { primaryServices, version } = useFallbackEditorContext();
10488
+ return React6.useMemo(() => {
10489
+ return Object.values(primaryServices != null ? primaryServices : {});
10490
+ }, [primaryServices, version]);
10491
+ }
10492
+ function useEligibleServiceList() {
10493
+ const { eligibleServices, version } = useFallbackEditorContext();
10494
+ return React6.useMemo(() => {
10495
+ return Object.values(eligibleServices != null ? eligibleServices : {});
10496
+ }, [eligibleServices, version]);
10497
+ }
10498
+
10499
+ // src/react/fallback-editor/FallbackEditor.tsx
10500
+ import { jsx as jsx6, jsxs as jsxs2 } from "react/jsx-runtime";
10501
+ function FallbackEditor({
10502
+ className,
10503
+ fallbacks,
10504
+ props,
10505
+ snapshot,
10506
+ primaryServices,
10507
+ eligibleServices,
10508
+ settings,
10509
+ initialServiceId,
10510
+ onSettingsChange,
10511
+ onSave,
10512
+ onValidate,
10513
+ onReset
10514
+ }) {
10515
+ return /* @__PURE__ */ jsx6(
10516
+ FallbackEditorProvider,
10517
+ {
10518
+ fallbacks,
10519
+ props,
10520
+ snapshot,
10521
+ primaryServices,
10522
+ eligibleServices,
10523
+ settings,
10524
+ initialServiceId,
10525
+ onSettingsChange,
10526
+ onSave,
10527
+ onValidate,
10528
+ onReset,
10529
+ children: /* @__PURE__ */ jsx6(FallbackEditorInner, { className })
10530
+ }
10531
+ );
10532
+ }
10533
+ function FallbackEditorInner({ className }) {
10534
+ const {
10535
+ activeTab,
10536
+ setActiveTab,
10537
+ activeServiceId,
10538
+ saveFallbacks,
10539
+ validateFallbacks: validateFallbacks2,
10540
+ resetEditor,
10541
+ headerSaving,
10542
+ headerValidating,
10543
+ headerResetting
10544
+ } = useFallbackEditor();
10545
+ return /* @__PURE__ */ jsx6(
10546
+ "div",
10547
+ {
10548
+ className: [
10549
+ "min-h-screen bg-zinc-100 p-4 text-zinc-900 dark:bg-zinc-950 dark:text-zinc-100",
10550
+ className
10551
+ ].filter(Boolean).join(" "),
10552
+ children: /* @__PURE__ */ jsxs2("div", { className: "mx-auto flex max-w-7xl flex-col gap-4", children: [
10553
+ /* @__PURE__ */ jsx6(
10554
+ FallbackEditorHeader,
10555
+ {
10556
+ onReset: resetEditor,
10557
+ onValidate: validateFallbacks2,
10558
+ onSave: saveFallbacks,
10559
+ resetting: headerResetting,
10560
+ validating: headerValidating,
10561
+ saving: headerSaving
10562
+ }
10563
+ ),
10564
+ /* @__PURE__ */ jsxs2("div", { className: "grid gap-4 xl:grid-cols-[300px_minmax(0,1fr)_360px]", children: [
10565
+ /* @__PURE__ */ jsx6(FallbackServiceSidebar, {}),
10566
+ /* @__PURE__ */ jsxs2("div", { className: "flex min-h-0 flex-col gap-4", children: [
10567
+ /* @__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: [
10568
+ /* @__PURE__ */ jsxs2("div", { children: [
10569
+ /* @__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" }),
10570
+ /* @__PURE__ */ jsx6("p", { className: "mt-1 text-sm text-zinc-500 dark:text-zinc-400", children: "Edit fallback registrations and inspect validation." })
10571
+ ] }),
10572
+ /* @__PURE__ */ jsxs2("div", { className: "flex gap-2", children: [
10573
+ /* @__PURE__ */ jsx6(
10574
+ TabButton,
10575
+ {
10576
+ active: activeTab === "registrations",
10577
+ onClick: () => setActiveTab("registrations"),
10578
+ children: "Registrations"
10579
+ }
10580
+ ),
10581
+ /* @__PURE__ */ jsx6(
10582
+ TabButton,
10583
+ {
10584
+ active: activeTab === "settings",
10585
+ onClick: () => setActiveTab("settings"),
10586
+ children: "Settings"
10587
+ }
10588
+ )
10589
+ ] })
10590
+ ] }) }),
10591
+ activeTab === "registrations" ? /* @__PURE__ */ jsx6(FallbackRegistrationsPanel, {}) : /* @__PURE__ */ jsx6(FallbackSettingsPanel, {})
10592
+ ] }),
10593
+ /* @__PURE__ */ jsx6(FallbackDetailsPanel, {})
10594
+ ] })
10595
+ ] })
10596
+ }
10597
+ );
10598
+ }
10599
+ function TabButton({
10600
+ active,
10601
+ onClick,
10602
+ children
10603
+ }) {
10604
+ return /* @__PURE__ */ jsx6(
10605
+ "button",
10606
+ {
10607
+ type: "button",
10608
+ onClick,
10609
+ className: [
10610
+ "rounded-xl px-3 py-2 text-sm font-medium transition",
10611
+ active ? "bg-blue-600 text-white" : "border border-zinc-300 bg-white text-zinc-700 hover:bg-zinc-50 dark:border-zinc-700 dark:bg-zinc-950 dark:text-zinc-200 dark:hover:bg-zinc-800"
10612
+ ].join(" "),
10613
+ children
10614
+ }
10615
+ );
10616
+ }
10617
+
10618
+ // src/react/fallback-editor/VirtualServiceList.tsx
10619
+ import React7 from "react";
10620
+ import { jsx as jsx7, jsxs as jsxs3 } from "react/jsx-runtime";
10621
+ function VirtualServiceList({
10622
+ items,
10623
+ selected,
10624
+ onToggle,
10625
+ height = 420,
10626
+ rowHeight = 52,
10627
+ emptyText = "No services found."
10628
+ }) {
10629
+ const [scrollTop, setScrollTop] = React7.useState(0);
10630
+ const total = items.length;
10631
+ const visibleCount = Math.ceil(height / rowHeight);
10632
+ const overscan = 8;
10633
+ const start = Math.max(0, Math.floor(scrollTop / rowHeight) - overscan);
10634
+ const end = Math.min(total, start + visibleCount + overscan * 2);
10635
+ const visible = items.slice(start, end);
10636
+ if (total === 0) {
10637
+ return /* @__PURE__ */ jsx7(
10638
+ "div",
10639
+ {
10640
+ className: "flex items-center justify-center rounded-xl border border-zinc-200 bg-zinc-50 text-sm text-zinc-500 dark:border-zinc-800 dark:bg-zinc-950 dark:text-zinc-400",
10641
+ style: { height },
10642
+ children: emptyText
10643
+ }
10644
+ );
10645
+ }
10646
+ return /* @__PURE__ */ jsx7(
10647
+ "div",
10648
+ {
10649
+ className: "overflow-auto rounded-xl border border-zinc-200 dark:border-zinc-800",
10650
+ style: { height },
10651
+ onScroll: (e) => setScrollTop(e.currentTarget.scrollTop),
10652
+ children: /* @__PURE__ */ jsx7("div", { className: "relative", style: { height: total * rowHeight }, children: visible.map((item, i) => {
10653
+ var _a, _b;
10654
+ const index = start + i;
10655
+ const key = String(item.id);
10656
+ const checked = selected.has(key);
10657
+ return /* @__PURE__ */ jsxs3(
10658
+ "button",
10659
+ {
10660
+ type: "button",
10661
+ onClick: () => onToggle(item.id),
10662
+ 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",
10663
+ style: {
10664
+ top: index * rowHeight,
10665
+ height: rowHeight
10666
+ },
10667
+ children: [
10668
+ /* @__PURE__ */ jsxs3("div", { className: "min-w-0", children: [
10669
+ /* @__PURE__ */ jsxs3("div", { className: "truncate text-sm font-medium text-zinc-900 dark:text-zinc-100", children: [
10670
+ "#",
10671
+ String(item.id),
10672
+ " \xB7",
10673
+ " ",
10674
+ (_a = item.name) != null ? _a : "Unnamed"
10675
+ ] }),
10676
+ /* @__PURE__ */ jsxs3("div", { className: "mt-0.5 text-xs text-zinc-500 dark:text-zinc-400", children: [
10677
+ (_b = item.platform) != null ? _b : "Unknown",
10678
+ typeof item.rate === "number" ? ` \xB7 rate ${item.rate}` : ""
10679
+ ] })
10680
+ ] }),
10681
+ /* @__PURE__ */ jsx7(
10682
+ "input",
10683
+ {
10684
+ type: "checkbox",
10685
+ checked,
10686
+ readOnly: true,
10687
+ className: "h-4 w-4 rounded border-zinc-300"
10688
+ }
10689
+ )
10690
+ ]
10691
+ },
10692
+ key
10693
+ );
10694
+ }) })
10695
+ }
10696
+ );
10697
+ }
10698
+
10699
+ // src/react/fallback-editor/FallbackDetailsPanel.tsx
10700
+ import React8 from "react";
10701
+ import { jsx as jsx8, jsxs as jsxs4 } from "react/jsx-runtime";
10702
+ function FallbackDetailsPanel() {
10703
+ var _a, _b, _c, _d, _e, _f;
10704
+ const { activeServiceId, state, settings } = useFallbackEditor();
10705
+ const services = usePrimaryServiceList();
10706
+ const service = React8.useMemo(
10707
+ () => services.find((s) => String(s.id) === String(activeServiceId)),
10708
+ [services, activeServiceId]
10709
+ );
10710
+ return /* @__PURE__ */ jsxs4("aside", { className: "flex min-h-0 flex-col gap-4", children: [
10711
+ /* @__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: [
10712
+ /* @__PURE__ */ jsx8("h3", { className: "text-sm font-semibold text-zinc-900 dark:text-zinc-100", children: "Primary service info" }),
10713
+ !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: [
10714
+ /* @__PURE__ */ jsx8(Detail, { label: "ID", value: String(service.id) }),
10715
+ /* @__PURE__ */ jsx8(
10716
+ Detail,
10717
+ {
10718
+ label: "Name",
10719
+ value: (_a = service.name) != null ? _a : "Unnamed"
10720
+ }
10721
+ ),
10722
+ /* @__PURE__ */ jsx8(
10723
+ Detail,
10724
+ {
10725
+ label: "Platform",
10726
+ value: (_b = service.platform) != null ? _b : "\u2014"
10727
+ }
10728
+ ),
10729
+ /* @__PURE__ */ jsx8(
10730
+ Detail,
10731
+ {
10732
+ label: "Rate",
10733
+ value: typeof service.rate === "number" ? String(service.rate) : "\u2014"
10734
+ }
10735
+ ),
10736
+ /* @__PURE__ */ jsx8(
10737
+ Detail,
10738
+ {
10739
+ label: "Changed",
10740
+ value: state.changed ? "yes" : "no"
10741
+ }
10742
+ )
10743
+ ] })
10744
+ ] }),
10745
+ /* @__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: [
10746
+ /* @__PURE__ */ jsx8("h3", { className: "text-sm font-semibold text-zinc-900 dark:text-zinc-100", children: "Active policy" }),
10747
+ /* @__PURE__ */ jsxs4("div", { className: "mt-3 space-y-2 text-sm", children: [
10748
+ /* @__PURE__ */ jsx8(
10749
+ Detail,
10750
+ {
10751
+ label: "Constraint fit",
10752
+ value: settings.requireConstraintFit ? "enabled" : "disabled"
10753
+ }
10754
+ ),
10755
+ /* @__PURE__ */ jsx8(
10756
+ Detail,
10757
+ {
10758
+ label: "Rate policy",
10759
+ value: ((_c = settings.ratePolicy) == null ? void 0 : _c.kind) === "within_pct" ? `within_pct (${settings.ratePolicy.pct}%)` : ((_d = settings.ratePolicy) == null ? void 0 : _d.kind) === "at_least_pct_lower" ? `at_least_pct_lower (${settings.ratePolicy.pct}%)` : "lte_primary"
10760
+ }
10761
+ ),
10762
+ /* @__PURE__ */ jsx8(
10763
+ Detail,
10764
+ {
10765
+ label: "Strategy",
10766
+ value: (_e = settings.selectionStrategy) != null ? _e : "priority"
10767
+ }
10768
+ ),
10769
+ /* @__PURE__ */ jsx8(Detail, { label: "Mode", value: (_f = settings.mode) != null ? _f : "strict" })
10770
+ ] })
10771
+ ] }),
10772
+ /* @__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: [
10773
+ /* @__PURE__ */ jsx8("h3", { className: "text-sm font-semibold text-zinc-900 dark:text-zinc-100", children: "Current payload" }),
10774
+ /* @__PURE__ */ jsx8("pre", { className: "mt-3 overflow-auto rounded-xl bg-zinc-950 p-3 text-xs text-zinc-100", children: JSON.stringify(state.current, null, 2) })
10775
+ ] })
10776
+ ] });
10777
+ }
10778
+ function Detail({ label, value }) {
10779
+ return /* @__PURE__ */ jsxs4("div", { className: "flex items-start justify-between gap-4", children: [
10780
+ /* @__PURE__ */ jsx8("span", { className: "text-zinc-500 dark:text-zinc-400", children: label }),
10781
+ /* @__PURE__ */ jsx8("span", { className: "text-right text-zinc-900 dark:text-zinc-100", children: value })
10782
+ ] });
10783
+ }
10784
+
10785
+ // src/react/fallback-editor/FallbackEditorHeader.tsx
10786
+ import { jsx as jsx9, jsxs as jsxs5 } from "react/jsx-runtime";
10787
+ function FallbackEditorHeader({
10788
+ onReset,
10789
+ onValidate,
10790
+ onSave,
10791
+ resetting = false,
10792
+ validating = false,
10793
+ saving = false
10794
+ }) {
10795
+ return /* @__PURE__ */ jsxs5("div", { className: "flex flex-col gap-3 rounded-2xl border border-zinc-200 bg-white p-4 shadow-sm dark:border-zinc-800 dark:bg-zinc-900 md:flex-row md:items-center md:justify-between", children: [
10796
+ /* @__PURE__ */ jsxs5("div", { children: [
10797
+ /* @__PURE__ */ jsx9("h1", { className: "text-lg font-semibold text-zinc-900 dark:text-zinc-100", children: "Fallback Editor" }),
10798
+ /* @__PURE__ */ jsx9("p", { className: "mt-1 text-sm text-zinc-500 dark:text-zinc-400", children: "Manage global and node-scoped fallback registrations with live validation hints." })
10799
+ ] }),
10800
+ /* @__PURE__ */ jsxs5("div", { className: "flex flex-wrap gap-2", children: [
10801
+ /* @__PURE__ */ jsx9(
10802
+ "button",
10803
+ {
10804
+ type: "button",
10805
+ onClick: onReset,
10806
+ disabled: resetting,
10807
+ className: "rounded-xl border border-zinc-300 bg-white px-3 py-2 text-sm font-medium text-zinc-700 hover:bg-zinc-50 disabled:cursor-not-allowed disabled:opacity-60 dark:border-zinc-700 dark:bg-zinc-950 dark:text-zinc-200 dark:hover:bg-zinc-800",
10808
+ children: resetting ? "Resetting..." : "Reset"
10809
+ }
10810
+ ),
10811
+ /* @__PURE__ */ jsx9(
10812
+ "button",
10813
+ {
10814
+ type: "button",
10815
+ onClick: onValidate,
10816
+ disabled: validating,
10817
+ className: "rounded-xl border border-zinc-300 bg-white px-3 py-2 text-sm font-medium text-zinc-700 hover:bg-zinc-50 disabled:cursor-not-allowed disabled:opacity-60 dark:border-zinc-700 dark:bg-zinc-950 dark:text-zinc-200 dark:hover:bg-zinc-800",
10818
+ children: validating ? "Validating..." : "Validate"
10819
+ }
10820
+ ),
10821
+ /* @__PURE__ */ jsx9(
10822
+ "button",
10823
+ {
10824
+ type: "button",
10825
+ onClick: onSave,
10826
+ disabled: saving,
10827
+ 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",
10828
+ children: saving ? "Saving..." : "Save"
10829
+ }
10830
+ )
10831
+ ] })
10832
+ ] });
10833
+ }
10834
+
10835
+ // src/react/fallback-editor/FallbackSettingsPanel.tsx
10836
+ import React9 from "react";
10837
+ import { jsx as jsx10, jsxs as jsxs6 } from "react/jsx-runtime";
10838
+ function FallbackSettingsPanel() {
10839
+ var _a, _b, _c;
10840
+ const { settings, saveSettings, settingsSaving } = useFallbackEditorContext();
10841
+ const [draft, setDraft] = React9.useState(settings);
10842
+ const [error, setError] = React9.useState(null);
10843
+ const [saved, setSaved] = React9.useState(false);
10844
+ React9.useEffect(() => {
10845
+ setDraft(settings);
10846
+ setSaved(false);
10847
+ setError(null);
10848
+ }, [settings]);
10849
+ const changed = JSON.stringify(draft != null ? draft : {}) !== JSON.stringify(settings != null ? settings : {});
10850
+ async function handleSave() {
10851
+ setError(null);
10852
+ setSaved(false);
10853
+ try {
10854
+ await saveSettings(draft);
10855
+ setSaved(true);
10856
+ } catch (err) {
10857
+ setError(
10858
+ err instanceof Error ? err.message : "Failed to save fallback settings."
10859
+ );
10860
+ }
10861
+ }
10862
+ function setRatePolicy(next) {
10863
+ setDraft((prev) => ({
10864
+ ...prev,
10865
+ ratePolicy: next
10866
+ }));
10867
+ }
10868
+ const ratePolicy = (_a = draft.ratePolicy) != null ? _a : { kind: "lte_primary" };
10869
+ 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: [
10870
+ /* @__PURE__ */ jsxs6("div", { className: "mb-4 flex items-start justify-between gap-3", children: [
10871
+ /* @__PURE__ */ jsxs6("div", { children: [
10872
+ /* @__PURE__ */ jsx10("h3", { className: "text-sm font-semibold text-zinc-900 dark:text-zinc-100", children: "Fallback settings" }),
10873
+ /* @__PURE__ */ jsx10("p", { className: "mt-1 text-xs text-zinc-500 dark:text-zinc-400", children: "These settings can be persisted by the host and returned into the editor." })
10874
+ ] }),
10875
+ /* @__PURE__ */ jsx10(
10876
+ "button",
10877
+ {
10878
+ type: "button",
10879
+ onClick: handleSave,
10880
+ disabled: !changed || settingsSaving,
10881
+ className: [
10882
+ "rounded-xl px-3 py-2 text-sm font-medium transition",
10883
+ !changed || settingsSaving ? "cursor-not-allowed bg-zinc-200 text-zinc-500 dark:bg-zinc-800 dark:text-zinc-500" : "bg-blue-600 text-white hover:bg-blue-700"
10884
+ ].join(" "),
10885
+ children: settingsSaving ? "Saving..." : "Save settings"
10886
+ }
10887
+ )
10888
+ ] }),
10889
+ /* @__PURE__ */ jsxs6("div", { className: "space-y-4", children: [
10890
+ /* @__PURE__ */ jsx10(
10891
+ SettingRow,
10892
+ {
10893
+ title: "Require constraint fit",
10894
+ hint: "Reject or warn when a candidate does not match effective tag constraints.",
10895
+ children: /* @__PURE__ */ jsxs6(
10896
+ "button",
10897
+ {
10898
+ type: "button",
10899
+ onClick: () => setDraft((prev) => ({
10900
+ ...prev,
10901
+ requireConstraintFit: !(prev == null ? void 0 : prev.requireConstraintFit)
10902
+ })),
10903
+ className: [
10904
+ "inline-flex items-center gap-2 rounded-full border px-3 py-2 text-sm",
10905
+ 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"
10906
+ ].join(" "),
10907
+ children: [
10908
+ /* @__PURE__ */ jsx10("span", { children: draft.requireConstraintFit ? "Enabled" : "Disabled" }),
10909
+ /* @__PURE__ */ jsx10(
10910
+ "span",
10911
+ {
10912
+ className: [
10913
+ "h-2.5 w-2.5 rounded-full",
10914
+ draft.requireConstraintFit ? "bg-green-500" : "bg-zinc-400"
10915
+ ].join(" ")
10916
+ }
10917
+ )
10918
+ ]
10919
+ }
10920
+ )
10921
+ }
10922
+ ),
10923
+ /* @__PURE__ */ jsx10(
10924
+ SettingRow,
10925
+ {
10926
+ title: "Rate policy",
10927
+ hint: "Controls how fallback service rates are compared against the primary service.",
10928
+ children: /* @__PURE__ */ jsxs6("div", { className: "flex flex-col gap-2 md:items-end", children: [
10929
+ /* @__PURE__ */ jsxs6(
10930
+ "select",
10931
+ {
10932
+ value: ratePolicy.kind,
10933
+ onChange: (e) => {
10934
+ const kind = e.target.value;
10935
+ if (kind === "lte_primary") {
10936
+ setRatePolicy({ kind: "lte_primary" });
10937
+ return;
10938
+ }
10939
+ if (kind === "within_pct") {
10940
+ setRatePolicy({
10941
+ kind: "within_pct",
10942
+ pct: ratePolicy.kind === "within_pct" || ratePolicy.kind === "at_least_pct_lower" ? ratePolicy.pct : 10
10943
+ });
10944
+ return;
10945
+ }
10946
+ setRatePolicy({
10947
+ kind: "at_least_pct_lower",
10948
+ pct: ratePolicy.kind === "within_pct" || ratePolicy.kind === "at_least_pct_lower" ? ratePolicy.pct : 10
10949
+ });
10950
+ },
10951
+ 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",
10952
+ children: [
10953
+ /* @__PURE__ */ jsx10("option", { value: "lte_primary", children: "lte_primary" }),
10954
+ /* @__PURE__ */ jsx10("option", { value: "within_pct", children: "within_pct" }),
10955
+ /* @__PURE__ */ jsx10("option", { value: "at_least_pct_lower", children: "at_least_pct_lower" })
10956
+ ]
10957
+ }
10958
+ ),
10959
+ (ratePolicy.kind === "within_pct" || ratePolicy.kind === "at_least_pct_lower") && /* @__PURE__ */ jsxs6("div", { className: "flex items-center gap-2", children: [
10960
+ /* @__PURE__ */ jsx10(
10961
+ "input",
10962
+ {
10963
+ type: "number",
10964
+ min: 0,
10965
+ step: "0.01",
10966
+ value: ratePolicy.pct,
10967
+ onChange: (e) => {
10968
+ const pct = Number(e.target.value || 0);
10969
+ if (ratePolicy.kind === "within_pct") {
10970
+ setRatePolicy({
10971
+ kind: "within_pct",
10972
+ pct
10973
+ });
10974
+ return;
10975
+ }
10976
+ setRatePolicy({
10977
+ kind: "at_least_pct_lower",
10978
+ pct
10979
+ });
10980
+ },
10981
+ 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"
10982
+ }
10983
+ ),
10984
+ /* @__PURE__ */ jsx10("span", { className: "text-sm text-zinc-500 dark:text-zinc-400", children: "%" })
10985
+ ] })
10986
+ ] })
10987
+ }
10988
+ ),
10989
+ /* @__PURE__ */ jsx10(
10990
+ SettingRow,
10991
+ {
10992
+ title: "Selection strategy",
10993
+ hint: "How valid fallback candidates are ordered in previews.",
10994
+ children: /* @__PURE__ */ jsxs6(
10995
+ "select",
10996
+ {
10997
+ value: (_b = draft.selectionStrategy) != null ? _b : "priority",
10998
+ onChange: (e) => setDraft((prev) => ({
10999
+ ...prev,
11000
+ selectionStrategy: e.target.value
11001
+ })),
11002
+ 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",
11003
+ children: [
11004
+ /* @__PURE__ */ jsx10("option", { value: "priority", children: "priority" }),
11005
+ /* @__PURE__ */ jsx10("option", { value: "cheapest", children: "cheapest" })
11006
+ ]
11007
+ }
11008
+ )
11009
+ }
11010
+ ),
11011
+ /* @__PURE__ */ jsx10(
11012
+ SettingRow,
11013
+ {
11014
+ title: "Mode",
11015
+ hint: "Use strict for enforced filtering, dev for advisory feedback.",
11016
+ children: /* @__PURE__ */ jsxs6(
11017
+ "select",
11018
+ {
11019
+ value: (_c = draft.mode) != null ? _c : "strict",
11020
+ onChange: (e) => setDraft((prev) => ({
11021
+ ...prev,
11022
+ mode: e.target.value
11023
+ })),
11024
+ 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",
11025
+ children: [
11026
+ /* @__PURE__ */ jsx10("option", { value: "strict", children: "strict" }),
11027
+ /* @__PURE__ */ jsx10("option", { value: "dev", children: "dev" })
11028
+ ]
11029
+ }
11030
+ )
11031
+ }
11032
+ )
11033
+ ] }),
11034
+ saved && !error ? /* @__PURE__ */ jsx10("div", { className: "mt-4 rounded-xl border border-green-200 bg-green-50 px-3 py-2 text-sm text-green-700 dark:border-green-900/50 dark:bg-green-950/20 dark:text-green-300", children: "Settings saved." }) : null,
11035
+ error ? /* @__PURE__ */ jsx10("div", { className: "mt-4 rounded-xl border border-red-200 bg-red-50 px-3 py-2 text-sm text-red-700 dark:border-red-900/50 dark:bg-red-950/20 dark:text-red-300", children: error }) : null
11036
+ ] });
11037
+ }
11038
+ function SettingRow({
11039
+ title,
11040
+ hint,
11041
+ children
11042
+ }) {
11043
+ return /* @__PURE__ */ jsxs6("div", { className: "flex flex-col gap-3 border-b border-dashed border-zinc-200 pb-4 last:border-b-0 last:pb-0 dark:border-zinc-800 md:flex-row md:items-center md:justify-between", children: [
11044
+ /* @__PURE__ */ jsxs6("div", { className: "max-w-xl", children: [
11045
+ /* @__PURE__ */ jsx10("div", { className: "text-sm font-medium text-zinc-900 dark:text-zinc-100", children: title }),
11046
+ /* @__PURE__ */ jsx10("div", { className: "mt-1 text-xs text-zinc-500 dark:text-zinc-400", children: hint })
11047
+ ] }),
11048
+ /* @__PURE__ */ jsx10("div", { children })
11049
+ ] });
11050
+ }
11051
+
11052
+ // src/react/fallback-editor/FallbackServiceSidebar.tsx
11053
+ import { useMemo as useMemo7, useState as useState4 } from "react";
11054
+ import { jsx as jsx11, jsxs as jsxs7 } from "react/jsx-runtime";
11055
+ function FallbackServiceSidebar() {
11056
+ const { activeServiceId, setActiveServiceId, get } = useFallbackEditor();
11057
+ const services = usePrimaryServiceList();
11058
+ const [query, setQuery] = useState4("");
11059
+ const filtered = useMemo7(() => {
11060
+ const q = query.trim().toLowerCase();
11061
+ if (!q) return services;
11062
+ return services.filter(
11063
+ (service) => {
11064
+ var _a, _b;
11065
+ return String(service.id).includes(q) || String((_a = service.name) != null ? _a : "").toLowerCase().includes(q) || String((_b = service.platform) != null ? _b : "").toLowerCase().includes(q);
11066
+ }
11067
+ );
11068
+ }, [query, services]);
11069
+ 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: [
11070
+ /* @__PURE__ */ jsxs7("div", { className: "border-b border-zinc-200 p-4 dark:border-zinc-800", children: [
11071
+ /* @__PURE__ */ jsx11("h2", { className: "text-sm font-semibold text-zinc-900 dark:text-zinc-100", children: "Primary services" }),
11072
+ /* @__PURE__ */ jsx11("p", { className: "mt-1 text-xs text-zinc-500 dark:text-zinc-400", children: "Services currently active in the builder/runtime context." })
11073
+ ] }),
11074
+ /* @__PURE__ */ jsxs7("div", { className: "flex min-h-0 flex-1 flex-col p-4", children: [
11075
+ /* @__PURE__ */ jsx11(
11076
+ "input",
11077
+ {
11078
+ value: query,
11079
+ onChange: (e) => setQuery(e.target.value),
11080
+ placeholder: "Search primary service...",
11081
+ 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"
11082
+ }
11083
+ ),
11084
+ /* @__PURE__ */ jsx11("div", { className: "mt-3 flex-1 space-y-2 overflow-auto", children: filtered.map((service) => {
11085
+ var _a, _b;
11086
+ const active = String(service.id) === String(activeServiceId);
11087
+ const count = get(service.id).length;
11088
+ return /* @__PURE__ */ jsx11(
11089
+ "button",
11090
+ {
11091
+ type: "button",
11092
+ onClick: () => setActiveServiceId(service.id),
11093
+ className: [
11094
+ "w-full rounded-2xl border p-3 text-left transition",
11095
+ 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"
11096
+ ].join(" "),
11097
+ children: /* @__PURE__ */ jsxs7("div", { className: "flex items-start justify-between gap-3", children: [
11098
+ /* @__PURE__ */ jsxs7("div", { className: "min-w-0", children: [
11099
+ /* @__PURE__ */ jsxs7("div", { className: "truncate text-sm font-semibold text-zinc-900 dark:text-zinc-100", children: [
11100
+ "#",
11101
+ String(service.id),
11102
+ " \xB7",
11103
+ " ",
11104
+ (_a = service.name) != null ? _a : "Unnamed"
11105
+ ] }),
11106
+ /* @__PURE__ */ jsxs7("div", { className: "mt-1 text-xs text-zinc-500 dark:text-zinc-400", children: [
11107
+ (_b = service.platform) != null ? _b : "Unknown",
11108
+ typeof service.rate === "number" ? ` \xB7 rate ${service.rate}` : ""
11109
+ ] })
11110
+ ] }),
11111
+ /* @__PURE__ */ jsxs7("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: [
11112
+ count,
11113
+ " reg"
11114
+ ] })
11115
+ ] })
11116
+ },
11117
+ String(service.id)
11118
+ );
11119
+ }) })
11120
+ ] })
11121
+ ] });
11122
+ }
11123
+
11124
+ // src/react/fallback-editor/FallbackRegistrationsPanel.tsx
11125
+ import React13 from "react";
11126
+
11127
+ // src/react/fallback-editor/FallbackAddCandidatesDialog.tsx
11128
+ import React11 from "react";
11129
+ import { jsx as jsx12, jsxs as jsxs8 } from "react/jsx-runtime";
11130
+ function FallbackAddCandidatesDialog({
11131
+ open,
11132
+ onClose,
11133
+ context,
11134
+ primaryId
11135
+ }) {
11136
+ const { eligible, addMany } = useFallbackEditor();
11137
+ const eligibleServices = useEligibleServiceList();
11138
+ const [query, setQuery] = React11.useState("");
11139
+ const [filterEligibleOnly, setFilterEligibleOnly] = React11.useState(true);
11140
+ const [selected, setSelected] = React11.useState(/* @__PURE__ */ new Set());
11141
+ const [submitting, setSubmitting] = React11.useState(false);
11142
+ React11.useEffect(() => {
11143
+ if (!open) {
11144
+ setQuery("");
11145
+ setFilterEligibleOnly(true);
11146
+ setSelected(/* @__PURE__ */ new Set());
11147
+ }
11148
+ }, [open]);
11149
+ const allowedIds = React11.useMemo(() => {
11150
+ if (!context) return null;
11151
+ if (!filterEligibleOnly) return null;
11152
+ return new Set(eligible(context).map((id) => String(id)));
11153
+ }, [context, filterEligibleOnly, eligible]);
11154
+ const items = React11.useMemo(() => {
11155
+ const q = query.trim().toLowerCase();
11156
+ return eligibleServices.filter((service) => {
11157
+ var _a, _b;
11158
+ if (primaryId !== void 0 && String(service.id) === String(primaryId)) {
11159
+ return false;
11160
+ }
11161
+ if (allowedIds && !allowedIds.has(String(service.id))) {
11162
+ return false;
11163
+ }
11164
+ if (!q) return true;
11165
+ return String(service.id).includes(q) || String((_a = service.name) != null ? _a : "").toLowerCase().includes(q) || String((_b = service.platform) != null ? _b : "").toLowerCase().includes(q);
11166
+ });
11167
+ }, [eligibleServices, allowedIds, query, primaryId]);
11168
+ function toggle(id) {
11169
+ setSelected((prev) => {
11170
+ const next = new Set(prev);
11171
+ const key = String(id);
11172
+ if (next.has(key)) next.delete(key);
11173
+ else next.add(key);
11174
+ return next;
11175
+ });
11176
+ }
11177
+ async function handleAdd() {
11178
+ if (!context || selected.size === 0) return;
11179
+ setSubmitting(true);
11180
+ try {
11181
+ const ids = items.filter((item) => selected.has(String(item.id))).map((item) => item.id);
11182
+ addMany(context, ids);
11183
+ onClose();
11184
+ } finally {
11185
+ setSubmitting(false);
11186
+ }
11187
+ }
11188
+ if (!open || !context) return null;
11189
+ 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: [
11190
+ /* @__PURE__ */ jsxs8("div", { className: "border-b border-zinc-200 p-4 dark:border-zinc-800", children: [
11191
+ /* @__PURE__ */ jsx12("h3", { className: "text-base font-semibold text-zinc-900 dark:text-zinc-100", children: "Add fallback services" }),
11192
+ /* @__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." })
11193
+ ] }),
11194
+ /* @__PURE__ */ jsxs8("div", { className: "flex flex-col gap-3 p-4", children: [
11195
+ /* @__PURE__ */ jsx12(
11196
+ "input",
11197
+ {
11198
+ value: query,
11199
+ onChange: (e) => setQuery(e.target.value),
11200
+ placeholder: "Search eligible services...",
11201
+ 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"
11202
+ }
11203
+ ),
11204
+ /* @__PURE__ */ jsxs8("label", { className: "inline-flex items-center gap-2 text-sm text-zinc-700 dark:text-zinc-300", children: [
11205
+ /* @__PURE__ */ jsx12(
11206
+ "input",
11207
+ {
11208
+ type: "checkbox",
11209
+ checked: filterEligibleOnly,
11210
+ onChange: (e) => setFilterEligibleOnly(e.target.checked),
11211
+ className: "h-4 w-4 rounded border-zinc-300"
11212
+ }
11213
+ ),
11214
+ "Filter eligible only"
11215
+ ] }),
11216
+ /* @__PURE__ */ jsx12(
11217
+ VirtualServiceList,
11218
+ {
11219
+ items,
11220
+ selected,
11221
+ onToggle: toggle,
11222
+ emptyText: "No eligible services found."
11223
+ }
11224
+ )
11225
+ ] }),
11226
+ /* @__PURE__ */ jsxs8("div", { className: "flex items-center justify-between border-t border-zinc-200 p-4 dark:border-zinc-800", children: [
11227
+ /* @__PURE__ */ jsxs8("div", { className: "text-sm text-zinc-500 dark:text-zinc-400", children: [
11228
+ selected.size,
11229
+ " selected"
11230
+ ] }),
11231
+ /* @__PURE__ */ jsxs8("div", { className: "flex gap-2", children: [
11232
+ /* @__PURE__ */ jsx12(
11233
+ "button",
11234
+ {
11235
+ type: "button",
11236
+ onClick: onClose,
11237
+ 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",
11238
+ children: "Cancel"
11239
+ }
11240
+ ),
11241
+ /* @__PURE__ */ jsx12(
11242
+ "button",
11243
+ {
11244
+ type: "button",
11245
+ onClick: handleAdd,
11246
+ disabled: selected.size === 0 || submitting,
11247
+ 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",
11248
+ children: submitting ? "Adding..." : "Add selected"
11249
+ }
11250
+ )
11251
+ ] })
11252
+ ] })
11253
+ ] }) });
11254
+ }
11255
+
11256
+ // src/react/fallback-editor/FallbackAddRegistrationDialog.tsx
11257
+ import React12 from "react";
11258
+ import { jsx as jsx13, jsxs as jsxs9 } from "react/jsx-runtime";
11259
+ function FallbackAddRegistrationDialog({
11260
+ open,
11261
+ onClose,
11262
+ onSelect
11263
+ }) {
11264
+ const { activeServiceId, serviceProps, snapshot } = useFallbackEditor();
11265
+ const registrations = useActiveFallbackRegistrations();
11266
+ const [scope, setScope] = React12.useState("global");
11267
+ const [nodeId, setNodeId] = React12.useState("");
11268
+ const mode = React12.useMemo(() => {
11269
+ if (snapshot) return "snapshot";
11270
+ if (serviceProps) return "props";
11271
+ return "none";
11272
+ }, [snapshot, serviceProps]);
11273
+ React12.useEffect(() => {
11274
+ if (open) {
11275
+ setScope("global");
11276
+ setNodeId("");
11277
+ }
11278
+ }, [open]);
11279
+ const hasGlobal = React12.useMemo(() => {
11280
+ return registrations.some((r) => r.scope === "global");
11281
+ }, [registrations]);
11282
+ const nodeTargets = React12.useMemo(() => {
11283
+ var _a, _b, _c, _d, _e, _f, _g, _h, _i, _j, _k;
11284
+ if (activeServiceId === void 0 || activeServiceId === null) {
11285
+ return [];
11286
+ }
11287
+ if (mode === "snapshot" && (snapshot == null ? void 0 : snapshot.serviceMap)) {
11288
+ const out = [];
11289
+ for (const [id, primaryIds] of Object.entries(
11290
+ snapshot.serviceMap
11291
+ )) {
11292
+ const matchesPrimary = (primaryIds != null ? primaryIds : []).some(
11293
+ (serviceId) => String(serviceId) === String(activeServiceId)
11294
+ );
11295
+ if (!matchesPrimary) continue;
11296
+ const meta = resolveNodeMeta(serviceProps, id);
11297
+ out.push({
11298
+ id,
11299
+ kind: meta.kind,
11300
+ label: meta.label,
11301
+ serviceId: activeServiceId
11302
+ });
11303
+ }
11304
+ const activeTagId = (_a = snapshot.selection) == null ? void 0 : _a.tag;
11305
+ out.sort((a, b) => {
11306
+ if (activeTagId && a.id === activeTagId && b.id !== activeTagId) {
11307
+ return -1;
11308
+ }
11309
+ if (activeTagId && b.id === activeTagId && a.id !== activeTagId) {
11310
+ return 1;
11311
+ }
11312
+ return a.label.localeCompare(b.label);
11313
+ });
11314
+ const seen = /* @__PURE__ */ new Set();
11315
+ return out.filter((item) => {
11316
+ if (seen.has(item.id)) return false;
11317
+ seen.add(item.id);
11318
+ return true;
11319
+ });
11320
+ }
11321
+ if (mode === "props" && serviceProps) {
11322
+ const out = [];
11323
+ for (const tag of (_b = serviceProps.filters) != null ? _b : []) {
11324
+ if ((tag == null ? void 0 : tag.service_id) === void 0 || (tag == null ? void 0 : tag.service_id) === null) {
11325
+ continue;
11326
+ }
11327
+ if (String(tag.service_id) !== String(activeServiceId)) {
11328
+ continue;
11329
+ }
11330
+ out.push({
11331
+ id: tag.id,
11332
+ kind: "tag",
11333
+ label: (_d = (_c = tag.label) != null ? _c : tag.title) != null ? _d : tag.id,
11334
+ serviceId: tag.service_id
11335
+ });
11336
+ }
11337
+ for (const field of (_e = serviceProps.fields) != null ? _e : []) {
11338
+ if ((field == null ? void 0 : field.service_id) !== void 0 && (field == null ? void 0 : field.service_id) !== null && String(field.service_id) === String(activeServiceId)) {
11339
+ out.push({
11340
+ id: field.id,
11341
+ kind: "field",
11342
+ label: (_g = (_f = field.label) != null ? _f : field.title) != null ? _g : field.id,
11343
+ serviceId: field.service_id
11344
+ });
11345
+ }
11346
+ for (const option of (_h = field.options) != null ? _h : []) {
11347
+ if ((option == null ? void 0 : option.service_id) === void 0 || (option == null ? void 0 : option.service_id) === null) {
11348
+ continue;
11349
+ }
11350
+ if (String(option.service_id) !== String(activeServiceId)) {
11351
+ continue;
11352
+ }
11353
+ out.push({
11354
+ id: option.id,
11355
+ kind: "option",
11356
+ label: (_k = (_i = option.label) != null ? _i : option.title) != null ? _k : String(
11357
+ (_j = option.value) != null ? _j : option.id
11358
+ ),
11359
+ serviceId: option.service_id
11360
+ });
11361
+ }
11362
+ }
11363
+ const seen = /* @__PURE__ */ new Set();
11364
+ return out.filter((item) => {
11365
+ if (seen.has(item.id)) return false;
11366
+ seen.add(item.id);
11367
+ return true;
11368
+ });
11369
+ }
11370
+ return [];
11371
+ }, [mode, snapshot, serviceProps, activeServiceId]);
11372
+ React12.useEffect(() => {
11373
+ if (hasGlobal && scope === "global") {
11374
+ setScope("node");
11375
+ }
11376
+ }, [hasGlobal, scope]);
11377
+ React12.useEffect(() => {
11378
+ if (scope === "node" && nodeId) {
11379
+ const exists = nodeTargets.some((node) => node.id === nodeId);
11380
+ if (!exists) setNodeId("");
11381
+ }
11382
+ }, [scope, nodeId, nodeTargets]);
11383
+ function handleContinue() {
11384
+ var _a;
11385
+ if (activeServiceId === void 0 || activeServiceId === null) return;
11386
+ if (scope === "global") {
11387
+ onSelect(
11388
+ {
11389
+ scope: "global",
11390
+ primary: activeServiceId
11391
+ },
11392
+ activeServiceId
11393
+ );
11394
+ return;
11395
+ }
11396
+ if (!nodeId) return;
11397
+ const node = nodeTargets.find((n) => n.id === nodeId);
11398
+ onSelect(
11399
+ {
11400
+ scope: "node",
11401
+ nodeId
11402
+ },
11403
+ (_a = node == null ? void 0 : node.serviceId) != null ? _a : activeServiceId
11404
+ );
11405
+ }
11406
+ if (!open) return null;
11407
+ const nodeScopeDisabled = nodeTargets.length === 0;
11408
+ 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: [
11409
+ /* @__PURE__ */ jsxs9("div", { className: "border-b border-zinc-200 p-4 dark:border-zinc-800", children: [
11410
+ /* @__PURE__ */ jsx13("h3", { className: "text-base font-semibold text-zinc-900 dark:text-zinc-100", children: "Add registration" }),
11411
+ /* @__PURE__ */ jsx13("p", { className: "mt-1 text-sm text-zinc-500 dark:text-zinc-400", children: "Choose the registration scope before selecting fallback candidates." })
11412
+ ] }),
11413
+ /* @__PURE__ */ jsxs9("div", { className: "space-y-4 p-4", children: [
11414
+ /* @__PURE__ */ jsxs9("div", { className: "space-y-2", children: [
11415
+ /* @__PURE__ */ jsx13("div", { className: "text-sm font-medium text-zinc-900 dark:text-zinc-100", children: "Scope" }),
11416
+ /* @__PURE__ */ jsxs9("div", { className: "grid gap-2", children: [
11417
+ !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: [
11418
+ /* @__PURE__ */ jsx13(
11419
+ "input",
11420
+ {
11421
+ type: "radio",
11422
+ name: "scope",
11423
+ checked: scope === "global",
11424
+ onChange: () => setScope("global"),
11425
+ className: "mt-1 h-4 w-4"
11426
+ }
11427
+ ),
11428
+ /* @__PURE__ */ jsxs9("div", { children: [
11429
+ /* @__PURE__ */ jsx13("div", { className: "text-sm font-medium text-zinc-900 dark:text-zinc-100", children: "Global" }),
11430
+ /* @__PURE__ */ jsx13("div", { className: "mt-1 text-xs text-zinc-500 dark:text-zinc-400", children: "Use one global registration for this primary service." })
11431
+ ] })
11432
+ ] }),
11433
+ /* @__PURE__ */ jsxs9(
11434
+ "label",
11435
+ {
11436
+ className: [
11437
+ "flex items-start gap-3 rounded-xl border p-3",
11438
+ nodeScopeDisabled ? "cursor-not-allowed border-zinc-200 opacity-60 dark:border-zinc-800" : "cursor-pointer border-zinc-200 dark:border-zinc-800"
11439
+ ].join(" "),
11440
+ children: [
11441
+ /* @__PURE__ */ jsx13(
11442
+ "input",
11443
+ {
11444
+ type: "radio",
11445
+ name: "scope",
11446
+ checked: scope === "node",
11447
+ onChange: () => setScope("node"),
11448
+ disabled: nodeScopeDisabled,
11449
+ className: "mt-1 h-4 w-4"
11450
+ }
11451
+ ),
11452
+ /* @__PURE__ */ jsxs9("div", { children: [
11453
+ /* @__PURE__ */ jsx13("div", { className: "text-sm font-medium text-zinc-900 dark:text-zinc-100", children: "Node" }),
11454
+ /* @__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." })
11455
+ ] })
11456
+ ]
11457
+ }
11458
+ )
11459
+ ] })
11460
+ ] }),
11461
+ scope === "node" && /* @__PURE__ */ jsxs9("div", { className: "space-y-2", children: [
11462
+ /* @__PURE__ */ jsx13("div", { className: "text-sm font-medium text-zinc-900 dark:text-zinc-100", children: "Node id" }),
11463
+ /* @__PURE__ */ jsxs9(
11464
+ "select",
11465
+ {
11466
+ value: nodeId,
11467
+ onChange: (e) => setNodeId(e.target.value),
11468
+ 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",
11469
+ children: [
11470
+ /* @__PURE__ */ jsx13("option", { value: "", children: "Select node\u2026" }),
11471
+ nodeTargets.map((node) => /* @__PURE__ */ jsxs9("option", { value: node.id, children: [
11472
+ "[",
11473
+ node.kind,
11474
+ "] ",
11475
+ node.label,
11476
+ " \xB7 #",
11477
+ String(node.serviceId)
11478
+ ] }, node.id))
11479
+ ]
11480
+ }
11481
+ ),
11482
+ 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
11483
+ ] })
11484
+ ] }),
11485
+ /* @__PURE__ */ jsxs9("div", { className: "flex items-center justify-end gap-2 border-t border-zinc-200 p-4 dark:border-zinc-800", children: [
11486
+ /* @__PURE__ */ jsx13(
11487
+ "button",
11488
+ {
11489
+ type: "button",
11490
+ onClick: onClose,
11491
+ 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",
11492
+ children: "Cancel"
11493
+ }
11494
+ ),
11495
+ /* @__PURE__ */ jsx13(
11496
+ "button",
11497
+ {
11498
+ type: "button",
11499
+ onClick: handleContinue,
11500
+ disabled: activeServiceId === void 0 || scope === "node" && !nodeId,
11501
+ 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",
11502
+ children: "Continue"
11503
+ }
11504
+ )
11505
+ ] })
11506
+ ] }) });
11507
+ }
11508
+ function resolveNodeMeta(props, nodeId) {
11509
+ var _a, _b, _c, _d, _e, _f, _g, _h, _i, _j, _k;
11510
+ if (!props) {
11511
+ return { kind: "node", label: nodeId };
11512
+ }
11513
+ const tag = (_a = props.filters) == null ? void 0 : _a.find((t) => t.id === nodeId);
11514
+ if (tag) {
11515
+ return {
11516
+ kind: "tag",
11517
+ label: (_c = (_b = tag.label) != null ? _b : tag.title) != null ? _c : tag.id
11518
+ };
11519
+ }
11520
+ const field = (_d = props.fields) == null ? void 0 : _d.find((f) => f.id === nodeId);
11521
+ if (field) {
11522
+ return {
11523
+ kind: "field",
11524
+ label: (_f = (_e = field.label) != null ? _e : field.title) != null ? _f : field.id
11525
+ };
11526
+ }
11527
+ for (const fieldItem of (_g = props.fields) != null ? _g : []) {
11528
+ const option = (_h = fieldItem.options) == null ? void 0 : _h.find((o) => o.id === nodeId);
11529
+ if (option) {
11530
+ return {
11531
+ kind: "option",
11532
+ label: (_k = (_i = option.label) != null ? _i : option.title) != null ? _k : String((_j = option.value) != null ? _j : option.id)
11533
+ };
11534
+ }
11535
+ }
11536
+ return { kind: "node", label: nodeId };
11537
+ }
11538
+
11539
+ // src/react/fallback-editor/FallbackRegistrationsPanel.tsx
11540
+ import { Fragment, jsx as jsx14, jsxs as jsxs10 } from "react/jsx-runtime";
11541
+ function FallbackRegistrationsPanel() {
11542
+ const { activeServiceId, remove, clear, check } = useFallbackEditor();
11543
+ const registrations = useActiveFallbackRegistrations();
11544
+ const eligibleServices = useEligibleServiceList();
11545
+ const [candidatePickerOpen, setCandidatePickerOpen] = React13.useState(false);
11546
+ const [candidateContext, setCandidateContext] = React13.useState(null);
11547
+ const [candidatePrimaryId, setCandidatePrimaryId] = React13.useState(void 0);
11548
+ const [registrationDialogOpen, setRegistrationDialogOpen] = React13.useState(false);
11549
+ const makeContext = React13.useCallback(
11550
+ (registration) => {
11551
+ if (registration.scope === "global") {
11552
+ return {
11553
+ scope: "global",
11554
+ primary: registration.primary
11555
+ };
11556
+ }
11557
+ return {
11558
+ scope: "node",
11559
+ nodeId: registration.scopeId
11560
+ };
11561
+ },
11562
+ []
11563
+ );
11564
+ const openCandidatePicker = React13.useCallback(
11565
+ (context, primaryId) => {
11566
+ setCandidateContext(context);
11567
+ setCandidatePrimaryId(primaryId);
11568
+ setCandidatePickerOpen(true);
11569
+ },
11570
+ []
11571
+ );
11572
+ if (activeServiceId === void 0 || activeServiceId === null) {
11573
+ 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." }) });
11574
+ }
11575
+ return /* @__PURE__ */ jsxs10(Fragment, { children: [
11576
+ /* @__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: [
11577
+ /* @__PURE__ */ jsxs10("div", { className: "mb-4 flex items-start justify-between gap-3", children: [
11578
+ /* @__PURE__ */ jsxs10("div", { children: [
11579
+ /* @__PURE__ */ jsx14("h3", { className: "text-sm font-semibold text-zinc-900 dark:text-zinc-100", children: "Registered fallbacks" }),
11580
+ /* @__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." })
11581
+ ] }),
11582
+ /* @__PURE__ */ jsx14(
11583
+ "button",
11584
+ {
11585
+ type: "button",
11586
+ onClick: () => setRegistrationDialogOpen(true),
11587
+ 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",
11588
+ children: "Add registration"
11589
+ }
11590
+ )
11591
+ ] }),
11592
+ /* @__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) => {
11593
+ var _a;
11594
+ const context = makeContext(reg);
11595
+ const candidates = reg.services;
11596
+ return /* @__PURE__ */ jsxs10(
11597
+ "div",
11598
+ {
11599
+ className: "rounded-2xl border border-zinc-200 bg-zinc-50 p-4 dark:border-zinc-800 dark:bg-zinc-950",
11600
+ children: [
11601
+ /* @__PURE__ */ jsxs10("div", { className: "flex flex-wrap items-start justify-between gap-3", children: [
11602
+ /* @__PURE__ */ jsxs10("div", { children: [
11603
+ /* @__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}` }),
11604
+ /* @__PURE__ */ jsxs10("div", { className: "mt-1 text-xs text-zinc-500 dark:text-zinc-400", children: [
11605
+ "Primary #",
11606
+ String(reg.primary)
11607
+ ] })
11608
+ ] }),
11609
+ /* @__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: [
11610
+ reg.scope,
11611
+ reg.scopeId ? ` \xB7 ${reg.scopeId}` : ""
11612
+ ] })
11613
+ ] }),
11614
+ /* @__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) => {
11615
+ var _a2;
11616
+ const preview = check(context, [
11617
+ candidate
11618
+ ]);
11619
+ const rejected = preview.rejected[0];
11620
+ 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";
11621
+ const service = eligibleServices.find(
11622
+ (s) => String(s.id) === String(candidate)
11623
+ );
11624
+ return /* @__PURE__ */ jsxs10(
11625
+ "div",
11626
+ {
11627
+ className: `inline-flex items-center gap-2 rounded-xl border px-3 py-2 text-xs ${tone}`,
11628
+ children: [
11629
+ /* @__PURE__ */ jsx14("span", { children: service ? `#${String(service.id)} \xB7 ${(_a2 = service.name) != null ? _a2 : "Unnamed"}` : `#${String(candidate)}` }),
11630
+ rejected ? /* @__PURE__ */ jsx14("span", { className: "rounded-full border border-current/20 px-2 py-0.5 text-[10px]", children: rejected.reasons.join(
11631
+ ", "
11632
+ ) }) : /* @__PURE__ */ jsx14("span", { className: "rounded-full border border-current/20 px-2 py-0.5 text-[10px]", children: "valid" }),
11633
+ /* @__PURE__ */ jsx14(
11634
+ "button",
11635
+ {
11636
+ type: "button",
11637
+ onClick: () => remove(
11638
+ context,
11639
+ candidate
11640
+ ),
11641
+ className: "text-current/70 hover:text-current",
11642
+ children: "\xD7"
11643
+ }
11644
+ )
11645
+ ]
11646
+ },
11647
+ String(candidate)
11648
+ );
11649
+ }) }),
11650
+ /* @__PURE__ */ jsxs10("div", { className: "mt-4 flex gap-2", children: [
11651
+ /* @__PURE__ */ jsx14(
11652
+ "button",
11653
+ {
11654
+ type: "button",
11655
+ onClick: () => openCandidatePicker(
11656
+ context,
11657
+ reg.primary
11658
+ ),
11659
+ 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",
11660
+ children: "Add fallback"
11661
+ }
11662
+ ),
11663
+ /* @__PURE__ */ jsx14(
11664
+ "button",
11665
+ {
11666
+ type: "button",
11667
+ onClick: () => clear(context),
11668
+ 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",
11669
+ children: "Clear"
11670
+ }
11671
+ )
11672
+ ] })
11673
+ ]
11674
+ },
11675
+ `${reg.scope}:${String((_a = reg.scopeId) != null ? _a : "global")}:${index}`
11676
+ );
11677
+ }) })
11678
+ ] }),
11679
+ /* @__PURE__ */ jsx14(
11680
+ FallbackAddRegistrationDialog,
11681
+ {
11682
+ open: registrationDialogOpen,
11683
+ onClose: () => setRegistrationDialogOpen(false),
11684
+ onSelect: (context, primaryId) => {
11685
+ setRegistrationDialogOpen(false);
11686
+ openCandidatePicker(context, primaryId);
11687
+ }
11688
+ }
11689
+ ),
11690
+ /* @__PURE__ */ jsx14(
11691
+ FallbackAddCandidatesDialog,
11692
+ {
11693
+ open: candidatePickerOpen,
11694
+ onClose: () => setCandidatePickerOpen(false),
11695
+ context: candidateContext,
11696
+ primaryId: candidatePrimaryId
11697
+ }
11698
+ )
11699
+ ] });
11700
+ }
9819
11701
  export {
9820
11702
  CanvasAPI,
9821
11703
  EventBus,
11704
+ FallbackAddCandidatesDialog,
11705
+ FallbackAddRegistrationDialog,
11706
+ FallbackDetailsPanel,
11707
+ FallbackEditor,
11708
+ FallbackEditorHeader,
11709
+ FallbackEditorProvider,
11710
+ FallbackRegistrationsPanel,
11711
+ FallbackServiceSidebar,
11712
+ FallbackSettingsPanel,
9822
11713
  FormProvider,
9823
11714
  OrderFlowProvider,
9824
11715
  Provider,
11716
+ VirtualServiceList,
9825
11717
  Wrapper,
9826
11718
  createInputRegistry,
9827
11719
  registerEntries,
9828
11720
  resolveInputDescriptor,
11721
+ useActiveFallbackRegistrations,
11722
+ useEligibleServiceList,
11723
+ useFallbackEditor,
11724
+ useFallbackEditorContext,
9829
11725
  useFormApi,
9830
11726
  useInputs,
9831
11727
  useOptionalFormApi,
9832
11728
  useOrderFlow,
9833
- useOrderFlowContext
11729
+ useOrderFlowContext,
11730
+ usePrimaryServiceList
9834
11731
  };
9835
11732
  //# sourceMappingURL=index.js.map