@timeax/service-builder 0.2.0 → 0.2.1

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.
package/dist/index.js CHANGED
@@ -3453,6 +3453,23 @@ function StatusDot({ tone = "default", className }) {
3453
3453
 
3454
3454
  // src/builder/service-context.ts
3455
3455
  import { createBuilder } from "@timeax/digital-service-engine/core";
3456
+ function createEmptyServiceContextSnapshot(state, props) {
3457
+ const filters = props?.filters ?? [];
3458
+ const tags = filters.map((tag) => ({
3459
+ id: tag.id,
3460
+ label: tag.label,
3461
+ description: describeTag(tag)
3462
+ }));
3463
+ const selectedTag = filters.find((tag) => tag.id === state.selectedTagId) ?? null;
3464
+ return {
3465
+ state,
3466
+ tags,
3467
+ selectedTag,
3468
+ buttonGroups: [],
3469
+ visibleFieldIds: [],
3470
+ usedServiceIds: []
3471
+ };
3472
+ }
3456
3473
  function createDefaultServiceContext(props, preferredTagId) {
3457
3474
  const tagIds = (props?.filters ?? []).map((tag) => tag.id);
3458
3475
  return {
@@ -3468,43 +3485,37 @@ function sanitizeServiceContext(snapshot, state) {
3468
3485
  if (selectedButtons.length === state.selectedButtons.length) return state;
3469
3486
  return { ...state, selectedButtons };
3470
3487
  }
3471
- function buildServiceContextSnapshot(args) {
3472
- const props = args.props ?? void 0;
3473
- const serviceMap = args.services ?? {};
3474
- const tags = (props?.filters ?? []).map((tag) => ({
3475
- id: tag.id,
3476
- label: tag.label,
3477
- description: describeTag(tag)
3478
- }));
3479
- const selectedTag = (props?.filters ?? []).find((tag) => tag.id === args.state.selectedTagId) ?? null;
3480
- if (!props || !selectedTag) {
3481
- return {
3482
- state: args.state,
3483
- tags,
3484
- selectedTag,
3485
- buttonGroups: [],
3486
- visibleFieldIds: [],
3487
- usedServiceIds: []
3488
- };
3489
- }
3490
- const sandbox = createBuilder({ serviceMap });
3491
- sandbox.load(props);
3492
- const visibleFieldIds = sandbox.visibleFields(selectedTag.id, args.state.selectedButtons);
3493
- const buttonGroups = buildButtonGroups(props.fields ?? [], visibleFieldIds);
3494
- const usedServiceIds = collectUsedServiceIds({
3495
- props,
3496
- tagId: selectedTag.id,
3497
- visibleFieldIds,
3498
- selectedButtons: args.state.selectedButtons
3488
+ async function buildServiceContextSnapshot(args) {
3489
+ return new Promise((resolve) => {
3490
+ setTimeout(() => {
3491
+ const props = args.props ?? void 0;
3492
+ const serviceMap = args.services ?? {};
3493
+ const emptySnapshot = createEmptyServiceContextSnapshot(args.state, props);
3494
+ const { tags, selectedTag } = emptySnapshot;
3495
+ if (!props || !selectedTag) {
3496
+ resolve(emptySnapshot);
3497
+ return;
3498
+ }
3499
+ const sandbox = createBuilder({ serviceMap });
3500
+ sandbox.load(props);
3501
+ const visibleFieldIds = sandbox.visibleFields(selectedTag.id, args.state.selectedButtons);
3502
+ const buttonGroups = buildButtonGroups(props.fields ?? [], visibleFieldIds);
3503
+ const usedServiceIds = collectUsedServiceIds({
3504
+ props,
3505
+ tagId: selectedTag.id,
3506
+ visibleFieldIds,
3507
+ selectedButtons: args.state.selectedButtons
3508
+ });
3509
+ resolve({
3510
+ state: args.state,
3511
+ tags,
3512
+ selectedTag,
3513
+ buttonGroups,
3514
+ visibleFieldIds,
3515
+ usedServiceIds
3516
+ });
3517
+ }, 0);
3499
3518
  });
3500
- return {
3501
- state: args.state,
3502
- tags,
3503
- selectedTag,
3504
- buttonGroups,
3505
- visibleFieldIds,
3506
- usedServiceIds
3507
- };
3508
3519
  }
3509
3520
  function buildServiceRowVM(args) {
3510
3521
  const service = args.services?.[args.summary.id] ?? null;
@@ -6444,6 +6455,8 @@ function CanvasPanel({
6444
6455
  const [presentationVersion, setPresentationVersion] = useState8(0);
6445
6456
  const lastSyncedSnapshotHashRef = useRef5(initialSnapshotHash);
6446
6457
  const guardedEditorRef = useRef5(null);
6458
+ const liveSnapshotRequestRef = useRef5(0);
6459
+ const checksByIdRequestRef = useRef5(0);
6447
6460
  const servicesMap = ws.services?.data ?? {};
6448
6461
  const errorNodeIds = useMemo9(
6449
6462
  () => new Set((errors.merged.validation ?? []).map((row) => row.nodeId).filter(Boolean)),
@@ -6682,21 +6695,45 @@ function CanvasPanel({
6682
6695
  }),
6683
6696
  [resolvedCurrentTagId, selectedButtons]
6684
6697
  );
6685
- const liveContextSnapshot = useMemo9(
6686
- () => buildServiceContextSnapshot({ props: canvas.props, services: servicesMap, state: liveContextState }),
6687
- [canvas.props, liveContextState, servicesMap]
6688
- );
6689
- const checksById = useMemo9(() => {
6690
- if (!liveContextSnapshot.selectedTag) return /* @__PURE__ */ new Map();
6698
+ const [liveContextSnapshot, setLiveContextSnapshot] = useState8(() => createEmptyServiceContextSnapshot(liveContextState, canvas.props));
6699
+ useEffect8(() => {
6700
+ const requestId = ++liveSnapshotRequestRef.current;
6701
+ const fallback = createEmptyServiceContextSnapshot(liveContextState, canvas.props);
6702
+ setLiveContextSnapshot(fallback);
6703
+ buildServiceContextSnapshot({ props: canvas.props, services: servicesMap, state: liveContextState }).then((snapshot) => {
6704
+ if (liveSnapshotRequestRef.current !== requestId) return;
6705
+ setLiveContextSnapshot(snapshot);
6706
+ }).catch(() => {
6707
+ if (liveSnapshotRequestRef.current !== requestId) return;
6708
+ setLiveContextSnapshot(fallback);
6709
+ });
6710
+ }, [canvas.props, liveContextState, servicesMap]);
6711
+ const [checksById, setChecksById] = useState8(/* @__PURE__ */ new Map());
6712
+ useEffect8(() => {
6713
+ const requestId = ++checksByIdRequestRef.current;
6714
+ if (!liveContextSnapshot.selectedTag) {
6715
+ setChecksById(/* @__PURE__ */ new Map());
6716
+ return;
6717
+ }
6691
6718
  const candidateIds = Object.values(servicesMap).map((service) => service.id);
6692
- const checks = canvas.api.editor.filterServicesForVisibleGroup(candidateIds, {
6693
- tagId: liveContextSnapshot.selectedTag.id,
6694
- selectedButtons: liveContextSnapshot.state.selectedButtons,
6695
- usedServiceIds: liveContextSnapshot.usedServiceIds,
6696
- effectiveConstraints: liveContextSnapshot.selectedTag.constraints,
6697
- policies: ws.policies?.policies?.data ?? []
6719
+ new Promise((resolve) => {
6720
+ setTimeout(() => {
6721
+ const checks = canvas.api.editor.filterServicesForVisibleGroup(candidateIds, {
6722
+ tagId: liveContextSnapshot.selectedTag.id,
6723
+ selectedButtons: liveContextSnapshot.state.selectedButtons,
6724
+ usedServiceIds: liveContextSnapshot.usedServiceIds,
6725
+ effectiveConstraints: liveContextSnapshot.selectedTag.constraints,
6726
+ policies: ws.policies?.policies?.data ?? []
6727
+ });
6728
+ resolve(Array.isArray(checks) ? checks : []);
6729
+ }, 0);
6730
+ }).then((checks) => {
6731
+ if (checksByIdRequestRef.current !== requestId) return;
6732
+ setChecksById(new Map(checks.map((check) => [String(check.id), check])));
6733
+ }).catch(() => {
6734
+ if (checksByIdRequestRef.current !== requestId) return;
6735
+ setChecksById(/* @__PURE__ */ new Map());
6698
6736
  });
6699
- return new Map(checks.map((check) => [String(check.id), check]));
6700
6737
  }, [canvas.api.editor, liveContextSnapshot, servicesMap, ws.policies?.policies?.data]);
6701
6738
  const serviceContextRows = useMemo9(() => {
6702
6739
  return liveContextSnapshot.usedServiceIds.map((id) => {
@@ -11916,6 +11953,76 @@ function IncludesIconAction({ kind, onClick, disabled, title, icon, className })
11916
11953
  );
11917
11954
  }
11918
11955
 
11956
+ // src/builder/compatibility-runner.ts
11957
+ var DEFAULT_CHUNK_SIZE = 100;
11958
+ function yieldToMainThread() {
11959
+ return new Promise((resolve) => {
11960
+ setTimeout(() => resolve(), 0);
11961
+ });
11962
+ }
11963
+ async function runCompatibilityChecksIncremental(input) {
11964
+ const { editor, args, cache, cacheKey } = input;
11965
+ const chunkSize = input.chunkSize ?? DEFAULT_CHUNK_SIZE;
11966
+ const candidateKeys = input.candidateIds.map((id) => String(id));
11967
+ const checksById = /* @__PURE__ */ new Map();
11968
+ const missingIds = [];
11969
+ for (let index = 0; index < input.candidateIds.length; index += 1) {
11970
+ const candidateId = input.candidateIds[index];
11971
+ const candidateKey = candidateKeys[index];
11972
+ const entryKey = `${cacheKey}|${candidateKey}`;
11973
+ const cached = cache.get(entryKey);
11974
+ if (cached) {
11975
+ checksById.set(candidateKey, cached);
11976
+ continue;
11977
+ }
11978
+ missingIds.push(candidateId);
11979
+ }
11980
+ if (!missingIds.length) {
11981
+ return candidateKeys.map((id) => checksById.get(id)).filter(Boolean);
11982
+ }
11983
+ for (let start = 0; start < missingIds.length; start += chunkSize) {
11984
+ if (input.shouldAbort?.()) return [];
11985
+ await yieldToMainThread();
11986
+ if (input.shouldAbort?.()) return [];
11987
+ const chunk = missingIds.slice(start, start + chunkSize);
11988
+ const checks = editor.filterServicesForVisibleGroup?.(chunk, args) ?? [];
11989
+ for (const check of checks) {
11990
+ const id = check?.id != null ? String(check.id) : null;
11991
+ if (!id) continue;
11992
+ checksById.set(id, check);
11993
+ cache.set(`${cacheKey}|${id}`, check);
11994
+ }
11995
+ }
11996
+ return candidateKeys.map((id) => checksById.get(id)).filter(Boolean);
11997
+ }
11998
+ function createCompatibilityCacheKey(input) {
11999
+ const selectedButtons = [...input.selectedButtons].sort().join(",");
12000
+ const usedServiceIds = [...input.usedServiceIds].sort().join(",");
12001
+ const primaryRate = input.primaryRate != null ? String(input.primaryRate) : "";
12002
+ const primaryServiceId = input.primaryServiceId ?? "";
12003
+ return [
12004
+ `tag:${input.tagId}`,
12005
+ `buttons:${selectedButtons}`,
12006
+ `used:${usedServiceIds}`,
12007
+ `policies:${input.policiesKey}`,
12008
+ `mode:${input.rateContextMode}`,
12009
+ `source:${input.rateContextSource}`,
12010
+ `rate:${primaryRate}`,
12011
+ `service:${primaryServiceId}`
12012
+ ].join("|");
12013
+ }
12014
+ function areServiceContextStatesEqual(left, right) {
12015
+ if (left.selectedTagId !== right.selectedTagId) return false;
12016
+ if (left.strictSafety !== right.strictSafety) return false;
12017
+ if (left.enforcePolicies !== right.enforcePolicies) return false;
12018
+ if (left.selectedButtons.length !== right.selectedButtons.length) return false;
12019
+ const leftSet = new Set(left.selectedButtons);
12020
+ for (const id of right.selectedButtons) {
12021
+ if (!leftSet.has(id)) return false;
12022
+ }
12023
+ return true;
12024
+ }
12025
+
11919
12026
  // src/components/service-meta-popover.tsx
11920
12027
  import { useEffect as useEffect18, useMemo as useMemo24, useRef as useRef11, useState as useState25 } from "react";
11921
12028
  import { FiInfo } from "react-icons/fi";
@@ -12045,6 +12152,27 @@ function ServiceMetaPopover({
12045
12152
  ] });
12046
12153
  }
12047
12154
 
12155
+ // src/hooks/use-service-catalog-state.ts
12156
+ import { useEffect as useEffect19, useState as useState26 } from "react";
12157
+ function useServiceCatalogState(canvasApi, editor) {
12158
+ const [catalogState, setCatalogState] = useState26(null);
12159
+ useEffect19(() => {
12160
+ const resolveCatalog = (catalog) => catalog ?? editor.getCatalog?.() ?? editor.ensureCatalog?.() ?? null;
12161
+ setCatalogState(resolveCatalog());
12162
+ const offCatalogChange = canvasApi.on("catalog:change", (payload) => {
12163
+ setCatalogState(resolveCatalog(payload?.catalog));
12164
+ });
12165
+ const offCatalogActiveChange = canvasApi.on("catalog:active-change", () => {
12166
+ setCatalogState(resolveCatalog());
12167
+ });
12168
+ return () => {
12169
+ offCatalogChange?.();
12170
+ offCatalogActiveChange?.();
12171
+ };
12172
+ }, [canvasApi, editor]);
12173
+ return catalogState;
12174
+ }
12175
+
12048
12176
  // src/panels/right/components/renderif.tsx
12049
12177
  import "react";
12050
12178
  import { Fragment as Fragment7, jsx as jsx51 } from "react/jsx-runtime";
@@ -12073,7 +12201,7 @@ function RenderIf({ data, emptyMessage = null, children, when }) {
12073
12201
  // src/panels/right/partials/global/add-service.tsx
12074
12202
  import { useCanvas as useCanvas7, useWorkspace as useWorkspace11 } from "@timeax/digital-service-engine/workspace";
12075
12203
  import { InputField as InputField5 } from "@timeax/form-palette";
12076
- import { useCallback as useCallback18, useEffect as useEffect19, useMemo as useMemo25, useState as useState26 } from "react";
12204
+ import { useCallback as useCallback18, useEffect as useEffect20, useMemo as useMemo25, useRef as useRef12, useState as useState27 } from "react";
12077
12205
  import { BsPlus } from "react-icons/bs";
12078
12206
  import { FaFolderOpen } from "react-icons/fa";
12079
12207
  import { FiFilter } from "react-icons/fi";
@@ -12156,13 +12284,22 @@ function AddServicePopover({
12156
12284
  }) {
12157
12285
  const ws = useWorkspace11();
12158
12286
  const canvas = useCanvas7();
12159
- const [open, setOpen] = useState26(false);
12160
- const [filterOpen, setFilterOpen] = useState26(false);
12161
- const [query, setQuery] = useState26("");
12162
- const [selected, setSelected] = useState26("");
12163
- const [contextFilterEnabled, setContextFilterEnabled] = useState26(false);
12164
- const [catalogState, setCatalogState] = useState26(null);
12287
+ const [open, setOpen] = useState27(false);
12288
+ const [filterOpen, setFilterOpen] = useState27(false);
12289
+ const [query, setQuery] = useState27("");
12290
+ const [selected, setSelected] = useState27("");
12291
+ const [contextFilterEnabled, setContextFilterEnabled] = useState27(false);
12292
+ const [rateContextMode, setRateContextMode] = useState27("context");
12293
+ const [rateContextSource, setRateContextSource] = useState27("service");
12294
+ const [manualPrimaryRate, setManualPrimaryRate] = useState27("");
12295
+ const [primaryServiceId, setPrimaryServiceId] = useState27(null);
12296
+ const [compatibleIds, setCompatibleIds] = useState27(null);
12297
+ const compatibleRequestRef = useRef12(0);
12298
+ const compatibleCacheRef = useRef12(/* @__PURE__ */ new Map());
12299
+ const catalogState = useServiceCatalogState(canvas.api, canvas.api.editor);
12165
12300
  const policies2 = ws.policies.policies.data ?? [];
12301
+ const policiesKey = useMemo25(() => JSON.stringify(policies2 ?? []), [policies2]);
12302
+ const candidateIds = useMemo25(() => services2.map((service) => service.id), [services2]);
12166
12303
  const currentTagId = canvas.api.selection.currentTag?.();
12167
12304
  const preferredTagId = currentTagId != null ? String(currentTagId) : canvas.layers.tags[0]?.id != null ? String(canvas.layers.tags[0]?.id) : null;
12168
12305
  const selectedButtons = useMemo25(
@@ -12178,33 +12315,52 @@ function AddServicePopover({
12178
12315
  }),
12179
12316
  [preferredTagId, selectedButtons]
12180
12317
  );
12181
- const liveSnapshot = useMemo25(
12182
- () => buildServiceContextSnapshot({ props: canvas.props, services: servicesMap, state: liveSelectionContext }),
12183
- [canvas.props, liveSelectionContext, servicesMap]
12184
- );
12185
- useEffect19(() => {
12186
- const editor = canvas.api.editor;
12187
- const ensured = editor.getCatalog?.() ?? editor.ensureCatalog?.();
12188
- if (ensured) setCatalogState(ensured);
12189
- const offCatalogChange = canvas.api.on("catalog:change", ({ catalog }) => {
12190
- const next = catalog ?? editor.getCatalog?.() ?? editor.ensureCatalog?.();
12191
- setCatalogState(next ?? null);
12192
- });
12193
- const offCatalogActiveChange = canvas.api.on("catalog:active-change", () => {
12194
- const next = editor.getCatalog?.() ?? editor.ensureCatalog?.();
12195
- setCatalogState(next ?? null);
12318
+ const [liveSnapshot, setLiveSnapshot] = useState27(() => createEmptyServiceContextSnapshot(liveSelectionContext, canvas.props));
12319
+ useEffect20(() => {
12320
+ let active = true;
12321
+ const fallback = createEmptyServiceContextSnapshot(liveSelectionContext, canvas.props);
12322
+ setLiveSnapshot(fallback);
12323
+ buildServiceContextSnapshot({ props: canvas.props, services: servicesMap, state: liveSelectionContext }).then((snapshot) => {
12324
+ if (!active) return;
12325
+ setLiveSnapshot(snapshot);
12326
+ }).catch(() => {
12327
+ if (!active) return;
12328
+ setLiveSnapshot(fallback);
12196
12329
  });
12197
12330
  return () => {
12198
- offCatalogChange?.();
12199
- offCatalogActiveChange?.();
12331
+ active = false;
12200
12332
  };
12201
- }, [canvas.api]);
12333
+ }, [canvas.props, liveSelectionContext, servicesMap]);
12202
12334
  const catalogMode = catalogState?.viewMode === "grouped" ? "catalog" : "all";
12203
12335
  const catalogGroups = useMemo25(
12204
12336
  () => (catalogState?.nodes ?? []).filter((node) => node.kind === "group").sort((a, b) => (a.order ?? 0) - (b.order ?? 0) || a.label.localeCompare(b.label)),
12205
12337
  [catalogState]
12206
12338
  );
12207
12339
  const selectedCatalogGroupId = catalogGroups.some((node) => node.id === catalogState?.activeNodeId) ? String(catalogState?.activeNodeId) : null;
12340
+ const contextServiceOptions = useMemo25(
12341
+ () => liveSnapshot.usedServiceIds.map((id) => {
12342
+ const service = servicesMap[id];
12343
+ if (!service) return null;
12344
+ return {
12345
+ value: String(service.id),
12346
+ label: `${service.name ?? `Service ${service.id}`}${service.rate != null ? ` (Rate ${service.rate})` : ""}`
12347
+ };
12348
+ }).filter(Boolean),
12349
+ [liveSnapshot.usedServiceIds, servicesMap]
12350
+ );
12351
+ const parsedManualPrimaryRate = useMemo25(() => {
12352
+ const value = Number(manualPrimaryRate.trim());
12353
+ return Number.isFinite(value) ? value : null;
12354
+ }, [manualPrimaryRate]);
12355
+ const hasValidCustomPrimary = useMemo25(() => {
12356
+ if (rateContextMode !== "custom_primary_rate") return true;
12357
+ if (rateContextSource === "manual") return parsedManualPrimaryRate != null;
12358
+ return Boolean(primaryServiceId);
12359
+ }, [parsedManualPrimaryRate, primaryServiceId, rateContextMode, rateContextSource]);
12360
+ useEffect20(() => {
12361
+ if (primaryServiceId && contextServiceOptions.some((option) => option.value === primaryServiceId)) return;
12362
+ setPrimaryServiceId(contextServiceOptions[0]?.value ?? null);
12363
+ }, [contextServiceOptions, primaryServiceId]);
12208
12364
  const groupedServiceIds = useMemo25(() => {
12209
12365
  const ids = /* @__PURE__ */ new Set();
12210
12366
  for (const group of catalogGroups) {
@@ -12233,28 +12389,73 @@ function AddServicePopover({
12233
12389
  }
12234
12390
  return new Set(selectedGroup.serviceIds.map((id) => String(id)));
12235
12391
  }, [catalogGroups, catalogMode, groupedServiceIds, selectedCatalogGroupId, services2]);
12236
- const compatibleIds = useMemo25(() => {
12237
- if (!contextFilterEnabled) return null;
12238
- if (!liveSnapshot.selectedTag) return null;
12239
- const checks = canvas.api.editor.filterServicesForVisibleGroup?.(
12240
- services2.map((service) => service.id),
12241
- {
12392
+ useEffect20(() => {
12393
+ const requestId = ++compatibleRequestRef.current;
12394
+ if (!contextFilterEnabled || !liveSnapshot.selectedTag) {
12395
+ setCompatibleIds(null);
12396
+ return;
12397
+ }
12398
+ if (!hasValidCustomPrimary) {
12399
+ setCompatibleIds(null);
12400
+ return;
12401
+ }
12402
+ const cacheKey = createCompatibilityCacheKey({
12403
+ tagId: liveSnapshot.selectedTag.id,
12404
+ selectedButtons: liveSnapshot.state.selectedButtons,
12405
+ usedServiceIds: liveSnapshot.usedServiceIds,
12406
+ policiesKey,
12407
+ rateContextMode,
12408
+ rateContextSource,
12409
+ primaryRate: rateContextSource === "manual" ? parsedManualPrimaryRate : null,
12410
+ primaryServiceId: rateContextSource === "service" ? primaryServiceId : null
12411
+ });
12412
+ runCompatibilityChecksIncremental({
12413
+ editor: canvas.api.editor,
12414
+ candidateIds,
12415
+ args: {
12242
12416
  tagId: liveSnapshot.selectedTag.id,
12243
12417
  selectedButtons: liveSnapshot.state.selectedButtons,
12244
12418
  usedServiceIds: liveSnapshot.usedServiceIds,
12245
12419
  effectiveConstraints: liveSnapshot.selectedTag.constraints,
12246
- policies: policies2
12247
- }
12248
- );
12249
- if (!Array.isArray(checks)) return null;
12250
- const ids = /* @__PURE__ */ new Set();
12251
- for (const check of checks) {
12252
- if (check?.fitsConstraints && check?.passesRate && check?.passesPolicies) {
12253
- ids.add(String(check.id));
12420
+ policies: policies2,
12421
+ rateContext: rateContextMode === "custom_primary_rate" ? {
12422
+ mode: "custom_primary_rate",
12423
+ source: rateContextSource,
12424
+ primaryRate: rateContextSource === "manual" ? parsedManualPrimaryRate ?? void 0 : void 0,
12425
+ primaryServiceId: rateContextSource === "service" ? primaryServiceId ?? void 0 : void 0
12426
+ } : { mode: "context" }
12427
+ },
12428
+ cache: compatibleCacheRef.current,
12429
+ cacheKey,
12430
+ shouldAbort: () => compatibleRequestRef.current !== requestId
12431
+ }).then((checks) => {
12432
+ if (compatibleRequestRef.current !== requestId) return;
12433
+ const ids = /* @__PURE__ */ new Set();
12434
+ for (const check of checks) {
12435
+ if (check?.fitsConstraints && check?.passesRate && check?.passesPolicies) {
12436
+ ids.add(String(check.id));
12437
+ }
12254
12438
  }
12255
- }
12256
- return ids;
12257
- }, [canvas.api.editor, contextFilterEnabled, liveSnapshot, policies2, services2]);
12439
+ setCompatibleIds(ids);
12440
+ }).catch(() => {
12441
+ if (compatibleRequestRef.current !== requestId) return;
12442
+ setCompatibleIds(null);
12443
+ });
12444
+ }, [
12445
+ candidateIds,
12446
+ canvas.api.editor,
12447
+ contextFilterEnabled,
12448
+ hasValidCustomPrimary,
12449
+ liveSnapshot.selectedTag,
12450
+ liveSnapshot.state.selectedButtons,
12451
+ liveSnapshot.usedServiceIds,
12452
+ parsedManualPrimaryRate,
12453
+ policies2,
12454
+ policiesKey,
12455
+ primaryServiceId,
12456
+ rateContextMode,
12457
+ rateContextSource
12458
+ ]);
12258
12459
  const setCatalogMode = useCallback18(
12259
12460
  (mode) => {
12260
12461
  const editor = canvas.api.editor;
@@ -12276,7 +12477,7 @@ function AddServicePopover({
12276
12477
  service
12277
12478
  }));
12278
12479
  }, [allowedCatalogIds, compatibleIds, services2, query]);
12279
- useEffect19(() => {
12480
+ useEffect20(() => {
12280
12481
  if (selected && !options.some((option) => option.value === selected)) {
12281
12482
  setSelected("");
12282
12483
  }
@@ -12306,73 +12507,150 @@ function AddServicePopover({
12306
12507
  children: /* @__PURE__ */ jsx52(FiFilter, {})
12307
12508
  }
12308
12509
  ) }),
12309
- /* @__PURE__ */ jsxs34(PopoverContent, { align: "end", sideOffset: 8, collisionPadding: 12, className: "w-80 space-y-3 rounded-xl", children: [
12310
- /* @__PURE__ */ jsxs34("div", { children: [
12311
- /* @__PURE__ */ jsx52("div", { className: "text-sm font-semibold text-slate-900 dark:text-slate-100", children: "Service filters" }),
12312
- /* @__PURE__ */ jsx52("p", { className: "mt-1 text-xs text-slate-500 dark:text-slate-400", children: "Combine catalog grouping with current context compatibility." })
12313
- ] }),
12314
- /* @__PURE__ */ jsxs34("div", { className: "space-y-2 rounded-lg border border-slate-200 p-3 dark:border-slate-800", children: [
12315
- /* @__PURE__ */ jsx52("div", { className: "text-xs font-semibold tracking-[0.16em] text-slate-500 uppercase dark:text-slate-400", children: "Catalog scope" }),
12316
- /* @__PURE__ */ jsx52(
12317
- InputField5,
12318
- {
12319
- variant: "radio",
12320
- value: catalogMode,
12321
- options: [
12322
- { value: "all", label: "All services", description: "Show all services" },
12510
+ /* @__PURE__ */ jsx52(
12511
+ PopoverContent,
12512
+ {
12513
+ align: "end",
12514
+ sideOffset: 8,
12515
+ collisionPadding: 12,
12516
+ className: "flex h-[var(--radix-popover-content-available-height)] max-h-[var(--radix-popover-content-available-height)] w-80 flex-col overflow-hidden rounded-xl p-0",
12517
+ children: /* @__PURE__ */ jsx52(ScrollArea, { className: "flex-1 h-full grow ", children: /* @__PURE__ */ jsxs34("div", { className: "space-y-3 p-4", children: [
12518
+ /* @__PURE__ */ jsxs34("div", { children: [
12519
+ /* @__PURE__ */ jsx52("div", { className: "text-sm font-semibold text-slate-900 dark:text-slate-100", children: "Service filters" }),
12520
+ /* @__PURE__ */ jsx52("p", { className: "mt-1 text-xs text-slate-500 dark:text-slate-400", children: "Combine catalog grouping with current context compatibility." })
12521
+ ] }),
12522
+ /* @__PURE__ */ jsxs34("div", { className: "space-y-2 rounded-lg border border-slate-200 p-3 dark:border-slate-800", children: [
12523
+ /* @__PURE__ */ jsx52("div", { className: "text-xs font-semibold tracking-[0.16em] text-slate-500 uppercase dark:text-slate-400", children: "Catalog scope" }),
12524
+ /* @__PURE__ */ jsx52(
12525
+ InputField5,
12323
12526
  {
12324
- value: "catalog",
12325
- label: "Catalog group",
12326
- description: "Show all services in the current catalog group"
12527
+ variant: "radio",
12528
+ value: catalogMode,
12529
+ options: [
12530
+ { value: "all", label: "All services", description: "Show all services" },
12531
+ {
12532
+ value: "catalog",
12533
+ label: "Catalog group",
12534
+ description: "Show all services in the current catalog group"
12535
+ }
12536
+ ],
12537
+ optGroupClassName: "gap-1",
12538
+ onChange: (event) => setCatalogMode(event.value)
12327
12539
  }
12328
- ],
12329
- optGroupClassName: "gap-1",
12330
- onChange: (event) => setCatalogMode(event.value)
12331
- }
12332
- ),
12333
- /* @__PURE__ */ jsx52(
12334
- InputField5,
12335
- {
12336
- variant: "treeselect",
12337
- placeholder: "Select group",
12338
- disabled: catalogMode !== "catalog",
12339
- multiple: false,
12340
- value: treeValue,
12341
- triggerClassName: "px-2",
12342
- options: [{ key: UNGROUPED_TREE_VALUE, label: "Ungrouped" }, ...treeOptions],
12343
- searchable: true,
12344
- clearable: true,
12345
- onChange: (event) => {
12346
- const next = event.value == null ? null : String(event.value);
12347
- canvas.api.editor.setActiveCatalogNode?.(
12348
- next === UNGROUPED_TREE_VALUE || next == null ? void 0 : next
12349
- );
12350
- },
12351
- trailingIcons: [/* @__PURE__ */ jsx52(FaFolderOpen, {})]
12352
- }
12353
- ),
12354
- catalogMode === "catalog" ? /* @__PURE__ */ jsxs34("div", { className: "text-xs text-slate-500 dark:text-slate-400", children: [
12355
- "Showing ",
12356
- selectedCatalogGroup?.label ?? "Ungrouped",
12357
- " services."
12358
- ] }) : null
12359
- ] }),
12360
- /* @__PURE__ */ jsxs34("label", { className: "flex items-start justify-between gap-3 rounded-lg border border-slate-200 p-3 text-sm text-slate-900 dark:border-slate-800 dark:text-slate-100", children: [
12361
- /* @__PURE__ */ jsxs34("span", { className: "min-w-0", children: [
12362
- /* @__PURE__ */ jsx52("span", { className: "block font-medium", children: "Current context" }),
12363
- /* @__PURE__ */ jsx52("span", { className: "mt-1 block text-xs text-slate-500 dark:text-slate-400", children: "Filter services based on the current visible-group context." })
12364
- ] }),
12365
- /* @__PURE__ */ jsx52(
12366
- "input",
12367
- {
12368
- "aria-label": "Current context",
12369
- type: "checkbox",
12370
- checked: contextFilterEnabled,
12371
- onChange: (event) => setContextFilterEnabled(event.target.checked)
12372
- }
12373
- )
12374
- ] })
12375
- ] })
12540
+ ),
12541
+ /* @__PURE__ */ jsx52(
12542
+ InputField5,
12543
+ {
12544
+ variant: "treeselect",
12545
+ placeholder: "Select group",
12546
+ disabled: catalogMode !== "catalog",
12547
+ multiple: false,
12548
+ value: treeValue,
12549
+ triggerClassName: "px-2",
12550
+ options: [{ key: UNGROUPED_TREE_VALUE, label: "Ungrouped" }, ...treeOptions],
12551
+ searchable: true,
12552
+ clearable: true,
12553
+ onChange: (event) => {
12554
+ const next = event.value == null ? null : String(event.value);
12555
+ canvas.api.editor.setActiveCatalogNode?.(
12556
+ next === UNGROUPED_TREE_VALUE || next == null ? void 0 : next
12557
+ );
12558
+ },
12559
+ trailingIcons: [/* @__PURE__ */ jsx52(FaFolderOpen, {})]
12560
+ }
12561
+ ),
12562
+ catalogMode === "catalog" ? /* @__PURE__ */ jsxs34("div", { className: "text-xs text-slate-500 dark:text-slate-400", children: [
12563
+ "Showing ",
12564
+ selectedCatalogGroup?.label ?? "Ungrouped",
12565
+ " services."
12566
+ ] }) : null
12567
+ ] }),
12568
+ /* @__PURE__ */ jsxs34("label", { className: "flex items-start justify-between gap-3 rounded-lg border border-slate-200 p-3 text-sm text-slate-900 dark:border-slate-800 dark:text-slate-100", children: [
12569
+ /* @__PURE__ */ jsxs34("span", { className: "min-w-0", children: [
12570
+ /* @__PURE__ */ jsx52("span", { className: "block font-medium", children: "Current context" }),
12571
+ /* @__PURE__ */ jsx52("span", { className: "mt-1 block text-xs text-slate-500 dark:text-slate-400", children: "Filter services based on the current visible-group context." })
12572
+ ] }),
12573
+ /* @__PURE__ */ jsx52(
12574
+ "input",
12575
+ {
12576
+ "aria-label": "Current context",
12577
+ type: "checkbox",
12578
+ checked: contextFilterEnabled,
12579
+ onChange: (event) => setContextFilterEnabled(event.target.checked)
12580
+ }
12581
+ )
12582
+ ] }),
12583
+ /* @__PURE__ */ jsxs34("div", { className: "space-y-2 rounded-lg border border-slate-200 p-3 dark:border-slate-800", children: [
12584
+ /* @__PURE__ */ jsx52("div", { className: "text-xs font-semibold tracking-[0.16em] text-slate-500 uppercase dark:text-slate-400", children: "Rate compatibility" }),
12585
+ /* @__PURE__ */ jsx52(
12586
+ InputField5,
12587
+ {
12588
+ variant: "radio",
12589
+ value: rateContextMode,
12590
+ options: [
12591
+ {
12592
+ value: "context",
12593
+ label: "Context coherence",
12594
+ description: "Use active context services for rate coherence."
12595
+ },
12596
+ {
12597
+ value: "custom_primary_rate",
12598
+ label: "Custom primary rate",
12599
+ description: "Use selected service rate or manual rate."
12600
+ }
12601
+ ],
12602
+ optGroupClassName: "gap-1",
12603
+ onChange: (event) => setRateContextMode(event.value)
12604
+ }
12605
+ ),
12606
+ rateContextMode === "custom_primary_rate" ? /* @__PURE__ */ jsxs34("div", { className: "space-y-2", children: [
12607
+ /* @__PURE__ */ jsx52(
12608
+ InputField5,
12609
+ {
12610
+ variant: "radio",
12611
+ value: rateContextSource,
12612
+ options: [
12613
+ {
12614
+ value: "service",
12615
+ label: "From context service",
12616
+ description: "Use one active context service as primary."
12617
+ },
12618
+ {
12619
+ value: "manual",
12620
+ label: "Manual rate",
12621
+ description: "Enter a custom numeric primary rate."
12622
+ }
12623
+ ],
12624
+ optGroupClassName: "gap-1",
12625
+ onChange: (event) => setRateContextSource(event.value)
12626
+ }
12627
+ ),
12628
+ rateContextSource === "service" ? /* @__PURE__ */ jsx52(
12629
+ InputField5,
12630
+ {
12631
+ variant: "select",
12632
+ label: "Primary context service",
12633
+ options: contextServiceOptions,
12634
+ value: primaryServiceId ?? void 0,
12635
+ onChange: (event) => setPrimaryServiceId(event.value ? String(event.value) : null),
12636
+ placeholder: "Select service"
12637
+ }
12638
+ ) : /* @__PURE__ */ jsx52(
12639
+ InputField5,
12640
+ {
12641
+ variant: "number",
12642
+ label: "Primary rate",
12643
+ value: manualPrimaryRate === "" ? void 0 : Number(manualPrimaryRate),
12644
+ onChange: (event) => setManualPrimaryRate(String(event.value ?? "")),
12645
+ placeholder: "Enter rate"
12646
+ }
12647
+ ),
12648
+ !hasValidCustomPrimary ? /* @__PURE__ */ jsx52("p", { className: "text-xs text-amber-700 dark:text-amber-300", children: "Provide a primary service or valid primary rate to apply compatibility filtering." }) : null
12649
+ ] }) : null
12650
+ ] })
12651
+ ] }) })
12652
+ }
12653
+ )
12376
12654
  ] })
12377
12655
  }
12378
12656
  ),
@@ -12427,11 +12705,11 @@ var add_service_default = AddService;
12427
12705
  import { resolveInputDescriptor, useInputs as useInputs2 } from "@timeax/digital-service-engine/react";
12428
12706
  import { useCanvas as useCanvas13 } from "@timeax/digital-service-engine/workspace";
12429
12707
  import { InputField as InputField12 } from "@timeax/form-palette";
12430
- import { useEffect as useEffect20, useMemo as useMemo31, useState as useState30 } from "react";
12708
+ import { useEffect as useEffect21, useMemo as useMemo31, useState as useState31 } from "react";
12431
12709
 
12432
12710
  // src/panels/right/partials/properties/components/descriptor-settings.tsx
12433
12711
  import { InputField as InputField6 } from "@timeax/form-palette";
12434
- import { useMemo as useMemo26, useState as useState27 } from "react";
12712
+ import { useMemo as useMemo26, useState as useState28 } from "react";
12435
12713
 
12436
12714
  // src/panels/right/partials/properties/components/meta.ts
12437
12715
  function mergeNodeMeta(meta, patch) {
@@ -12728,9 +13006,9 @@ function DescriptorPrimitiveField({ schema, value, hasOverride, onSet, onClear,
12728
13006
  function DescriptorObjectField({ schema, value, hasOverride, onSet, onClear, path, allowClear = true }) {
12729
13007
  const objectValue = asRecord(value);
12730
13008
  const shapeEntries = useMemo26(() => Object.entries(schema.shape ?? {}), [schema.shape]);
12731
- const [draftKey, setDraftKey] = useState27("");
12732
- const [draftShape, setDraftShape] = useState27(shapeEntries[0]?.[0] ?? "");
12733
- const [activeShapes, setActiveShapes] = useState27({});
13009
+ const [draftKey, setDraftKey] = useState28("");
13010
+ const [draftShape, setDraftShape] = useState28(shapeEntries[0]?.[0] ?? "");
13011
+ const [activeShapes, setActiveShapes] = useState28({});
12734
13012
  const orderedEntries = useMemo26(() => getOrderedObjectEntries(schema), [schema]);
12735
13013
  const dynamicKeys = Object.keys(objectValue).filter((key) => !hasOwn(schema.fields, key));
12736
13014
  const commitObject = (nextObject) => {
@@ -12888,8 +13166,8 @@ function DescriptorObjectField({ schema, value, hasOverride, onSet, onClear, pat
12888
13166
  function DescriptorArrayField({ schema, value, hasOverride, onSet, onClear, path, allowClear = true }) {
12889
13167
  const arrayValue = asArray(value);
12890
13168
  const shapeEntries = useMemo26(() => Object.entries(schema.shape ?? {}), [schema.shape]);
12891
- const [draftShape, setDraftShape] = useState27(shapeEntries[0]?.[0] ?? "");
12892
- const [activeShapes, setActiveShapes] = useState27({});
13169
+ const [draftShape, setDraftShape] = useState28(shapeEntries[0]?.[0] ?? "");
13170
+ const [activeShapes, setActiveShapes] = useState28({});
12893
13171
  const commitArray = (nextArray) => {
12894
13172
  if (!nextArray.length) {
12895
13173
  onClear(path);
@@ -13942,15 +14220,15 @@ function ValidationSection({ node }) {
13942
14220
  import "@timeax/digital-service-engine/core";
13943
14221
  import { useCanvas as useCanvas12 } from "@timeax/digital-service-engine/workspace";
13944
14222
  import { keyBy } from "lodash";
13945
- import { useMemo as useMemo30, useState as useState29 } from "react";
14223
+ import { useMemo as useMemo30, useState as useState30 } from "react";
13946
14224
 
13947
14225
  // src/panels/right/partials/properties/components/AddIncludesPopover.tsx
13948
14226
  import { InputField as InputField11 } from "@timeax/form-palette";
13949
- import { useState as useState28 } from "react";
14227
+ import { useState as useState29 } from "react";
13950
14228
  import { BsPlus as BsPlus5 } from "react-icons/bs";
13951
14229
  import { jsx as jsx58, jsxs as jsxs40 } from "react/jsx-runtime";
13952
14230
  function AddIncludesPopover({ open, onOpenChange, onSelect, options }) {
13953
- const [value, setValue] = useState28();
14231
+ const [value, setValue] = useState29();
13954
14232
  return /* @__PURE__ */ jsxs40(Popover, { open, onOpenChange, children: [
13955
14233
  /* @__PURE__ */ jsx58(PopoverTrigger, { asChild: true, children: /* @__PURE__ */ jsx58(SectionActionTriggerButton, { icon: /* @__PURE__ */ jsx58(BsPlus5, {}), children: "Add" }) }),
13956
14234
  /* @__PURE__ */ jsx58(PopoverContent, { children: /* @__PURE__ */ jsxs40("div", { className: "flex flex-col gap-2", children: [
@@ -13987,7 +14265,7 @@ function AddIncludesPopover({ open, onOpenChange, onSelect, options }) {
13987
14265
  import { jsx as jsx59, jsxs as jsxs41 } from "react/jsx-runtime";
13988
14266
  function IncExcludeSection({ node, mode, capability }) {
13989
14267
  const canvas = useCanvas12();
13990
- const [open, setOpen] = useState29(false);
14268
+ const [open, setOpen] = useState30(false);
13991
14269
  const canEdit = capability?.canEdit ?? true;
13992
14270
  const helperMessage = capability?.message;
13993
14271
  const fields = useMemo30(() => keyBy(canvas.props.fields, "id"), [canvas.props]);
@@ -14092,19 +14370,19 @@ function FieldProperties({ className, node, kinds, defaultKind }) {
14092
14370
  const nameValue = node.raw.name ?? "";
14093
14371
  const placeholderValue = defaults.placeholder ?? "";
14094
14372
  const helpTextValue = defaults.helpText ?? "";
14095
- const [nameDraft, setNameDraft] = useState30(nameValue);
14096
- const [placeholderDraft, setPlaceholderDraft] = useState30(placeholderValue);
14097
- const [helpTextDraft, setHelpTextDraft] = useState30(helpTextValue);
14373
+ const [nameDraft, setNameDraft] = useState31(nameValue);
14374
+ const [placeholderDraft, setPlaceholderDraft] = useState31(placeholderValue);
14375
+ const [helpTextDraft, setHelpTextDraft] = useState31(helpTextValue);
14098
14376
  const descriptor = useMemo31(() => resolveInputDescriptor(registry, currentType, currentVariant), [currentType, currentVariant, registry]);
14099
14377
  const descriptorUi = descriptor?.ui ?? {};
14100
14378
  const descriptorKeySet = useMemo31(() => new Set(getDescriptorUiKeys(descriptorUi)), [descriptorUi]);
14101
- useEffect20(() => {
14379
+ useEffect21(() => {
14102
14380
  setNameDraft(nameValue);
14103
14381
  }, [node.id, nameValue]);
14104
- useEffect20(() => {
14382
+ useEffect21(() => {
14105
14383
  setPlaceholderDraft(placeholderValue);
14106
14384
  }, [node.id, placeholderValue]);
14107
- useEffect20(() => {
14385
+ useEffect21(() => {
14108
14386
  setHelpTextDraft(helpTextValue);
14109
14387
  }, [node.id, helpTextValue]);
14110
14388
  const fieldTypeOptions = useMemo31(() => {
@@ -14442,7 +14720,7 @@ import "@timeax/digital-service-engine/core";
14442
14720
  import { useCanvas as useCanvas15 } from "@timeax/digital-service-engine/workspace";
14443
14721
  import { InputField as InputField15 } from "@timeax/form-palette";
14444
14722
  import { ArrowUpRight } from "lucide-react";
14445
- import { useMemo as useMemo32, useState as useState31 } from "react";
14723
+ import { useMemo as useMemo32, useState as useState32 } from "react";
14446
14724
  import { TiDelete } from "react-icons/ti";
14447
14725
 
14448
14726
  // src/panels/right/partials/properties/tag/AddConstraintsPopover.tsx
@@ -14568,7 +14846,7 @@ import { Fragment as Fragment11, jsx as jsx65, jsxs as jsxs47 } from "react/jsx-
14568
14846
  function TagConstraintsSection({ node }) {
14569
14847
  const constraints = Object.keys(node.raw.constraints ?? {});
14570
14848
  const canvas = useCanvas15();
14571
- const [open, setOpen] = useState31(false);
14849
+ const [open, setOpen] = useState32(false);
14572
14850
  const allConstraints = useMemo32(() => canvas.api.getConstraints() ?? [], [canvas.props]);
14573
14851
  return /* @__PURE__ */ jsx65(Fragment11, { children: /* @__PURE__ */ jsxs47(Section, { children: [
14574
14852
  /* @__PURE__ */ jsxs47(Section.Header, { children: [
@@ -14687,7 +14965,7 @@ function TagProperties({ node, kinds, defaultKind }) {
14687
14965
  // src/panels/right/tabs/properties.tsx
14688
14966
  import { useCanvas as useCanvas16, useWorkspace as useWorkspace12 } from "@timeax/digital-service-engine/workspace";
14689
14967
  import { InputField as InputField16 } from "@timeax/form-palette";
14690
- import { useMemo as useMemo33, useState as useState32 } from "react";
14968
+ import { useMemo as useMemo33, useState as useState33 } from "react";
14691
14969
  import { AiOutlineLoading3Quarters } from "react-icons/ai";
14692
14970
  import { MdOutlineContentCopy } from "react-icons/md";
14693
14971
  import { jsx as jsx69, jsxs as jsxs49 } from "react/jsx-runtime";
@@ -14703,7 +14981,7 @@ var Properties = ({ kinds = [], defaultKind = "" }) => {
14703
14981
  if (!canvas.activeId) return { kind: "none" };
14704
14982
  return canvas.selector.getNode(canvas.activeId);
14705
14983
  }, [canvas.activeId, canvas.props, canvas.selection]);
14706
- const [copying, setCopying] = useState32(false);
14984
+ const [copying, setCopying] = useState33(false);
14707
14985
  const Kind = propertyComponents[node.kind];
14708
14986
  if (!Kind) {
14709
14987
  return /* @__PURE__ */ jsx69("div", { className: "p-4", children: /* @__PURE__ */ jsx69(EmptyState, { title: "No active node selected", description: "Pick a tag, field, or option from the layers panel or canvas to inspect its core settings." }) });
@@ -14918,7 +15196,7 @@ var wireframe_tags_widget_default = WireframeTagsWidget;
14918
15196
  // src/panels/right/tabs/wireframe.tsx
14919
15197
  import { useOrderFlow, Wrapper } from "@timeax/digital-service-engine/react";
14920
15198
  import { useCanvas as useCanvas17, useWorkspace as useWorkspace13 } from "@timeax/digital-service-engine/workspace";
14921
- import { useCallback as useCallback19, useMemo as useMemo34, useState as useState33 } from "react";
15199
+ import { useCallback as useCallback19, useMemo as useMemo34, useState as useState34 } from "react";
14922
15200
  import { BsChevronDown as BsChevronDown2 } from "react-icons/bs";
14923
15201
  import { jsx as jsx71, jsxs as jsxs51 } from "react/jsx-runtime";
14924
15202
  var CHECKBOX_SINGLE_EXTRA_PROPS = Object.freeze({ single: true });
@@ -14946,7 +15224,7 @@ function Wireframe({ kinds: _kinds = [], defaultKind: _defaultKind = "" }) {
14946
15224
  return void 0;
14947
15225
  }
14948
15226
  }, [flow.ready, flow.activeTagId, flow.optionSelectionsByFieldId, flow.formValuesByFieldId]);
14949
- const [isFooterOpen, setIsFooterOpen] = useState33(true);
15227
+ const [isFooterOpen, setIsFooterOpen] = useState34(true);
14950
15228
  const footerSummary = useMemo34(() => {
14951
15229
  const minText = formatMetricNumber(flow.min);
14952
15230
  const maxText = formatMetricNumber(flow.max);
@@ -15575,13 +15853,13 @@ function FallbackEditorHeader({
15575
15853
  // src/workspace/fallback-editor/fallback-registrations-panel.tsx
15576
15854
  import { useActiveFallbackRegistrations as useActiveFallbackRegistrations3, useEligibleServiceList as useEligibleServiceList2, useFallbackEditor as useFallbackEditor3 } from "@timeax/digital-service-engine/react";
15577
15855
  import { Plus, Trash2, X as X2 } from "lucide-react";
15578
- import { useCallback as useCallback20, useState as useState35 } from "react";
15856
+ import { useCallback as useCallback20, useState as useState36 } from "react";
15579
15857
 
15580
15858
  // src/workspace/fallback-editor/fallback-dialogs.tsx
15581
15859
  import { InputField as InputField17 } from "@timeax/form-palette";
15582
15860
  import { useActiveFallbackRegistrations as useActiveFallbackRegistrations2, useEligibleServiceList, useFallbackEditor as useFallbackEditor2 } from "@timeax/digital-service-engine/react";
15583
15861
  import { Check, Search } from "lucide-react";
15584
- import { useEffect as useEffect21, useMemo as useMemo36, useState as useState34 } from "react";
15862
+ import { useEffect as useEffect22, useMemo as useMemo36, useState as useState35 } from "react";
15585
15863
  import { jsx as jsx77, jsxs as jsxs56 } from "react/jsx-runtime";
15586
15864
  function FallbackAddRegistrationDialog({
15587
15865
  open,
@@ -15590,14 +15868,14 @@ function FallbackAddRegistrationDialog({
15590
15868
  }) {
15591
15869
  const { activeServiceId, serviceProps, snapshot } = useFallbackEditor2();
15592
15870
  const registrations = useActiveFallbackRegistrations2();
15593
- const [scope, setScope] = useState34("global");
15594
- const [nodeId, setNodeId] = useState34("");
15871
+ const [scope, setScope] = useState35("global");
15872
+ const [nodeId, setNodeId] = useState35("");
15595
15873
  const mode = useMemo36(() => {
15596
15874
  if (snapshot) return "snapshot";
15597
15875
  if (serviceProps) return "props";
15598
15876
  return "none";
15599
15877
  }, [snapshot, serviceProps]);
15600
- useEffect21(() => {
15878
+ useEffect22(() => {
15601
15879
  if (open) {
15602
15880
  setScope("global");
15603
15881
  setNodeId("");
@@ -15655,12 +15933,12 @@ function FallbackAddRegistrationDialog({
15655
15933
  }
15656
15934
  return [];
15657
15935
  }, [activeServiceId, mode, serviceProps, snapshot]);
15658
- useEffect21(() => {
15936
+ useEffect22(() => {
15659
15937
  if (hasGlobal && scope === "global") {
15660
15938
  setScope("node");
15661
15939
  }
15662
15940
  }, [hasGlobal, scope]);
15663
- useEffect21(() => {
15941
+ useEffect22(() => {
15664
15942
  if (!nodeId) return;
15665
15943
  if (nodeTargets.some((entry) => entry.id === nodeId)) return;
15666
15944
  setNodeId("");
@@ -15750,11 +16028,11 @@ function FallbackAddCandidatesDialog({
15750
16028
  }) {
15751
16029
  const { eligible, addMany } = useFallbackEditor2();
15752
16030
  const eligibleServices = useEligibleServiceList();
15753
- const [query, setQuery] = useState34("");
15754
- const [filterEligibleOnly, setFilterEligibleOnly] = useState34(true);
15755
- const [selected, setSelected] = useState34(/* @__PURE__ */ new Set());
15756
- const [submitting, setSubmitting] = useState34(false);
15757
- useEffect21(() => {
16031
+ const [query, setQuery] = useState35("");
16032
+ const [filterEligibleOnly, setFilterEligibleOnly] = useState35(true);
16033
+ const [selected, setSelected] = useState35(/* @__PURE__ */ new Set());
16034
+ const [submitting, setSubmitting] = useState35(false);
16035
+ useEffect22(() => {
15758
16036
  if (!open) {
15759
16037
  setQuery("");
15760
16038
  setFilterEligibleOnly(true);
@@ -15861,7 +16139,7 @@ function VirtualServiceList({
15861
16139
  rowHeight = 72,
15862
16140
  emptyText = "No services found."
15863
16141
  }) {
15864
- const [scrollTop, setScrollTop] = useState34(0);
16142
+ const [scrollTop, setScrollTop] = useState35(0);
15865
16143
  const total = items.length;
15866
16144
  const visibleCount = Math.ceil(height / rowHeight);
15867
16145
  const overscan = 6;
@@ -15985,10 +16263,10 @@ function FallbackRegistrationsPanel() {
15985
16263
  const { activeServiceId, remove, clear, check } = useFallbackEditor3();
15986
16264
  const registrations = useActiveFallbackRegistrations3();
15987
16265
  const eligibleServices = useEligibleServiceList2();
15988
- const [candidatePickerOpen, setCandidatePickerOpen] = useState35(false);
15989
- const [candidateContext, setCandidateContext] = useState35(null);
15990
- const [candidatePrimaryId, setCandidatePrimaryId] = useState35(void 0);
15991
- const [registrationDialogOpen, setRegistrationDialogOpen] = useState35(false);
16266
+ const [candidatePickerOpen, setCandidatePickerOpen] = useState36(false);
16267
+ const [candidateContext, setCandidateContext] = useState36(null);
16268
+ const [candidatePrimaryId, setCandidatePrimaryId] = useState36(void 0);
16269
+ const [registrationDialogOpen, setRegistrationDialogOpen] = useState36(false);
15992
16270
  const openCandidatePicker = useCallback20((context, primaryId) => {
15993
16271
  setCandidateContext(context);
15994
16272
  setCandidatePrimaryId(primaryId);
@@ -16117,12 +16395,12 @@ function FallbackRegistrationsPanel() {
16117
16395
  import { useFallbackEditor as useFallbackEditor4, usePrimaryServiceList as usePrimaryServiceList2 } from "@timeax/digital-service-engine/react";
16118
16396
  import { InputField as InputField18 } from "@timeax/form-palette";
16119
16397
  import { Search as Search2 } from "lucide-react";
16120
- import { useMemo as useMemo37, useState as useState36 } from "react";
16398
+ import { useMemo as useMemo37, useState as useState37 } from "react";
16121
16399
  import { jsx as jsx79, jsxs as jsxs58 } from "react/jsx-runtime";
16122
16400
  function FallbackServiceSidebar() {
16123
16401
  const { activeServiceId, setActiveServiceId, get } = useFallbackEditor4();
16124
16402
  const services2 = usePrimaryServiceList2();
16125
- const [query, setQuery] = useState36("");
16403
+ const [query, setQuery] = useState37("");
16126
16404
  const filtered = useMemo37(() => {
16127
16405
  const normalizedQuery = query.trim().toLowerCase();
16128
16406
  if (!normalizedQuery) return services2;
@@ -16182,14 +16460,14 @@ function FallbackServiceSidebar() {
16182
16460
  // src/workspace/fallback-editor/fallback-settings-panel.tsx
16183
16461
  import { useFallbackEditor as useFallbackEditor5 } from "@timeax/digital-service-engine/react";
16184
16462
  import { InputField as InputField19 } from "@timeax/form-palette";
16185
- import { useEffect as useEffect22, useState as useState37 } from "react";
16463
+ import { useEffect as useEffect23, useState as useState38 } from "react";
16186
16464
  import { jsx as jsx80, jsxs as jsxs59 } from "react/jsx-runtime";
16187
16465
  function FallbackSettingsPanel() {
16188
16466
  const { settings, saveSettings, settingsSaving } = useFallbackEditor5();
16189
- const [draft, setDraft] = useState37(settings);
16190
- const [error, setError] = useState37(null);
16191
- const [saved, setSaved] = useState37(false);
16192
- useEffect22(() => {
16467
+ const [draft, setDraft] = useState38(settings);
16468
+ const [error, setError] = useState38(null);
16469
+ const [saved, setSaved] = useState38(false);
16470
+ useEffect23(() => {
16193
16471
  setDraft(settings);
16194
16472
  setSaved(false);
16195
16473
  setError(null);
@@ -16440,7 +16718,7 @@ function NativeFallbackEditorInner({ className }) {
16440
16718
  // src/workspace/fallback-editor-modal.tsx
16441
16719
  import { useCanvas as useCanvas19, useWorkspace as useWorkspace14 } from "@timeax/digital-service-engine/workspace";
16442
16720
  import cloneDeep4 from "lodash/cloneDeep";
16443
- import { createContext as createContext4, useCallback as useCallback21, useContext as useContext4, useEffect as useEffect23, useMemo as useMemo39, useState as useState38 } from "react";
16721
+ import { createContext as createContext4, useCallback as useCallback21, useContext as useContext4, useEffect as useEffect24, useMemo as useMemo39, useState as useState39 } from "react";
16444
16722
  import { createPortal as createPortal4 } from "react-dom";
16445
16723
  import { FiX as FiX4 } from "react-icons/fi";
16446
16724
  import { jsx as jsx82, jsxs as jsxs61 } from "react/jsx-runtime";
@@ -16456,9 +16734,9 @@ var FallbackEditorModalContext = createContext4(defaultContextValue2);
16456
16734
  function FallbackEditorModalProvider({ children }) {
16457
16735
  const canvas = useCanvas19();
16458
16736
  const ws = useWorkspace14();
16459
- const [launch, setLaunch] = useState38(null);
16460
- const [sessionId, setSessionId] = useState38(0);
16461
- const [surfaceError, setSurfaceError] = useState38(null);
16737
+ const [launch, setLaunch] = useState39(null);
16738
+ const [sessionId, setSessionId] = useState39(0);
16739
+ const [surfaceError, setSurfaceError] = useState39(null);
16462
16740
  const close = useCallback21(() => {
16463
16741
  setLaunch(null);
16464
16742
  setSurfaceError(null);
@@ -16549,7 +16827,7 @@ function FallbackEditorModalProvider({ children }) {
16549
16827
  () => canvas.props?.fallbackSettings ?? {},
16550
16828
  [canvas.props]
16551
16829
  );
16552
- useEffect23(() => {
16830
+ useEffect24(() => {
16553
16831
  if (!launch) return;
16554
16832
  const onKeyDown = (event) => {
16555
16833
  if (event.key === "Escape") close();
@@ -16621,7 +16899,7 @@ function useFallbackEditorModal() {
16621
16899
 
16622
16900
  // src/workspace/bottom-panel/index.tsx
16623
16901
  import { useCanvas as useCanvas22, useWorkspace as useWorkspace15 } from "@timeax/digital-service-engine/workspace";
16624
- import { useCallback as useCallback22, useEffect as useEffect26, useMemo as useMemo41, useRef as useRef13, useState as useState41 } from "react";
16902
+ import { useCallback as useCallback22, useEffect as useEffect27, useMemo as useMemo41, useRef as useRef14, useState as useState42 } from "react";
16625
16903
  import { FiEye as FiEye3, FiEyeOff as FiEyeOff2, FiSearch, FiTerminal as FiTerminal2, FiX as FiX6 } from "react-icons/fi";
16626
16904
  import { LuGripHorizontal, LuLayers3 } from "react-icons/lu";
16627
16905
  import { MdOutlineSync } from "react-icons/md";
@@ -17076,7 +17354,7 @@ function LogCard({ row, onRemove }) {
17076
17354
 
17077
17355
  // src/workspace/bottom-panel/service-picker-dialog.tsx
17078
17356
  import { InputField as InputField20 } from "@timeax/form-palette";
17079
- import { useEffect as useEffect24, useMemo as useMemo40, useRef as useRef12, useState as useState39 } from "react";
17357
+ import { useEffect as useEffect25, useMemo as useMemo40, useRef as useRef13, useState as useState40 } from "react";
17080
17358
  import { createPortal as createPortal5 } from "react-dom";
17081
17359
 
17082
17360
  // src/workspace/bottom-panel/service-picker.ts
@@ -17205,15 +17483,15 @@ function ServicePickerDialog({
17205
17483
  onOpenChange,
17206
17484
  onConfirm
17207
17485
  }) {
17208
- const [filters, setFilters] = useState39(() => createDefaultServicePickerFilters());
17209
- const [selectedIds, setSelectedIds] = useState39(/* @__PURE__ */ new Set());
17210
- const selectAllRef = useRef12(null);
17211
- useEffect24(() => {
17486
+ const [filters, setFilters] = useState40(() => createDefaultServicePickerFilters());
17487
+ const [selectedIds, setSelectedIds] = useState40(/* @__PURE__ */ new Set());
17488
+ const selectAllRef = useRef13(null);
17489
+ useEffect25(() => {
17212
17490
  if (!open) return;
17213
17491
  setFilters(createDefaultServicePickerFilters());
17214
17492
  setSelectedIds(/* @__PURE__ */ new Set());
17215
17493
  }, [open]);
17216
- useEffect24(() => {
17494
+ useEffect25(() => {
17217
17495
  if (!open) return;
17218
17496
  const onKeyDown = (event) => {
17219
17497
  if (event.key === "Escape") onOpenChange(false);
@@ -17246,7 +17524,7 @@ function ServicePickerDialog({
17246
17524
  const partiallyFilteredSelected = filteredSelectedCount > 0 && filteredSelectedCount < filteredServiceIds.length;
17247
17525
  const selectedCount = selectedIds.size;
17248
17526
  const canConfirm = selectedCount > 0;
17249
- useEffect24(() => {
17527
+ useEffect25(() => {
17250
17528
  if (!selectAllRef.current) return;
17251
17529
  selectAllRef.current.indeterminate = partiallyFilteredSelected;
17252
17530
  }, [partiallyFilteredSelected]);
@@ -17560,7 +17838,7 @@ function toPickerSelectOptions(values) {
17560
17838
 
17561
17839
  // src/workspace/bottom-panel/services-split-pane.tsx
17562
17840
  import { InputField as InputField21 } from "@timeax/form-palette";
17563
- import { useEffect as useEffect25, useState as useState40 } from "react";
17841
+ import { useEffect as useEffect26, useState as useState41 } from "react";
17564
17842
  import { FaFolderOpen as FaFolderOpen2 } from "react-icons/fa";
17565
17843
  import { FiEdit2, FiFilter as FiFilter2, FiFolderPlus, FiPlus as FiPlus2, FiTrash2 as FiTrash22 } from "react-icons/fi";
17566
17844
 
@@ -17721,7 +17999,7 @@ var UNGROUPED_TREE_VALUE2 = "__catalog_ungrouped__";
17721
17999
  function ServicesSplitPane(props) {
17722
18000
  const emptyTitle = props.mode === "active" ? "No active services yet" : "No services match this view";
17723
18001
  const emptyDescription = props.mode === "active" ? "Connect a service to a tag, field, or option and it will appear here with its bindings." : props.mode === "catalog" ? "Select a catalog group or create one to organize source services for faster assignment." : "Try changing the search or toggles to bring more services into view.";
17724
- const [size, setSize] = useState40(44);
18002
+ const [size, setSize] = useState41(44);
17725
18003
  return /* @__PURE__ */ jsx87("div", { className: "min-h-90 p-4", children: /* @__PURE__ */ jsxs65(ResizablePanelGroup, { direction: "horizontal", className: "min-h-90", children: [
17726
18004
  /* @__PURE__ */ jsx87(ResizablePanel, { onResize: (s) => setSize(s.inPixels), defaultSize: 44, minSize: 30, children: /* @__PURE__ */ jsxs65("section", { className: "flex h-full min-h-0 flex-col overflow-hidden", style: { "--resizable-width": size + "px" }, children: [
17727
18005
  /* @__PURE__ */ jsxs65("div", { className: "space-y-3 border-b border-slate-100 pr-4 dark:border-slate-800", children: [
@@ -17855,6 +18133,16 @@ function CatalogContextPopover({
17855
18133
  onOpenChange,
17856
18134
  compatibleOnly,
17857
18135
  onCompatibleOnlyChange,
18136
+ rateContextMode,
18137
+ onRateContextModeChange,
18138
+ rateContextSource,
18139
+ onRateContextSourceChange,
18140
+ manualPrimaryRate,
18141
+ onManualPrimaryRateChange,
18142
+ primaryServiceId,
18143
+ onPrimaryServiceIdChange,
18144
+ contextServiceOptions,
18145
+ customRateError,
17858
18146
  contextLinked,
17859
18147
  onContextLinkedChange,
17860
18148
  draftContext,
@@ -17895,6 +18183,66 @@ function CatalogContextPopover({
17895
18183
  ] }),
17896
18184
  /* @__PURE__ */ jsx87("input", { type: "checkbox", checked: compatibleOnly, onChange: (event) => onCompatibleOnlyChange(event.target.checked) })
17897
18185
  ] }),
18186
+ /* @__PURE__ */ jsxs65("div", { className: "space-y-2 rounded-2xl border border-slate-200 bg-slate-50/70 p-3 dark:border-slate-800 dark:bg-slate-900/40", children: [
18187
+ /* @__PURE__ */ jsx87("div", { className: "text-xs font-semibold tracking-[0.16em] text-slate-500 uppercase dark:text-slate-400", children: "Rate compatibility mode" }),
18188
+ /* @__PURE__ */ jsx87(
18189
+ InputField21,
18190
+ {
18191
+ variant: "radio",
18192
+ value: rateContextMode,
18193
+ options: [
18194
+ {
18195
+ value: "context",
18196
+ label: "Context coherence",
18197
+ description: "Use current context services for rate coherence."
18198
+ },
18199
+ {
18200
+ value: "custom_primary_rate",
18201
+ label: "Custom primary rate",
18202
+ description: "Use a chosen service or manual rate as primary."
18203
+ }
18204
+ ],
18205
+ optGroupClassName: "gap-1",
18206
+ onChange: (event) => onRateContextModeChange(event.value)
18207
+ }
18208
+ ),
18209
+ rateContextMode === "custom_primary_rate" ? /* @__PURE__ */ jsxs65("div", { className: "space-y-2 rounded-xl border border-slate-200 bg-white p-2 dark:border-slate-700 dark:bg-slate-950/40", children: [
18210
+ /* @__PURE__ */ jsx87(
18211
+ InputField21,
18212
+ {
18213
+ variant: "radio",
18214
+ value: rateContextSource,
18215
+ options: [
18216
+ { value: "service", label: "From context service", description: "Use an active context service rate." },
18217
+ { value: "manual", label: "Manual rate", description: "Provide a numeric primary rate." }
18218
+ ],
18219
+ optGroupClassName: "gap-1",
18220
+ onChange: (event) => onRateContextSourceChange(event.value)
18221
+ }
18222
+ ),
18223
+ rateContextSource === "service" ? /* @__PURE__ */ jsx87(
18224
+ InputField21,
18225
+ {
18226
+ variant: "select",
18227
+ label: "Primary context service",
18228
+ options: contextServiceOptions,
18229
+ value: primaryServiceId ?? void 0,
18230
+ onChange: (event) => onPrimaryServiceIdChange(event.value ? String(event.value) : null),
18231
+ placeholder: "Select service"
18232
+ }
18233
+ ) : /* @__PURE__ */ jsx87(
18234
+ InputField21,
18235
+ {
18236
+ variant: "number",
18237
+ label: "Primary rate",
18238
+ value: manualPrimaryRate === "" ? void 0 : Number(manualPrimaryRate),
18239
+ onChange: (event) => onManualPrimaryRateChange(String(event.value ?? "")),
18240
+ placeholder: "Enter rate"
18241
+ }
18242
+ ),
18243
+ customRateError ? /* @__PURE__ */ jsx87("p", { className: "text-xs text-amber-700 dark:text-amber-300", children: customRateError }) : null
18244
+ ] }) : null
18245
+ ] }),
17898
18246
  /* @__PURE__ */ jsxs65("label", { className: "flex items-start justify-between gap-3 text-sm text-slate-900 dark:text-slate-100", children: [
17899
18247
  /* @__PURE__ */ jsxs65("span", { className: "min-w-0", children: [
17900
18248
  /* @__PURE__ */ jsx87("span", { className: "block font-medium", children: "Link to current context" }),
@@ -18103,9 +18451,9 @@ function GroupInputPopoverButton({
18103
18451
  disabled = false,
18104
18452
  onSubmit
18105
18453
  }) {
18106
- const [open, setOpen] = useState40(false);
18107
- const [value, setValue] = useState40(initialValue);
18108
- useEffect25(() => {
18454
+ const [open, setOpen] = useState41(false);
18455
+ const [value, setValue] = useState41(initialValue);
18456
+ useEffect26(() => {
18109
18457
  if (open) setValue(initialValue);
18110
18458
  }, [initialValue, open]);
18111
18459
  return /* @__PURE__ */ jsxs65(Popover, { open, onOpenChange: setOpen, children: [
@@ -18170,7 +18518,7 @@ function ConfirmPopoverButton({
18170
18518
  disabled = false,
18171
18519
  onConfirm
18172
18520
  }) {
18173
- const [open, setOpen] = useState40(false);
18521
+ const [open, setOpen] = useState41(false);
18174
18522
  return /* @__PURE__ */ jsxs65(Popover, { open, onOpenChange: setOpen, children: [
18175
18523
  /* @__PURE__ */ jsx87(PopoverTrigger, { asChild: true, children: /* @__PURE__ */ jsx87(
18176
18524
  "button",
@@ -18261,36 +18609,44 @@ function BottomConsolePanel({ controller, errors, activeServices, allServices, o
18261
18609
  const policies2 = ws.policies.policies.data ?? [];
18262
18610
  const currentTagId = canvas.api.selection.currentTag?.();
18263
18611
  const preferredTagId = currentTagId != null ? String(currentTagId) : canvas.layers.tags[0]?.id != null ? String(canvas.layers.tags[0]?.id) : null;
18264
- const [activeSearch, setActiveSearch] = useState41("");
18265
- const [allSearch, setAllSearch] = useState41("");
18266
- const [compatibleOnly, setCompatibleOnly] = useState41(false);
18267
- const [hideActiveServices, setHideActiveServices] = useState41(false);
18268
- const [hideNonEligibleServices, setHideNonEligibleServices] = useState41(false);
18269
- const [filterOpen, setFilterOpen] = useState41(false);
18270
- const [searchOpen, setSearchOpen] = useState41(false);
18271
- const [contextLinked, setContextLinked] = useState41(false);
18272
- const [servicePickerOpen, setServicePickerOpen] = useState41(false);
18273
- const [selectedActiveServiceId, setSelectedActiveServiceId] = useState41(null);
18274
- const [selectedAllServiceId, setSelectedAllServiceId] = useState41(null);
18275
- const [catalogContextDraft, setCatalogContextDraft] = useState41(
18612
+ const [activeSearch, setActiveSearch] = useState42("");
18613
+ const [allSearch, setAllSearch] = useState42("");
18614
+ const [compatibleOnly, setCompatibleOnly] = useState42(false);
18615
+ const [rateContextMode, setRateContextMode] = useState42("context");
18616
+ const [rateContextSource, setRateContextSource] = useState42("service");
18617
+ const [manualPrimaryRate, setManualPrimaryRate] = useState42("");
18618
+ const [primaryServiceId, setPrimaryServiceId] = useState42(null);
18619
+ const [hideActiveServices, setHideActiveServices] = useState42(false);
18620
+ const [hideNonEligibleServices, setHideNonEligibleServices] = useState42(false);
18621
+ const [filterOpen, setFilterOpen] = useState42(false);
18622
+ const [searchOpen, setSearchOpen] = useState42(false);
18623
+ const [contextLinked, setContextLinked] = useState42(false);
18624
+ const [servicePickerOpen, setServicePickerOpen] = useState42(false);
18625
+ const [selectedActiveServiceId, setSelectedActiveServiceId] = useState42(null);
18626
+ const [selectedAllServiceId, setSelectedAllServiceId] = useState42(null);
18627
+ const [catalogContextDraft, setCatalogContextDraft] = useState42(
18276
18628
  () => createDefaultServiceContext(canvas.props, preferredTagId)
18277
18629
  );
18278
- const [catalogState, setCatalogState] = useState41(null);
18279
- const [panelPosition, setPanelPosition] = useState41(() => readStoredPanelPosition());
18280
- const [draggingPanel, setDraggingPanel] = useState41(false);
18281
- const [consoleSubTab, setConsoleSubTab] = useState41("validation");
18282
- const [consoleScopeFilter, setConsoleScopeFilter] = useState41("all");
18283
- const [consoleSeverityFilter, setConsoleSeverityFilter] = useState41("all");
18284
- const [consoleIntroState, setConsoleIntroState] = useState41({
18630
+ const catalogState = useServiceCatalogState(canvas.api, canvas.api.editor);
18631
+ const [panelPosition, setPanelPosition] = useState42(() => readStoredPanelPosition());
18632
+ const [draggingPanel, setDraggingPanel] = useState42(false);
18633
+ const [consoleSubTab, setConsoleSubTab] = useState42("validation");
18634
+ const [consoleScopeFilter, setConsoleScopeFilter] = useState42("all");
18635
+ const [consoleSeverityFilter, setConsoleSeverityFilter] = useState42("all");
18636
+ const [consoleIntroState, setConsoleIntroState] = useState42({
18285
18637
  validation: { minimized: false, closed: false },
18286
18638
  logs: { minimized: false, closed: false },
18287
18639
  notices: { minimized: false, closed: false }
18288
18640
  });
18289
- const lastHighlightedIdsRef = useRef13([]);
18290
- const panelContainerRef = useRef13(null);
18291
- const panelRef = useRef13(null);
18292
- const searchInputRef = useRef13(null);
18293
- const dragStartRef = useRef13(null);
18641
+ const lastHighlightedIdsRef = useRef14([]);
18642
+ const panelContainerRef = useRef14(null);
18643
+ const panelRef = useRef14(null);
18644
+ const searchInputRef = useRef14(null);
18645
+ const dragStartRef = useRef14(null);
18646
+ const appliedSnapshotRequestRef = useRef14(0);
18647
+ const draftSnapshotRequestRef = useRef14(0);
18648
+ const allChecksByIdRequestRef = useRef14(0);
18649
+ const allChecksCacheRef = useRef14(/* @__PURE__ */ new Map());
18294
18650
  const selectedButtons = useMemo41(
18295
18651
  () => (canvas.api.selection.selectedButtons?.() ?? []).map((value) => String(value)),
18296
18652
  [canvas.api.selection, canvas.selectionInfo.ids, canvas.selectionInfo.optionIds]
@@ -18305,26 +18661,119 @@ function BottomConsolePanel({ controller, errors, activeServices, allServices, o
18305
18661
  [preferredTagId, selectedButtons]
18306
18662
  );
18307
18663
  const effectiveCatalogContext = contextLinked ? liveSelectionContext : catalogContextDraft;
18308
- const appliedSnapshot = useMemo41(
18309
- () => buildServiceContextSnapshot({ props: canvas.props, services: servicesMap, state: effectiveCatalogContext }),
18310
- [canvas.props, effectiveCatalogContext, servicesMap]
18311
- );
18312
- const draftSnapshot = useMemo41(
18313
- () => buildServiceContextSnapshot({ props: canvas.props, services: servicesMap, state: catalogContextDraft }),
18314
- [canvas.props, catalogContextDraft, servicesMap]
18664
+ const [appliedSnapshot, setAppliedSnapshot] = useState42(() => createEmptyServiceContextSnapshot(effectiveCatalogContext, canvas.props));
18665
+ const [draftSnapshot, setDraftSnapshot] = useState42(() => createEmptyServiceContextSnapshot(catalogContextDraft, canvas.props));
18666
+ useEffect27(() => {
18667
+ const requestId = ++appliedSnapshotRequestRef.current;
18668
+ const fallback = createEmptyServiceContextSnapshot(effectiveCatalogContext, canvas.props);
18669
+ setAppliedSnapshot(fallback);
18670
+ buildServiceContextSnapshot({ props: canvas.props, services: servicesMap, state: effectiveCatalogContext }).then((snapshot) => {
18671
+ if (appliedSnapshotRequestRef.current !== requestId) return;
18672
+ setAppliedSnapshot(snapshot);
18673
+ }).catch(() => {
18674
+ if (appliedSnapshotRequestRef.current !== requestId) return;
18675
+ setAppliedSnapshot(fallback);
18676
+ });
18677
+ }, [canvas.props, effectiveCatalogContext, servicesMap]);
18678
+ useEffect27(() => {
18679
+ const requestId = ++draftSnapshotRequestRef.current;
18680
+ const fallback = createEmptyServiceContextSnapshot(catalogContextDraft, canvas.props);
18681
+ setDraftSnapshot(fallback);
18682
+ buildServiceContextSnapshot({ props: canvas.props, services: servicesMap, state: catalogContextDraft }).then((snapshot) => {
18683
+ if (draftSnapshotRequestRef.current !== requestId) return;
18684
+ setDraftSnapshot(snapshot);
18685
+ }).catch(() => {
18686
+ if (draftSnapshotRequestRef.current !== requestId) return;
18687
+ setDraftSnapshot(fallback);
18688
+ });
18689
+ }, [canvas.props, catalogContextDraft, servicesMap]);
18690
+ const [allChecksById, setAllChecksById] = useState42(/* @__PURE__ */ new Map());
18691
+ const allCandidateIds = useMemo41(() => Object.values(servicesMap).map((service) => service.id), [servicesMap]);
18692
+ const policiesKey = useMemo41(() => JSON.stringify(policies2 ?? []), [policies2]);
18693
+ const contextServiceOptions = useMemo41(
18694
+ () => appliedSnapshot.usedServiceIds.map((id) => {
18695
+ const service = servicesMap[id];
18696
+ if (!service) return null;
18697
+ return {
18698
+ value: String(service.id),
18699
+ label: `${service.name ?? `Service ${service.id}`}${service.rate != null ? ` (Rate ${service.rate})` : ""}`
18700
+ };
18701
+ }).filter(Boolean),
18702
+ [appliedSnapshot.usedServiceIds, servicesMap]
18315
18703
  );
18316
- const allChecksById = useMemo41(() => {
18317
- if (!appliedSnapshot.selectedTag) return /* @__PURE__ */ new Map();
18318
- const candidateIds = Object.values(servicesMap).map((service) => service.id);
18319
- const checks = canvas.api.editor.filterServicesForVisibleGroup(candidateIds, {
18704
+ const parsedManualPrimaryRate = useMemo41(() => {
18705
+ const value = Number(manualPrimaryRate.trim());
18706
+ return Number.isFinite(value) ? value : null;
18707
+ }, [manualPrimaryRate]);
18708
+ const hasValidCustomPrimary = useMemo41(() => {
18709
+ if (rateContextMode !== "custom_primary_rate") return true;
18710
+ if (rateContextSource === "manual") return parsedManualPrimaryRate != null;
18711
+ return Boolean(primaryServiceId);
18712
+ }, [parsedManualPrimaryRate, primaryServiceId, rateContextMode, rateContextSource]);
18713
+ useEffect27(() => {
18714
+ if (primaryServiceId && contextServiceOptions.some((option) => option.value === primaryServiceId)) return;
18715
+ setPrimaryServiceId(contextServiceOptions[0]?.value ?? null);
18716
+ }, [contextServiceOptions, primaryServiceId]);
18717
+ useEffect27(() => {
18718
+ const requestId = ++allChecksByIdRequestRef.current;
18719
+ if (!appliedSnapshot.selectedTag) {
18720
+ setAllChecksById(/* @__PURE__ */ new Map());
18721
+ return;
18722
+ }
18723
+ if (!hasValidCustomPrimary) {
18724
+ setAllChecksById(/* @__PURE__ */ new Map());
18725
+ return;
18726
+ }
18727
+ const cacheKey = createCompatibilityCacheKey({
18320
18728
  tagId: appliedSnapshot.selectedTag.id,
18321
18729
  selectedButtons: appliedSnapshot.state.selectedButtons,
18322
18730
  usedServiceIds: appliedSnapshot.usedServiceIds,
18323
- effectiveConstraints: appliedSnapshot.selectedTag.constraints,
18324
- policies: policies2
18731
+ policiesKey,
18732
+ rateContextMode,
18733
+ rateContextSource,
18734
+ primaryRate: rateContextSource === "manual" ? parsedManualPrimaryRate : null,
18735
+ primaryServiceId: rateContextSource === "service" ? primaryServiceId : null
18736
+ });
18737
+ runCompatibilityChecksIncremental({
18738
+ editor: canvas.api.editor,
18739
+ candidateIds: allCandidateIds,
18740
+ args: {
18741
+ tagId: appliedSnapshot.selectedTag.id,
18742
+ selectedButtons: appliedSnapshot.state.selectedButtons,
18743
+ usedServiceIds: appliedSnapshot.usedServiceIds,
18744
+ effectiveConstraints: appliedSnapshot.selectedTag.constraints,
18745
+ policies: policies2,
18746
+ rateContext: rateContextMode === "custom_primary_rate" ? {
18747
+ mode: "custom_primary_rate",
18748
+ source: rateContextSource,
18749
+ primaryRate: rateContextSource === "manual" ? parsedManualPrimaryRate ?? void 0 : void 0,
18750
+ primaryServiceId: rateContextSource === "service" ? primaryServiceId ?? void 0 : void 0
18751
+ } : { mode: "context" }
18752
+ },
18753
+ cache: allChecksCacheRef.current,
18754
+ cacheKey,
18755
+ shouldAbort: () => allChecksByIdRequestRef.current !== requestId
18756
+ }).then((checks) => {
18757
+ if (allChecksByIdRequestRef.current !== requestId) return;
18758
+ setAllChecksById(new Map(checks.map((check) => [String(check.id), check])));
18759
+ }).catch(() => {
18760
+ if (allChecksByIdRequestRef.current !== requestId) return;
18761
+ setAllChecksById(/* @__PURE__ */ new Map());
18325
18762
  });
18326
- return new Map(checks.map((check) => [String(check.id), check]));
18327
- }, [appliedSnapshot, canvas.api.editor, policies2, servicesMap]);
18763
+ }, [
18764
+ allCandidateIds,
18765
+ appliedSnapshot.selectedTag,
18766
+ appliedSnapshot.state.selectedButtons,
18767
+ appliedSnapshot.usedServiceIds,
18768
+ canvas.api.editor,
18769
+ hasValidCustomPrimary,
18770
+ parsedManualPrimaryRate,
18771
+ policies2,
18772
+ policiesKey,
18773
+ primaryServiceId,
18774
+ rateContextMode,
18775
+ rateContextSource
18776
+ ]);
18328
18777
  const hiddenServiceIds = useMemo41(() => {
18329
18778
  const ids = new Set(activeServices.map((service) => String(service.id)));
18330
18779
  for (const id of appliedSnapshot.usedServiceIds) ids.add(String(id));
@@ -18356,41 +18805,24 @@ function BottomConsolePanel({ controller, errors, activeServices, allServices, o
18356
18805
  servicesMap
18357
18806
  ]
18358
18807
  );
18359
- useEffect26(() => {
18808
+ useEffect27(() => {
18360
18809
  const next = createDefaultServiceContext(canvas.props, preferredTagId);
18361
18810
  setCatalogContextDraft((current) => {
18362
18811
  if (contextLinked) return current;
18363
18812
  return current.selectedTagId ? current : next;
18364
18813
  });
18365
18814
  }, [canvas.props, contextLinked, preferredTagId]);
18366
- useEffect26(() => {
18815
+ useEffect27(() => {
18367
18816
  if (!contextLinked) return;
18368
18817
  setCatalogContextDraft(liveSelectionContext);
18369
18818
  }, [contextLinked, liveSelectionContext]);
18370
- useEffect26(() => {
18819
+ useEffect27(() => {
18371
18820
  if (contextLinked) return;
18372
18821
  const next = sanitizeServiceContext(draftSnapshot, catalogContextDraft);
18373
- if (JSON.stringify(next) !== JSON.stringify(catalogContextDraft)) {
18822
+ if (!areServiceContextStatesEqual(next, catalogContextDraft)) {
18374
18823
  setCatalogContextDraft(next);
18375
18824
  }
18376
18825
  }, [catalogContextDraft, contextLinked, draftSnapshot]);
18377
- useEffect26(() => {
18378
- const editor = canvas.api.editor;
18379
- const ensured = editor.getCatalog?.() ?? editor.ensureCatalog?.();
18380
- if (ensured) setCatalogState(ensured);
18381
- const offCatalogChange = canvas.api.on("catalog:change", ({ catalog }) => {
18382
- const next = catalog ?? editor.getCatalog?.() ?? editor.ensureCatalog?.();
18383
- setCatalogState(next ?? null);
18384
- });
18385
- const offCatalogActiveChange = canvas.api.on("catalog:active-change", () => {
18386
- const next = editor.getCatalog?.() ?? editor.ensureCatalog?.();
18387
- setCatalogState(next ?? null);
18388
- });
18389
- return () => {
18390
- offCatalogChange?.();
18391
- offCatalogActiveChange?.();
18392
- };
18393
- }, [canvas.api]);
18394
18826
  const catalogPanelMode = catalogState?.viewMode === "grouped" ? "catalog" : "all";
18395
18827
  const catalogGroups = useMemo41(
18396
18828
  () => (catalogState?.nodes ?? []).filter((node) => node.kind === "group").sort((a, b) => (a.order ?? 0) - (b.order ?? 0) || a.label.localeCompare(b.label)),
@@ -18420,24 +18852,24 @@ function BottomConsolePanel({ controller, errors, activeServices, allServices, o
18420
18852
  return ids;
18421
18853
  }, [canvas.activeId, canvas.selectionInfo.ids]);
18422
18854
  const consoleIssueCount = errors.validation.length + errors.logs.length + notices.length;
18423
- useEffect26(() => {
18855
+ useEffect27(() => {
18424
18856
  setSelectedActiveServiceId((current) => ensureSelectedRow(current, activeRows));
18425
18857
  }, [activeRows]);
18426
- useEffect26(() => {
18858
+ useEffect27(() => {
18427
18859
  if (catalogState?.selectedServiceId == null) return;
18428
18860
  setSelectedAllServiceId(String(catalogState.selectedServiceId));
18429
18861
  }, [catalogState?.selectedServiceId]);
18430
- useEffect26(() => {
18862
+ useEffect27(() => {
18431
18863
  setSelectedAllServiceId((current) => ensureSelectedRow(current, visibleAllRows));
18432
18864
  }, [visibleAllRows]);
18433
- useEffect26(() => {
18865
+ useEffect27(() => {
18434
18866
  const desiredIds = controller.isOpen && controller.activeTab === "activeServices" && selectedActiveServiceId ? activeRows.find((row) => row.id === selectedActiveServiceId)?.summary.attachedNodeIds ?? [] : [];
18435
18867
  if (!sameIds(lastHighlightedIdsRef.current, desiredIds)) {
18436
18868
  canvas.api.setHighlighted(desiredIds);
18437
18869
  lastHighlightedIdsRef.current = [...desiredIds];
18438
18870
  }
18439
18871
  }, [activeRows, canvas.api, controller.activeTab, controller.isOpen, selectedActiveServiceId]);
18440
- useEffect26(() => {
18872
+ useEffect27(() => {
18441
18873
  return () => {
18442
18874
  if (lastHighlightedIdsRef.current.length) {
18443
18875
  canvas.api.setHighlighted([]);
@@ -18445,10 +18877,10 @@ function BottomConsolePanel({ controller, errors, activeServices, allServices, o
18445
18877
  }
18446
18878
  };
18447
18879
  }, [canvas.api]);
18448
- useEffect26(() => {
18880
+ useEffect27(() => {
18449
18881
  if (searchOpen) searchInputRef.current?.focus();
18450
18882
  }, [searchOpen]);
18451
- useEffect26(() => {
18883
+ useEffect27(() => {
18452
18884
  setPanelPosition((current) => {
18453
18885
  const resolved = resolvePanelPosition(current, panelRef.current, panelContainerRef.current);
18454
18886
  persistPanelPosition(resolved);
@@ -18470,7 +18902,7 @@ function BottomConsolePanel({ controller, errors, activeServices, allServices, o
18470
18902
  }
18471
18903
  event.preventDefault();
18472
18904
  };
18473
- useEffect26(() => {
18905
+ useEffect27(() => {
18474
18906
  if (!draggingPanel) return;
18475
18907
  const onPointerMove = (event) => {
18476
18908
  const start = dragStartRef.current;
@@ -18724,6 +19156,16 @@ function BottomConsolePanel({ controller, errors, activeServices, allServices, o
18724
19156
  onOpenChange: setFilterOpen,
18725
19157
  compatibleOnly,
18726
19158
  onCompatibleOnlyChange: setCompatibleOnly,
19159
+ rateContextMode,
19160
+ onRateContextModeChange: setRateContextMode,
19161
+ rateContextSource,
19162
+ onRateContextSourceChange: setRateContextSource,
19163
+ manualPrimaryRate,
19164
+ onManualPrimaryRateChange: setManualPrimaryRate,
19165
+ primaryServiceId,
19166
+ onPrimaryServiceIdChange: setPrimaryServiceId,
19167
+ contextServiceOptions,
19168
+ customRateError: hasValidCustomPrimary ? null : "Provide a primary service or valid primary rate to apply compatibility filtering.",
18727
19169
  contextLinked,
18728
19170
  onContextLinkedChange: setContextLinked,
18729
19171
  draftContext: catalogContextDraft,
@@ -18953,7 +19395,7 @@ import {
18953
19395
  useWorkspace as useWorkspace16,
18954
19396
  Workspace
18955
19397
  } from "@timeax/digital-service-engine/workspace";
18956
- import { useMemo as useMemo42, useState as useState42 } from "react";
19398
+ import { useMemo as useMemo42, useState as useState43 } from "react";
18957
19399
 
18958
19400
  // backend/memory/create-backend.ts
18959
19401
  import { createMemoryWorkspaceBackend } from "@timeax/digital-service-engine/workspace";
@@ -58367,15 +58809,8 @@ function WorkspaceLayout({ onShare, onPlay, menu, kinds, defaultKind }) {
58367
58809
  const ws = useWorkspace16();
58368
58810
  const errors = useErrors();
58369
58811
  const bottomPanel = useBottomConsolePanel();
58370
- const [draggingServiceId, setDraggingServiceId] = useState42(null);
58371
- const resolvedKinds = useMemo42(
58372
- () => Array.from(
58373
- new Set(
58374
- (kinds ?? []).map((kind) => String(kind).trim()).filter(Boolean)
58375
- )
58376
- ),
58377
- [kinds]
58378
- );
58812
+ const [draggingServiceId, setDraggingServiceId] = useState43(null);
58813
+ const resolvedKinds = useMemo42(() => Array.from(new Set((kinds ?? []).map((kind) => String(kind).trim()).filter(Boolean))), [kinds]);
58379
58814
  const resolvedDefaultKind = useMemo42(() => {
58380
58815
  const next = defaultKind?.trim();
58381
58816
  if (next) return next;