@miragon/mcp-toolkit-ui 1.1.0 → 1.2.0

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.
Files changed (77) hide show
  1. package/dist/app/builder/BuilderBanners.d.ts +19 -0
  2. package/dist/app/builder/BuilderBanners.d.ts.map +1 -0
  3. package/dist/app/builder/BuilderBanners.js +22 -0
  4. package/dist/app/builder/BuilderBanners.js.map +1 -0
  5. package/dist/app/builder/CanvasRow.d.ts +26 -0
  6. package/dist/app/builder/CanvasRow.d.ts.map +1 -0
  7. package/dist/app/builder/CanvasRow.js +98 -0
  8. package/dist/app/builder/CanvasRow.js.map +1 -0
  9. package/dist/app/builder/CellPropsSheet.d.ts +19 -0
  10. package/dist/app/builder/CellPropsSheet.d.ts.map +1 -0
  11. package/dist/app/builder/CellPropsSheet.js +28 -0
  12. package/dist/app/builder/CellPropsSheet.js.map +1 -0
  13. package/dist/app/builder/EmptyHint.d.ts +6 -0
  14. package/dist/app/builder/EmptyHint.d.ts.map +1 -0
  15. package/dist/app/builder/EmptyHint.js +10 -0
  16. package/dist/app/builder/EmptyHint.js.map +1 -0
  17. package/dist/app/builder/LayoutSurface.d.ts +27 -0
  18. package/dist/app/builder/LayoutSurface.d.ts.map +1 -0
  19. package/dist/app/builder/LayoutSurface.js +45 -0
  20. package/dist/app/builder/LayoutSurface.js.map +1 -0
  21. package/dist/app/builder/Workspace.d.ts +9 -3
  22. package/dist/app/builder/Workspace.d.ts.map +1 -1
  23. package/dist/app/builder/Workspace.js +3 -97
  24. package/dist/app/builder/Workspace.js.map +1 -1
  25. package/dist/app/builder/builder-reducer.d.ts.map +1 -1
  26. package/dist/app/builder/builder-reducer.js +176 -164
  27. package/dist/app/builder/builder-reducer.js.map +1 -1
  28. package/dist/app/builder/use-builder-catalogue.d.ts +33 -0
  29. package/dist/app/builder/use-builder-catalogue.d.ts.map +1 -0
  30. package/dist/app/builder/use-builder-catalogue.js +155 -0
  31. package/dist/app/builder/use-builder-catalogue.js.map +1 -0
  32. package/dist/app/builder/use-key-step-entries.d.ts +22 -0
  33. package/dist/app/builder/use-key-step-entries.d.ts.map +1 -0
  34. package/dist/app/builder/use-key-step-entries.js +46 -0
  35. package/dist/app/builder/use-key-step-entries.js.map +1 -0
  36. package/dist/app/builder/use-save-dashboard.d.ts +33 -0
  37. package/dist/app/builder/use-save-dashboard.d.ts.map +1 -0
  38. package/dist/app/builder/use-save-dashboard.js +78 -0
  39. package/dist/app/builder/use-save-dashboard.js.map +1 -0
  40. package/dist/app/layout-builder.d.ts.map +1 -1
  41. package/dist/app/layout-builder.js +46 -277
  42. package/dist/app/layout-builder.js.map +1 -1
  43. package/dist/app/mcp-app-view-chrome.d.ts +77 -0
  44. package/dist/app/mcp-app-view-chrome.d.ts.map +1 -0
  45. package/dist/app/mcp-app-view-chrome.js +53 -0
  46. package/dist/app/mcp-app-view-chrome.js.map +1 -0
  47. package/dist/app/mcp-app-view.d.ts +3 -9
  48. package/dist/app/mcp-app-view.d.ts.map +1 -1
  49. package/dist/app/mcp-app-view.js +4 -31
  50. package/dist/app/mcp-app-view.js.map +1 -1
  51. package/dist/render-cases-primitives.d.ts +17 -0
  52. package/dist/render-cases-primitives.d.ts.map +1 -0
  53. package/dist/render-cases-primitives.js +88 -0
  54. package/dist/render-cases-primitives.js.map +1 -0
  55. package/dist/render-cases.d.ts +4 -0
  56. package/dist/render-cases.d.ts.map +1 -0
  57. package/dist/render-cases.js +140 -0
  58. package/dist/render-cases.js.map +1 -0
  59. package/package.json +10 -5
  60. package/src/app/builder/BuilderBanners.tsx +61 -0
  61. package/src/app/builder/CanvasRow.tsx +316 -0
  62. package/src/app/builder/CellPropsSheet.tsx +47 -0
  63. package/src/app/builder/EmptyHint.tsx +29 -0
  64. package/src/app/builder/LayoutSurface.tsx +140 -0
  65. package/src/app/builder/Workspace.tsx +33 -342
  66. package/src/app/builder/builder-reducer.ts +204 -170
  67. package/src/app/builder/use-builder-catalogue.ts +187 -0
  68. package/src/app/builder/use-key-step-entries.ts +60 -0
  69. package/src/app/builder/use-save-dashboard.ts +97 -0
  70. package/src/app/layout-builder.tsx +92 -497
  71. package/src/app/mcp-app-view-chrome.tsx +204 -0
  72. package/src/app/mcp-app-view.tsx +32 -140
  73. package/src/lib/parse-tool-result.property.test.ts +90 -0
  74. package/src/render-cases-primitives.tsx +255 -0
  75. package/src/render-cases.tsx +239 -0
  76. package/src/render-smoke.test.tsx +100 -0
  77. package/src/ui-catalog.test.ts +52 -0
@@ -0,0 +1,46 @@
1
+ import { useCallback, useState } from "react";
2
+ import { defaultStepIdFor, keysToEntries } from "./builder-model.js";
3
+ // -------------------------------------------------------------------------- //
4
+ // Keys + steps editor state — the pipeline tab's editable entry lists
5
+ // -------------------------------------------------------------------------- //
6
+ /**
7
+ * Owns the key/step editor entries and their mutations, extracted from
8
+ * `layout-builder.tsx` unchanged. `addKey` de-dupes a prefilled name (the
9
+ * catalogue's "Add as key" action) and `addStep` derives a unique context
10
+ * id via `defaultStepIdFor`.
11
+ */
12
+ export function useKeyStepEntries({ initialKeys, initialSteps, }) {
13
+ const [keyEntries, setKeyEntries] = useState(() => keysToEntries(initialKeys));
14
+ const [stepEntries, setStepEntries] = useState(() => initialSteps ?? []);
15
+ const addKey = useCallback((prefilledName) => {
16
+ setKeyEntries((prev) => {
17
+ if (prefilledName) {
18
+ if (prev.some((e) => e.name === prefilledName))
19
+ return prev;
20
+ return [...prev, { name: prefilledName, rawValue: "" }];
21
+ }
22
+ return [...prev, { name: "", rawValue: "" }];
23
+ });
24
+ }, []);
25
+ const updateKey = useCallback((idx, patch) => {
26
+ setKeyEntries((prev) => prev.map((e, i) => (i === idx ? { ...e, ...patch } : e)));
27
+ }, []);
28
+ const removeKey = useCallback((idx) => {
29
+ setKeyEntries((prev) => prev.filter((_, i) => i !== idx));
30
+ }, []);
31
+ const addStep = useCallback((stepId) => {
32
+ setStepEntries((prev) => {
33
+ const taken = new Set(prev.map((s) => s.id));
34
+ const newId = stepId ? defaultStepIdFor(stepId, taken) : `step-${prev.length + 1}`;
35
+ return [...prev, { id: newId, step: stepId ?? "", optional: false }];
36
+ });
37
+ }, []);
38
+ const updateStep = useCallback((idx, patch) => {
39
+ setStepEntries((prev) => prev.map((s, i) => (i === idx ? { ...s, ...patch } : s)));
40
+ }, []);
41
+ const removeStep = useCallback((idx) => {
42
+ setStepEntries((prev) => prev.filter((_, i) => i !== idx));
43
+ }, []);
44
+ return { keyEntries, stepEntries, addKey, updateKey, removeKey, addStep, updateStep, removeStep };
45
+ }
46
+ //# sourceMappingURL=use-key-step-entries.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"use-key-step-entries.js","sourceRoot":"","sources":["../../../src/app/builder/use-key-step-entries.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,33 @@
1
+ import type { PipelineStepRef } from "@miragon/mcp-toolkit-core";
2
+ import { type DraftLayout, type KeyEntry } from "./builder-model.js";
3
+ /**
4
+ * Owns the save dialog's state (open flag, name/description inputs, busy
5
+ * flag, inline error) and the `save-dashboard` call. Extracted from
6
+ * `layout-builder.tsx` unchanged: a rejected save surfaces its reason
7
+ * inline in the SaveDialog instead of failing silently (Finding [3]).
8
+ */
9
+ export declare function useSaveDashboard({ callTool, saveToolName, dashboardId, title, keyEntries, stepEntries, draft, onSaved, }: {
10
+ callTool: (name: string, args: object) => Promise<unknown>;
11
+ saveToolName: string;
12
+ dashboardId?: string;
13
+ title?: string;
14
+ keyEntries: KeyEntry[];
15
+ stepEntries: PipelineStepRef[];
16
+ draft: DraftLayout;
17
+ onSaved?: (result: {
18
+ id: string;
19
+ name: string;
20
+ }) => void;
21
+ }): {
22
+ isBusy: boolean;
23
+ saveOpen: boolean;
24
+ setSaveOpen: import("react").Dispatch<import("react").SetStateAction<boolean>>;
25
+ saveName: string;
26
+ setSaveName: import("react").Dispatch<import("react").SetStateAction<string>>;
27
+ saveDescription: string;
28
+ setSaveDescription: import("react").Dispatch<import("react").SetStateAction<string>>;
29
+ saveError: string | null;
30
+ setSaveError: import("react").Dispatch<import("react").SetStateAction<string | null>>;
31
+ saveDraft: () => Promise<void>;
32
+ };
33
+ //# sourceMappingURL=use-save-dashboard.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"use-save-dashboard.d.ts","sourceRoot":"","sources":["../../../src/app/builder/use-save-dashboard.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,2BAA2B,CAAA;AAEhE,OAAO,EAAgC,KAAK,WAAW,EAAE,KAAK,QAAQ,EAAE,MAAM,oBAAoB,CAAA;AAMlG;;;;;GAKG;AACH,wBAAgB,gBAAgB,CAAC,EAC/B,QAAQ,EACR,YAAY,EACZ,WAAW,EACX,KAAK,EACL,UAAU,EACV,WAAW,EACX,KAAK,EACL,OAAO,GACR,EAAE;IACD,QAAQ,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC,OAAO,CAAC,CAAA;IAC1D,YAAY,EAAE,MAAM,CAAA;IACpB,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,UAAU,EAAE,QAAQ,EAAE,CAAA;IACtB,WAAW,EAAE,eAAe,EAAE,CAAA;IAC9B,KAAK,EAAE,WAAW,CAAA;IAClB,OAAO,CAAC,EAAE,CAAC,MAAM,EAAE;QAAE,EAAE,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,KAAK,IAAI,CAAA;CACzD;;;;;;;;;;;EA+DA"}
@@ -0,0 +1,78 @@
1
+ import { useCallback, useState } from "react";
2
+ import { parseToolResult } from "../../lib/parse-tool-result.js";
3
+ import { draftToLayout, entriesToKeys } from "./builder-model.js";
4
+ // -------------------------------------------------------------------------- //
5
+ // Save-dashboard hook — dialog state + the `save-dashboard` call
6
+ // -------------------------------------------------------------------------- //
7
+ /**
8
+ * Owns the save dialog's state (open flag, name/description inputs, busy
9
+ * flag, inline error) and the `save-dashboard` call. Extracted from
10
+ * `layout-builder.tsx` unchanged: a rejected save surfaces its reason
11
+ * inline in the SaveDialog instead of failing silently (Finding [3]).
12
+ */
13
+ export function useSaveDashboard({ callTool, saveToolName, dashboardId, title, keyEntries, stepEntries, draft, onSaved, }) {
14
+ const [isBusy, setBusy] = useState(false);
15
+ const [saveOpen, setSaveOpen] = useState(false);
16
+ const [saveName, setSaveName] = useState(title ?? "");
17
+ const [saveDescription, setSaveDescription] = useState("");
18
+ // Last save failure (Finding [3]). Shown inline in the SaveDialog so a
19
+ // rejected `save-dashboard` call no longer fails silently.
20
+ const [saveError, setSaveError] = useState(null);
21
+ const saveDraft = useCallback(async () => {
22
+ if (!saveName.trim())
23
+ return;
24
+ setBusy(true);
25
+ setSaveError(null);
26
+ try {
27
+ const result = await callTool(saveToolName, {
28
+ id: dashboardId,
29
+ name: saveName.trim(),
30
+ description: saveDescription.trim() || undefined,
31
+ keys: entriesToKeys(keyEntries),
32
+ steps: stepEntries.filter((s) => s.step.trim().length > 0),
33
+ layout: draftToLayout(draft),
34
+ title,
35
+ });
36
+ const saved = parseToolResult(result);
37
+ setSaveOpen(false);
38
+ if (saved?.id && onSaved) {
39
+ onSaved({
40
+ id: saved.id,
41
+ name: saved.name ?? saveName.trim(),
42
+ });
43
+ }
44
+ }
45
+ catch (err) {
46
+ // Previously uncaught: a rejected `save-dashboard` left the dialog open
47
+ // with no feedback. Surface the reason inline (Finding [3]).
48
+ setSaveError(err instanceof Error ? err.message : String(err));
49
+ }
50
+ finally {
51
+ setBusy(false);
52
+ }
53
+ }, [
54
+ callTool,
55
+ dashboardId,
56
+ draft,
57
+ keyEntries,
58
+ onSaved,
59
+ saveDescription,
60
+ saveName,
61
+ saveToolName,
62
+ stepEntries,
63
+ title,
64
+ ]);
65
+ return {
66
+ isBusy,
67
+ saveOpen,
68
+ setSaveOpen,
69
+ saveName,
70
+ setSaveName,
71
+ saveDescription,
72
+ setSaveDescription,
73
+ saveError,
74
+ setSaveError,
75
+ saveDraft,
76
+ };
77
+ }
78
+ //# sourceMappingURL=use-save-dashboard.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"use-save-dashboard.js","sourceRoot":"","sources":["../../../src/app/builder/use-save-dashboard.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,QAAQ,EAAE,MAAM,OAAO,CAAA;AAE7C,OAAO,EAAE,eAAe,EAAE,MAAM,gCAAgC,CAAA;AAChE,OAAO,EAAE,aAAa,EAAE,aAAa,EAAmC,MAAM,oBAAoB,CAAA;AAElG,gFAAgF;AAChF,iEAAiE;AACjE,gFAAgF;AAEhF;;;;;GAKG;AACH,MAAM,UAAU,gBAAgB,CAAC,EAC/B,QAAQ,EACR,YAAY,EACZ,WAAW,EACX,KAAK,EACL,UAAU,EACV,WAAW,EACX,KAAK,EACL,OAAO,GAUR;IACC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,QAAQ,CAAC,KAAK,CAAC,CAAA;IACzC,MAAM,CAAC,QAAQ,EAAE,WAAW,CAAC,GAAG,QAAQ,CAAC,KAAK,CAAC,CAAA;IAC/C,MAAM,CAAC,QAAQ,EAAE,WAAW,CAAC,GAAG,QAAQ,CAAC,KAAK,IAAI,EAAE,CAAC,CAAA;IACrD,MAAM,CAAC,eAAe,EAAE,kBAAkB,CAAC,GAAG,QAAQ,CAAC,EAAE,CAAC,CAAA;IAC1D,uEAAuE;IACvE,2DAA2D;IAC3D,MAAM,CAAC,SAAS,EAAE,YAAY,CAAC,GAAG,QAAQ,CAAgB,IAAI,CAAC,CAAA;IAE/D,MAAM,SAAS,GAAG,WAAW,CAAC,KAAK,IAAI,EAAE;QACvC,IAAI,CAAC,QAAQ,CAAC,IAAI,EAAE;YAAE,OAAM;QAC5B,OAAO,CAAC,IAAI,CAAC,CAAA;QACb,YAAY,CAAC,IAAI,CAAC,CAAA;QAClB,IAAI,CAAC;YACH,MAAM,MAAM,GAAG,MAAM,QAAQ,CAAC,YAAY,EAAE;gBAC1C,EAAE,EAAE,WAAW;gBACf,IAAI,EAAE,QAAQ,CAAC,IAAI,EAAE;gBACrB,WAAW,EAAE,eAAe,CAAC,IAAI,EAAE,IAAI,SAAS;gBAChD,IAAI,EAAE,aAAa,CAAC,UAAU,CAAC;gBAC/B,KAAK,EAAE,WAAW,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,EAAE,CAAC,MAAM,GAAG,CAAC,CAAC;gBAC1D,MAAM,EAAE,aAAa,CAAC,KAAK,CAAC;gBAC5B,KAAK;aACN,CAAC,CAAA;YACF,MAAM,KAAK,GAAG,eAAe,CAAwC,MAAM,CAAC,CAAA;YAC5E,WAAW,CAAC,KAAK,CAAC,CAAA;YAClB,IAAI,KAAK,EAAE,EAAE,IAAI,OAAO,EAAE,CAAC;gBACzB,OAAO,CAAC;oBACN,EAAE,EAAE,KAAK,CAAC,EAAE;oBACZ,IAAI,EAAE,KAAK,CAAC,IAAI,IAAI,QAAQ,CAAC,IAAI,EAAE;iBACpC,CAAC,CAAA;YACJ,CAAC;QACH,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACb,wEAAwE;YACxE,6DAA6D;YAC7D,YAAY,CAAC,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,CAAA;QAChE,CAAC;gBAAS,CAAC;YACT,OAAO,CAAC,KAAK,CAAC,CAAA;QAChB,CAAC;IACH,CAAC,EAAE;QACD,QAAQ;QACR,WAAW;QACX,KAAK;QACL,UAAU;QACV,OAAO;QACP,eAAe;QACf,QAAQ;QACR,YAAY;QACZ,WAAW;QACX,KAAK;KACN,CAAC,CAAA;IAEF,OAAO;QACL,MAAM;QACN,QAAQ;QACR,WAAW;QACX,QAAQ;QACR,WAAW;QACX,eAAe;QACf,kBAAkB;QAClB,SAAS;QACT,YAAY;QACZ,SAAS;KACV,CAAA;AACH,CAAC"}
@@ -1 +1 @@
1
- {"version":3,"file":"layout-builder.d.ts","sourceRoot":"","sources":["../../src/app/layout-builder.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAGV,YAAY,EAEZ,eAAe,EAKhB,MAAM,2BAA2B,CAAA;AAiBlC,OAAO,EAAkB,KAAK,mBAAmB,EAAE,MAAM,qBAAqB,CAAA;AAW9E,OAAO,EAAE,KAAK,eAAe,EAAE,MAAM,sBAAsB,CAAA;AAO3D,YAAY,EAAE,mBAAmB,EAAE,MAAM,qBAAqB,CAAA;AAM9D,MAAM,WAAW,kBAAkB;IACjC,aAAa,CAAC,EAAE,YAAY,CAAA;IAC5B,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,WAAW,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;IACrC,YAAY,CAAC,EAAE,eAAe,EAAE,CAAA;IAChC,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,eAAe,CAAC,CAAA;IACxC,QAAQ,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC,OAAO,CAAC,CAAA;IAC1D,sEAAsE;IACtE,iBAAiB,CAAC,EAAE,MAAM,CAAA;IAC1B,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,MAAM,CAAC,EAAE,mBAAmB,CAAA;IAC5B,OAAO,CAAC,EAAE,CAAC,MAAM,EAAE;QAAE,EAAE,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,KAAK,IAAI,CAAA;IACxD;;;;OAIG;IACH,MAAM,CAAC,EAAE,CAAC,MAAM,CAAC,EAAE;QACjB,MAAM,EAAE,YAAY,CAAA;QACpB,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;QAC9B,KAAK,CAAC,EAAE,eAAe,EAAE,CAAA;QACzB,KAAK,CAAC,EAAE,MAAM,CAAA;QACd,OAAO,CAAC,EAAE;YACR,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;YAC7B,OAAO,EAAE,MAAM,EAAE,CAAA;YACjB,QAAQ,EAAE,MAAM,CACd,MAAM,EACN;gBAAE,IAAI,EAAE,OAAO,CAAC;gBAAC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;gBAAC,IAAI,EAAE,MAAM,CAAC;gBAAC,SAAS,EAAE,MAAM,CAAA;aAAE,CAClF,CAAA;YACD,MAAM,EAAE;gBAAE,MAAM,EAAE,MAAM,CAAC;gBAAC,MAAM,EAAE,MAAM,CAAA;aAAE,EAAE,CAAA;SAC7C,CAAA;KACF,KAAK,IAAI,CAAA;CACX;AAQD,wBAAgB,aAAa,CAAC,EAC5B,aAAa,EACb,KAAK,EACL,WAAW,EACX,YAAY,EACZ,OAAO,EACP,QAAQ,EACR,iBAA2C,EAC3C,YAA+B,EAC/B,WAAW,EACX,MAAM,EACN,OAAO,EACP,MAAM,GACP,EAAE,kBAAkB,+BA2qBpB"}
1
+ {"version":3,"file":"layout-builder.d.ts","sourceRoot":"","sources":["../../src/app/layout-builder.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,YAAY,EAAE,eAAe,EAAuB,MAAM,2BAA2B,CAAA;AAOnG,OAAO,EAAkB,KAAK,mBAAmB,EAAE,MAAM,qBAAqB,CAAA;AAc9E,OAAO,EAAE,KAAK,eAAe,EAAE,MAAM,sBAAsB,CAAA;AAO3D,YAAY,EAAE,mBAAmB,EAAE,MAAM,qBAAqB,CAAA;AAM9D,MAAM,WAAW,kBAAkB;IACjC,aAAa,CAAC,EAAE,YAAY,CAAA;IAC5B,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,WAAW,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;IACrC,YAAY,CAAC,EAAE,eAAe,EAAE,CAAA;IAChC,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,eAAe,CAAC,CAAA;IACxC,QAAQ,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC,OAAO,CAAC,CAAA;IAC1D,sEAAsE;IACtE,iBAAiB,CAAC,EAAE,MAAM,CAAA;IAC1B,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,MAAM,CAAC,EAAE,mBAAmB,CAAA;IAC5B,OAAO,CAAC,EAAE,CAAC,MAAM,EAAE;QAAE,EAAE,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,KAAK,IAAI,CAAA;IACxD;;;;OAIG;IACH,MAAM,CAAC,EAAE,CAAC,MAAM,CAAC,EAAE;QACjB,MAAM,EAAE,YAAY,CAAA;QACpB,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;QAC9B,KAAK,CAAC,EAAE,eAAe,EAAE,CAAA;QACzB,KAAK,CAAC,EAAE,MAAM,CAAA;QACd,OAAO,CAAC,EAAE;YACR,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;YAC7B,OAAO,EAAE,MAAM,EAAE,CAAA;YACjB,QAAQ,EAAE,MAAM,CACd,MAAM,EACN;gBAAE,IAAI,EAAE,OAAO,CAAC;gBAAC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;gBAAC,IAAI,EAAE,MAAM,CAAC;gBAAC,SAAS,EAAE,MAAM,CAAA;aAAE,CAClF,CAAA;YACD,MAAM,EAAE;gBAAE,MAAM,EAAE,MAAM,CAAC;gBAAC,MAAM,EAAE,MAAM,CAAA;aAAE,EAAE,CAAA;SAC7C,CAAA;KACF,KAAK,IAAI,CAAA;CACX;AAMD,wBAAgB,aAAa,CAAC,EAC5B,aAAa,EACb,KAAK,EACL,WAAW,EACX,YAAY,EACZ,OAAO,EACP,QAAQ,EACR,iBAA2C,EAC3C,YAA+B,EAC/B,WAAW,EACX,MAAM,EACN,OAAO,EACP,MAAM,GACP,EAAE,kBAAkB,+BA0XpB"}
@@ -1,24 +1,25 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { useCallback, useEffect, useMemo, useReducer, useRef, useState } from "react";
3
- import { AlertTriangle, Eye, Layers, Plus, SlidersHorizontal, X } from "lucide-react";
4
- import { parseToolResult } from "../lib/parse-tool-result.js";
5
- import { defaultStepIdFor, draftToLayout, entriesToKeys, initDraft, keysToEntries, sizeToSpan, toPipelineContext, } from "./builder/builder-model.js";
2
+ import { useCallback, useMemo, useReducer, useState } from "react";
3
+ import { Eye, Layers, SlidersHorizontal } from "lucide-react";
4
+ import { draftToLayout, entriesToKeys, initDraft, sizeToSpan } from "./builder/builder-model.js";
6
5
  import { draftReducer, initBuilderState, } from "./builder/builder-reducer.js";
7
6
  import { DEFAULT_LABELS } from "./builder/labels.js";
8
7
  import { Toolbar } from "./builder/Toolbar.js";
9
8
  import { PipelineStrip } from "./builder/PipelineStrip.js";
10
- import { Workspace } from "./builder/Workspace.js";
11
9
  import { CatalogueSheet } from "./builder/CatalogueSheet.js";
12
10
  import { SaveDialog } from "./builder/SaveDialog.js";
13
11
  import { PreviewPane } from "./builder/PreviewPane.js";
14
- import { WidgetPropsSheet } from "./widget-props-sheet.js";
15
- import { Button } from "../primitives/button.js";
12
+ import { LayoutSurface } from "./builder/LayoutSurface.js";
13
+ import { BuilderBanners } from "./builder/BuilderBanners.js";
14
+ import { EmptyHint } from "./builder/EmptyHint.js";
15
+ import { CellPropsSheet } from "./builder/CellPropsSheet.js";
16
+ import { useBuilderCatalogue } from "./builder/use-builder-catalogue.js";
17
+ import { useKeyStepEntries } from "./builder/use-key-step-entries.js";
18
+ import { useSaveDashboard } from "./builder/use-save-dashboard.js";
16
19
  import { Tabs, TabsContent, TabsList, TabsTrigger } from "../primitives/tabs.js";
17
- import { cn } from "../lib/utils.js";
18
20
  // -------------------------------------------------------------------------- //
19
21
  // Main component
20
22
  // -------------------------------------------------------------------------- //
21
- const EMPTY_CONTEXT = { steps: {}, keys: {}, errors: [] };
22
23
  export function LayoutBuilder({ initialLayout, title, initialKeys, initialSteps, widgets, callTool, catalogueToolName = "get-builder-catalogue", saveToolName = "save-dashboard", dashboardId, labels, onSaved, onExit, }) {
23
24
  const L = useMemo(() => ({ ...DEFAULT_LABELS, ...labels }), [labels]);
24
25
  // ── Draft layout (pure reducer) ─────────────────────────────────────────
@@ -54,33 +55,22 @@ export function LayoutBuilder({ initialLayout, title, initialKeys, initialSteps,
54
55
  setRenameRejected(false);
55
56
  }
56
57
  // ── Keys + steps editor state ───────────────────────────────────────────
57
- const [keyEntries, setKeyEntries] = useState(() => keysToEntries(initialKeys));
58
- const [stepEntries, setStepEntries] = useState(() => initialSteps ?? []);
59
- // ── Live snapshots from get-builder-catalogue responses ─────────────────
60
- // Start empty — the first effect below fetches the catalogue on mount.
61
- const [liveReachable, setLiveReachable] = useState([]);
62
- const [liveUnreachable, setLiveUnreachable] = useState([]);
63
- const [liveCatalog, setLiveCatalog] = useState([]);
64
- const [liveSteps, setLiveSteps] = useState([]);
65
- const [liveContext, setLiveContext] = useState(EMPTY_CONTEXT);
66
- // Pipeline validation issues from the catalogue (Finding [10]/[6]). The
67
- // catalogue is fail-soft, but `render-view` is fail-hard, so we surface
68
- // these so the user sees — before committing — that the pipeline as
69
- // configured wouldn't render.
70
- const [validationIssues, setValidationIssues] = useState([]);
71
- const [status, setStatus] = useState("idle");
72
- // Last catalogue-fetch failure (Finding [3]). Surfaced as a toolbar banner
73
- // instead of only `console.warn`-ing, so the user knows the palette/context
74
- // may be stale. Cleared on the next successful refresh.
75
- const [refreshError, setRefreshError] = useState(null);
58
+ const { keyEntries, stepEntries, addKey, updateKey, removeKey, addStep, updateStep, removeStep } = useKeyStepEntries({ initialKeys, initialSteps });
59
+ // ── Catalogue fetch (mount + on key/step edits) ─────────────────────────
60
+ // The builder fetches its own catalogue from the app-only
61
+ // `get-builder-catalogue` tool; the LLM never sees this data.
62
+ const { liveReachable, liveUnreachable, liveCatalog, liveSteps, liveContext, validationIssues, status, refreshError, widgetById, propsSchemaByWidgetId, paletteByApp, liveKeyCount, stepsByApp, } = useBuilderCatalogue({ callTool, catalogueToolName, keyEntries, stepEntries });
76
63
  // ── Save dialog ─────────────────────────────────────────────────────────
77
- const [isBusy, setBusy] = useState(false);
78
- const [saveOpen, setSaveOpen] = useState(false);
79
- const [saveName, setSaveName] = useState(title ?? "");
80
- const [saveDescription, setSaveDescription] = useState("");
81
- // Last save failure (Finding [3]). Shown inline in the SaveDialog so a
82
- // rejected `save-dashboard` call no longer fails silently.
83
- const [saveError, setSaveError] = useState(null);
64
+ const { isBusy, saveOpen, setSaveOpen, saveName, setSaveName, saveDescription, setSaveDescription, saveError, setSaveError, saveDraft, } = useSaveDashboard({
65
+ callTool,
66
+ saveToolName,
67
+ dashboardId,
68
+ title,
69
+ keyEntries,
70
+ stepEntries,
71
+ draft,
72
+ onSaved,
73
+ });
84
74
  // ── DnD ──────────────────────────────────────────────────────────────────
85
75
  const [dragOverRow, setDragOverRow] = useState(null);
86
76
  // Which cell (if any) is having its props configured. The
@@ -89,48 +79,6 @@ export function LayoutBuilder({ initialLayout, title, initialKeys, initialSteps,
89
79
  // — including across tabs. Stored as `{ rowIdx, cellIdx }` against the
90
80
  // currently active draft surface (rows or active tab's rows).
91
81
  const [configuringCell, setConfiguringCell] = useState(null);
92
- // ── Derived ─────────────────────────────────────────────────────────────
93
- const widgetById = useMemo(() => {
94
- const map = new Map();
95
- for (const w of liveReachable)
96
- map.set(w.id, w);
97
- return map;
98
- }, [liveReachable]);
99
- // Per-widget propsSchema lookup. Merged from both reachable and
100
- // unreachable lists because a saved layout can reference a widget whose
101
- // `requires` aren't currently satisfied — we still want the user to be
102
- // able to edit its props.
103
- const propsSchemaByWidgetId = useMemo(() => {
104
- const map = new Map();
105
- for (const w of liveReachable) {
106
- if (w.propsSchema)
107
- map.set(w.id, w.propsSchema);
108
- }
109
- for (const w of liveUnreachable) {
110
- if (w.propsSchema)
111
- map.set(w.id, w.propsSchema);
112
- }
113
- return map;
114
- }, [liveReachable, liveUnreachable]);
115
- const paletteByApp = useMemo(() => {
116
- const groups = new Map();
117
- for (const w of liveReachable) {
118
- if (!groups.has(w.app))
119
- groups.set(w.app, []);
120
- groups.get(w.app).push(w);
121
- }
122
- return [...groups.entries()].sort(([a], [b]) => a.localeCompare(b));
123
- }, [liveReachable]);
124
- const liveKeyCount = useMemo(() => liveCatalog.filter((c) => c.inContext).length, [liveCatalog]);
125
- const stepsByApp = useMemo(() => {
126
- const groups = new Map();
127
- for (const s of liveSteps) {
128
- if (!groups.has(s.app))
129
- groups.set(s.app, []);
130
- groups.get(s.app).push(s);
131
- }
132
- return [...groups.entries()].sort(([a], [b]) => a.localeCompare(b));
133
- }, [liveSteps]);
134
82
  const activeRows = draft.kind === "tabs" ? (draft.tabs[activeTabIndex]?.rows ?? []) : draft.rows;
135
83
  // ── Layout mutations (dispatch pure actions) ────────────────────────────
136
84
  const addWidgetToRow = useCallback((widgetId, targetRowIdx) => {
@@ -171,149 +119,6 @@ export function LayoutBuilder({ initialLayout, title, initialKeys, initialSteps,
171
119
  setEditingTabIdx(null);
172
120
  }, [draft, setEditingTabIdx]);
173
121
  const removeTab = useCallback((tabIdx) => dispatch({ type: "removeTab", tabIdx }), []);
174
- // ── Keys/steps mutations ────────────────────────────────────────────────
175
- const addKey = useCallback((prefilledName) => {
176
- setKeyEntries((prev) => {
177
- if (prefilledName) {
178
- if (prev.some((e) => e.name === prefilledName))
179
- return prev;
180
- return [...prev, { name: prefilledName, rawValue: "" }];
181
- }
182
- return [...prev, { name: "", rawValue: "" }];
183
- });
184
- }, []);
185
- const updateKey = useCallback((idx, patch) => {
186
- setKeyEntries((prev) => prev.map((e, i) => (i === idx ? { ...e, ...patch } : e)));
187
- }, []);
188
- const removeKey = useCallback((idx) => {
189
- setKeyEntries((prev) => prev.filter((_, i) => i !== idx));
190
- }, []);
191
- const addStep = useCallback((stepId) => {
192
- setStepEntries((prev) => {
193
- const taken = new Set(prev.map((s) => s.id));
194
- const newId = stepId ? defaultStepIdFor(stepId, taken) : `step-${prev.length + 1}`;
195
- return [...prev, { id: newId, step: stepId ?? "", optional: false }];
196
- });
197
- }, []);
198
- const updateStep = useCallback((idx, patch) => {
199
- setStepEntries((prev) => prev.map((s, i) => (i === idx ? { ...s, ...patch } : s)));
200
- }, []);
201
- const removeStep = useCallback((idx) => {
202
- setStepEntries((prev) => prev.filter((_, i) => i !== idx));
203
- }, []);
204
- // ── Catalogue fetch (mount + on key/step edits) ─────────────────────────
205
- // The builder fetches its own catalogue from the app-only
206
- // `get-builder-catalogue` tool; the LLM never sees this data.
207
- const refreshBuilder = useCallback(async () => {
208
- setStatus("refreshing");
209
- try {
210
- const result = await callTool(catalogueToolName, {
211
- keys: entriesToKeys(keyEntries),
212
- steps: stepEntries.filter((s) => s.step.trim().length > 0),
213
- });
214
- const sc = parseToolResult(result);
215
- if (sc?.reachableWidgets)
216
- setLiveReachable(sc.reachableWidgets);
217
- if (sc?.unreachableWidgets)
218
- setLiveUnreachable(sc.unreachableWidgets);
219
- if (sc?.availableSteps)
220
- setLiveSteps(sc.availableSteps);
221
- if (sc?.keyCatalog)
222
- setLiveCatalog(sc.keyCatalog);
223
- if (sc?.context)
224
- setLiveContext(toPipelineContext(sc.context));
225
- // Pipeline issues are fail-soft in the catalogue but fail-hard in
226
- // render-view; surface them so the user sees the render path wouldn't
227
- // run as configured (Finding [10]/[6]). Coerce undefined → [] so a
228
- // fixed pipeline clears a previously-shown warning.
229
- setValidationIssues(sc?.validationIssues ?? []);
230
- // A successful fetch clears any stale failure banner.
231
- setRefreshError(null);
232
- }
233
- catch (err) {
234
- // Fail soft: when the host's server hasn't registered the app-only
235
- // `get-builder-catalogue` tool (the builder platform is opt-in via
236
- // `app.builder`), the call rejects. Degrade to the static catalogue
237
- // already derived from the layout rather than crashing the iframe —
238
- // but surface the failure so the user knows the palette may be stale
239
- // (Finding [3]).
240
- const reason = err instanceof Error ? err.message : String(err);
241
- console.warn(`[mcp-toolkit] builder catalogue fetch failed (${catalogueToolName}): ${reason}. ` +
242
- "The builder platform may be disabled on this server (app.builder).");
243
- setRefreshError(reason);
244
- }
245
- finally {
246
- setStatus("idle");
247
- }
248
- }, [catalogueToolName, callTool, keyEntries, stepEntries]);
249
- // Fetch the catalogue on mount (no debounce) so the builder is ready
250
- // immediately. Then on every keys/steps edit, debounce 600ms before
251
- // re-fetching. The ref pattern keeps the effect dependent on only
252
- // keyEntries / stepEntries.
253
- const refreshRef = useRef(refreshBuilder);
254
- useEffect(() => {
255
- refreshRef.current = refreshBuilder;
256
- }, [refreshBuilder]);
257
- const isFirstRun = useRef(true);
258
- useEffect(() => {
259
- if (isFirstRun.current) {
260
- isFirstRun.current = false;
261
- void refreshRef.current();
262
- return;
263
- }
264
- setStatus("pending");
265
- const handle = window.setTimeout(() => {
266
- void refreshRef.current();
267
- }, 600);
268
- return () => {
269
- window.clearTimeout(handle);
270
- };
271
- }, [keyEntries, stepEntries]);
272
- // ── Save ────────────────────────────────────────────────────────────────
273
- const saveDraft = useCallback(async () => {
274
- if (!saveName.trim())
275
- return;
276
- setBusy(true);
277
- setSaveError(null);
278
- try {
279
- const result = await callTool(saveToolName, {
280
- id: dashboardId,
281
- name: saveName.trim(),
282
- description: saveDescription.trim() || undefined,
283
- keys: entriesToKeys(keyEntries),
284
- steps: stepEntries.filter((s) => s.step.trim().length > 0),
285
- layout: draftToLayout(draft),
286
- title,
287
- });
288
- const saved = parseToolResult(result);
289
- setSaveOpen(false);
290
- if (saved?.id && onSaved) {
291
- onSaved({
292
- id: saved.id,
293
- name: saved.name ?? saveName.trim(),
294
- });
295
- }
296
- }
297
- catch (err) {
298
- // Previously uncaught: a rejected `save-dashboard` left the dialog open
299
- // with no feedback. Surface the reason inline (Finding [3]).
300
- setSaveError(err instanceof Error ? err.message : String(err));
301
- }
302
- finally {
303
- setBusy(false);
304
- }
305
- }, [
306
- callTool,
307
- dashboardId,
308
- draft,
309
- keyEntries,
310
- onSaved,
311
- saveDescription,
312
- saveName,
313
- saveToolName,
314
- stepEntries,
315
- title,
316
- ]);
317
122
  // ── Smart action: add a producing step for a missing key ────────────────
318
123
  const addProducingStepFor = useCallback((key) => {
319
124
  const producer = liveCatalog.find((c) => c.key === key)?.producedBySteps[0];
@@ -324,25 +129,6 @@ export function LayoutBuilder({ initialLayout, title, initialKeys, initialSteps,
324
129
  const widgetProps = useMemo(() => ({ keys: liveContext.keys, context: liveContext }), [liveContext]);
325
130
  const hasAnyCell = activeRows.some((r) => r.row.length > 0);
326
131
  const liveKeysMap = liveContext.keys;
327
- const layoutContent = draft.kind === "tabs" ? (_jsxs(Tabs, { value: draft.tabs[activeTabIndex]?.label ?? "", onValueChange: (label) => {
328
- const idx = draft.tabs.findIndex((t) => t.label === label);
329
- if (idx >= 0)
330
- setActiveTabIndex(idx);
331
- }, children: [_jsxs("div", { className: "mb-3 flex items-center gap-1", children: [_jsx(TabsList, { children: draft.tabs.map((tab, idx) => (_jsxs("div", { className: "group relative inline-flex", children: [_jsx(TabsTrigger, { value: tab.label, className: "pr-7", onDoubleClick: () => setEditingTabIdx(idx), title: "Double-click to rename", children: editingTabIdx === idx ? (_jsx("input", { autoFocus: true, defaultValue: tab.label, "aria-invalid": renameRejected, onClick: (e) => e.stopPropagation(), onPointerDown: (e) => e.stopPropagation(), onKeyDown: (e) => {
332
- if (e.key === "Enter") {
333
- e.preventDefault();
334
- commitRename(idx, e.currentTarget.value);
335
- }
336
- else if (e.key === "Escape") {
337
- e.preventDefault();
338
- setEditingTabIdx(null);
339
- }
340
- }, onBlur: (e) => {
341
- commitRename(idx, e.currentTarget.value);
342
- }, className: cn("w-24 border-none bg-transparent p-0 text-inherit outline-none focus:ring-0", renameRejected && "text-destructive"), size: Math.max(tab.label.length + 1, 6) })) : (_jsx("span", { children: tab.label })) }), _jsx("button", { type: "button", "aria-label": L.removeTab, title: L.removeTab, onPointerDown: (e) => e.stopPropagation(), onClick: (e) => {
343
- e.stopPropagation();
344
- removeTab(idx);
345
- }, className: "text-muted-foreground/60 hover:text-foreground absolute top-1/2 right-1 -translate-y-1/2 rounded p-0.5 opacity-0 transition-opacity group-hover:opacity-100 data-[state=active]:opacity-100", children: _jsx(X, { className: "size-3" }) })] }, tab.label))) }), _jsx(Button, { variant: "ghost", size: "icon-sm", onClick: addTab, "aria-label": L.addTab, title: L.addTab, children: _jsx(Plus, {}) }), _jsx("span", { className: "text-muted-foreground/70 ml-2 hidden text-[10px] sm:inline", children: "double-click a tab to rename" })] }), renameRejected && editingTabIdx !== null && (_jsx("p", { className: "text-destructive -mt-1 mb-2 text-[11px]", role: "alert", children: L.renameDuplicate })), draft.tabs.map((tab, idx) => (_jsx(TabsContent, { value: tab.label, className: "mt-0", children: idx === activeTabIndex ? (_jsx(Workspace, { L: L, paletteByApp: paletteByApp, paletteCount: liveReachable.length, activeRows: activeRows, focusedRowIndex: focusedRowIndex, setFocusedRowIndex: setFocusedRowIndex, dragOverRow: dragOverRow, setDragOverRow: setDragOverRow, onAddRow: addRow, onRemoveRow: removeRow, onMoveRow: moveRow, onSetCellSpan: setCellSpan, onRemoveCell: removeCell, onAddWidget: addWidgetToRow, onConfigureCell: (rowIdx, cellIdx) => setConfiguringCell({ rowIdx, cellIdx }), propsSchemaByWidgetId: propsSchemaByWidgetId, widgets: widgets, widgetProps: widgetProps })) : null }, tab.label)))] })) : (_jsx(Workspace, { L: L, paletteByApp: paletteByApp, paletteCount: liveReachable.length, activeRows: activeRows, focusedRowIndex: focusedRowIndex, setFocusedRowIndex: setFocusedRowIndex, dragOverRow: dragOverRow, setDragOverRow: setDragOverRow, onAddRow: addRow, onRemoveRow: removeRow, onMoveRow: moveRow, onSetCellSpan: setCellSpan, onRemoveCell: removeCell, onAddWidget: addWidgetToRow, onConfigureCell: (rowIdx, cellIdx) => setConfiguringCell({ rowIdx, cellIdx }), propsSchemaByWidgetId: propsSchemaByWidgetId, widgets: widgets, widgetProps: widgetProps, showAddTabButton: true, onAddTab: addTab }));
346
132
  return (_jsxs("div", { className: "flex min-h-[60vh] flex-col gap-4 p-1", children: [_jsx(Toolbar, { L: L, previewMode: view === "preview", isBusy: isBusy, status: status, liveKeys: liveKeyCount, reachableCount: liveReachable.length, unreachableCount: liveUnreachable.length, onOpenCatalogue: () => setCatalogueOpen(true), onOpenSave: () => setSaveOpen(true), onDone: onExit
347
133
  ? () => onExit({
348
134
  layout: draftToLayout(draft),
@@ -364,7 +150,26 @@ export function LayoutBuilder({ initialLayout, title, initialKeys, initialSteps,
364
150
  errors: liveContext.errors,
365
151
  },
366
152
  })
367
- : undefined, hasAnyCell: hasAnyCell }), _jsx(BuilderBanners, { L: L, catalogueToolName: catalogueToolName, refreshError: refreshError, validationIssues: validationIssues }), _jsxs(Tabs, { value: view, onValueChange: (v) => setView(v), children: [_jsxs(TabsList, { children: [_jsxs(TabsTrigger, { value: "layout", children: [_jsx(Layers, { className: "size-3.5" }), L.viewLayoutTab] }), _jsxs(TabsTrigger, { value: "pipeline", children: [_jsx(SlidersHorizontal, { className: "size-3.5" }), L.viewPipelineTab, (keyEntries.length > 0 || stepEntries.length > 0) && (_jsxs("span", { className: "bg-muted text-muted-foreground ml-1 rounded px-1 font-mono text-[10px]", children: [keyEntries.length, "/", stepEntries.length] }))] }), _jsxs(TabsTrigger, { value: "preview", disabled: !hasAnyCell, children: [_jsx(Eye, { className: "size-3.5" }), L.viewPreviewTab] })] }), _jsx(TabsContent, { value: "layout", className: "mt-4", children: liveReachable.length === 0 && !hasAnyCell ? (_jsx(EmptyHint, { L: L, onGotoPipeline: () => setView("pipeline") })) : (layoutContent) }), _jsx(TabsContent, { value: "pipeline", className: "mt-4", children: _jsx(PipelineStrip, { L: L, keyEntries: keyEntries, stepEntries: stepEntries, keyCatalog: liveCatalog, availableSteps: liveSteps, stepsByApp: stepsByApp, liveKeys: liveKeysMap, onAddKey: addKey, onUpdateKey: updateKey, onRemoveKey: removeKey, onAddStep: addStep, onUpdateStep: updateStep, onRemoveStep: removeStep, status: status }) }), _jsx(TabsContent, { value: "preview", className: "mt-4", children: _jsx(PreviewPane, { draft: draft, activeTabIndex: activeTabIndex, setActiveTabIndex: setActiveTabIndex, widgets: widgets, widgetProps: widgetProps }) })] }), _jsx(CatalogueSheet, { L: L, open: catalogueOpen, onOpenChange: setCatalogueOpen, keyCatalog: liveCatalog, availableSteps: liveSteps, unreachableWidgets: liveUnreachable, onSeedKey: (name) => {
153
+ : undefined, hasAnyCell: hasAnyCell }), _jsx(BuilderBanners, { L: L, catalogueToolName: catalogueToolName, refreshError: refreshError, validationIssues: validationIssues }), _jsxs(Tabs, { value: view, onValueChange: (v) => setView(v), children: [_jsxs(TabsList, { children: [_jsxs(TabsTrigger, { value: "layout", children: [_jsx(Layers, { className: "size-3.5" }), L.viewLayoutTab] }), _jsxs(TabsTrigger, { value: "pipeline", children: [_jsx(SlidersHorizontal, { className: "size-3.5" }), L.viewPipelineTab, (keyEntries.length > 0 || stepEntries.length > 0) && (_jsxs("span", { className: "bg-muted text-muted-foreground ml-1 rounded px-1 font-mono text-[10px]", children: [keyEntries.length, "/", stepEntries.length] }))] }), _jsxs(TabsTrigger, { value: "preview", disabled: !hasAnyCell, children: [_jsx(Eye, { className: "size-3.5" }), L.viewPreviewTab] })] }), _jsx(TabsContent, { value: "layout", className: "mt-4", children: liveReachable.length === 0 && !hasAnyCell ? (_jsx(EmptyHint, { L: L, onGotoPipeline: () => setView("pipeline") })) : (_jsx(LayoutSurface, { L: L, draft: draft, activeTabIndex: activeTabIndex, setActiveTabIndex: setActiveTabIndex, editingTabIdx: editingTabIdx, setEditingTabIdx: setEditingTabIdx, renameRejected: renameRejected, commitRename: commitRename, onAddTab: addTab, onRemoveTab: removeTab, workspace: {
154
+ L,
155
+ paletteByApp,
156
+ paletteCount: liveReachable.length,
157
+ activeRows,
158
+ focusedRowIndex,
159
+ setFocusedRowIndex,
160
+ dragOverRow,
161
+ setDragOverRow,
162
+ onAddRow: addRow,
163
+ onRemoveRow: removeRow,
164
+ onMoveRow: moveRow,
165
+ onSetCellSpan: setCellSpan,
166
+ onRemoveCell: removeCell,
167
+ onAddWidget: addWidgetToRow,
168
+ onConfigureCell: (rowIdx, cellIdx) => setConfiguringCell({ rowIdx, cellIdx }),
169
+ propsSchemaByWidgetId,
170
+ widgets,
171
+ widgetProps,
172
+ } })) }), _jsx(TabsContent, { value: "pipeline", className: "mt-4", children: _jsx(PipelineStrip, { L: L, keyEntries: keyEntries, stepEntries: stepEntries, keyCatalog: liveCatalog, availableSteps: liveSteps, stepsByApp: stepsByApp, liveKeys: liveKeysMap, onAddKey: addKey, onUpdateKey: updateKey, onRemoveKey: removeKey, onAddStep: addStep, onUpdateStep: updateStep, onRemoveStep: removeStep, status: status }) }), _jsx(TabsContent, { value: "preview", className: "mt-4", children: _jsx(PreviewPane, { draft: draft, activeTabIndex: activeTabIndex, setActiveTabIndex: setActiveTabIndex, widgets: widgets, widgetProps: widgetProps }) })] }), _jsx(CatalogueSheet, { L: L, open: catalogueOpen, onOpenChange: setCatalogueOpen, keyCatalog: liveCatalog, availableSteps: liveSteps, unreachableWidgets: liveUnreachable, onSeedKey: (name) => {
368
173
  addKey(name);
369
174
  setCatalogueOpen(false);
370
175
  setView("pipeline");
@@ -377,42 +182,6 @@ export function LayoutBuilder({ initialLayout, title, initialKeys, initialSteps,
377
182
  // dismissed so a fresh attempt starts clean.
378
183
  setSaveError(null);
379
184
  setSaveOpen(open);
380
- }, name: saveName, setName: setSaveName, description: saveDescription, setDescription: setSaveDescription, onSubmit: saveDraft, busy: isBusy, error: saveError }), (() => {
381
- if (!configuringCell)
382
- return null;
383
- const cell = activeRows[configuringCell.rowIdx]?.row[configuringCell.cellIdx];
384
- if (!cell)
385
- return null;
386
- const schema = propsSchemaByWidgetId.get(cell.widget);
387
- return (_jsx(WidgetPropsSheet, { open: true, onOpenChange: (open) => {
388
- if (!open)
389
- setConfiguringCell(null);
390
- }, widgetId: cell.widget, schema: schema, value: cell.props, onApply: (next) => {
391
- setCellProps(configuringCell.rowIdx, configuringCell.cellIdx, next);
392
- setConfiguringCell(null);
393
- } }));
394
- })()] }));
395
- }
396
- function EmptyHint({ L, onGotoPipeline, }) {
397
- return (_jsxs("div", { className: "border-muted-foreground/30 text-muted-foreground flex flex-col items-center gap-2 rounded-md border border-dashed p-10 text-sm", children: [_jsx(Layers, { className: "size-5" }), _jsx("p", { children: "No widgets reachable yet." }), _jsxs("p", { className: "text-xs", children: ["Open the ", _jsx("span", { className: "font-medium", children: L.viewPipelineTab }), " tab to seed keys or add a pipeline step \u2014 the palette unlocks as soon as a widget's contract is satisfied."] }), _jsxs(Button, { variant: "outline", size: "sm", onClick: onGotoPipeline, className: "mt-1", children: [_jsx(SlidersHorizontal, {}), " Open ", L.viewPipelineTab] })] }));
398
- }
399
- // -------------------------------------------------------------------------- //
400
- // Builder banners — catalogue-fetch failure + pipeline validation warning
401
- // -------------------------------------------------------------------------- //
402
- /**
403
- * Inline feedback strip under the toolbar. Surfaces two things the builder
404
- * previously swallowed:
405
- * - a `get-builder-catalogue` fetch failure (Finding [3]) — only console-
406
- * logged before, so the user couldn't tell the palette was stale, and
407
- * - pipeline validation issues (Finding [10]/[6]) the catalogue reports
408
- * fail-soft but `render-view` would reject fail-hard.
409
- *
410
- * Renders nothing when there's no error and no issues, so the happy path is
411
- * visually unchanged.
412
- */
413
- function BuilderBanners({ L, catalogueToolName, refreshError, validationIssues, }) {
414
- if (!refreshError && validationIssues.length === 0)
415
- return null;
416
- return (_jsxs("div", { className: "flex flex-col gap-2", role: "status", "aria-live": "polite", children: [refreshError && (_jsxs("div", { className: "border-destructive/30 bg-destructive/10 text-destructive flex items-start gap-2 rounded-md border px-3 py-2 text-xs", children: [_jsx(AlertTriangle, { className: "mt-0.5 size-3.5 shrink-0", "aria-hidden": "true" }), _jsxs("div", { className: "min-w-0", children: [_jsx("p", { className: "font-medium", children: L.catalogueError }), _jsxs("p", { className: "text-destructive/80 mt-0.5 break-words", children: [_jsx("span", { className: "font-mono", children: catalogueToolName }), ": ", refreshError] })] })] })), validationIssues.length > 0 && (_jsxs("div", { className: "flex items-start gap-2 rounded-md border border-amber-500/30 bg-amber-500/10 px-3 py-2 text-xs text-amber-700 dark:text-amber-400", children: [_jsx(AlertTriangle, { className: "mt-0.5 size-3.5 shrink-0", "aria-hidden": "true" }), _jsxs("div", { className: "min-w-0", children: [_jsx("p", { className: "font-medium", children: L.validationWarning }), _jsx("ul", { className: "mt-0.5 list-inside list-disc space-y-0.5", children: validationIssues.map((issue, idx) => (_jsx("li", { className: "break-words", children: issue }, idx))) })] })] }))] }));
185
+ }, name: saveName, setName: setSaveName, description: saveDescription, setDescription: setSaveDescription, onSubmit: saveDraft, busy: isBusy, error: saveError }), _jsx(CellPropsSheet, { configuringCell: configuringCell, activeRows: activeRows, propsSchemaByWidgetId: propsSchemaByWidgetId, onApply: setCellProps, onClose: () => setConfiguringCell(null) })] }));
417
186
  }
418
187
  //# sourceMappingURL=layout-builder.js.map