@burdenoff/microfe-bigconsole 2026.1008.11 → 2026.1008.13

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 (140) hide show
  1. package/dist/bigconsole/BigConsoleRoutes.d.ts.map +1 -1
  2. package/dist/bigconsole/BigConsoleRoutes.js +136 -127
  3. package/dist/bigconsole/BigConsoleRoutes.js.map +1 -1
  4. package/dist/bigconsole/assistant/assistantProduct.d.ts.map +1 -1
  5. package/dist/bigconsole/assistant/assistantProduct.js +3 -1
  6. package/dist/bigconsole/assistant/assistantProduct.js.map +1 -1
  7. package/dist/bigconsole/components/builder/inspector/DataBindingSection.js +31 -31
  8. package/dist/bigconsole/components/dashboard/CreateDashboardDialog.js +44 -44
  9. package/dist/bigconsole/components/dashboard/CreateDashboardDialog.js.map +1 -1
  10. package/dist/bigconsole/components/dashboard/DashboardCanvas.js +36 -36
  11. package/dist/bigconsole/components/dashboard/DashboardCanvas.js.map +1 -1
  12. package/dist/bigconsole/components/dashboard/DashboardToolbar.js +59 -59
  13. package/dist/bigconsole/components/dashboard/DrilldownBreadcrumb.js +15 -15
  14. package/dist/bigconsole/components/dashboard/GlobalFiltersBar/FilterChip.js +30 -30
  15. package/dist/bigconsole/components/dashboard/GlobalFiltersBar/FilterEditor.js +41 -41
  16. package/dist/bigconsole/components/dashboard/GlobalFiltersBar/FilterPresetSelector.js +34 -34
  17. package/dist/bigconsole/components/dashboard/GlobalFiltersBar/GlobalFiltersBar.js +34 -34
  18. package/dist/bigconsole/components/dashboard/ImportDashboardDialog.js +110 -110
  19. package/dist/bigconsole/components/dashboard/ImportDashboardDialog.js.map +1 -1
  20. package/dist/bigconsole/components/dashboard/PublishToStoreDialog.js +21 -21
  21. package/dist/bigconsole/components/dashboard/SavedViewsDropdown/SavedViewsDropdown.js +46 -46
  22. package/dist/bigconsole/components/dashboard/ShareDialog/EmbedTab.js +91 -91
  23. package/dist/bigconsole/components/dashboard/ShareDialog/EmbedTab.js.map +1 -1
  24. package/dist/bigconsole/components/dashboard/ShareDialog/ShareDialog.js +101 -101
  25. package/dist/bigconsole/components/dashboard/ShareDialog/ShareDialog.js.map +1 -1
  26. package/dist/bigconsole/components/dashboard/TemplateInstallOptions.js +31 -31
  27. package/dist/bigconsole/components/dashboard/TemplateLayoutPreview.js +15 -15
  28. package/dist/bigconsole/components/dashboard/TemplatePicker/TemplatePicker.js +1 -1
  29. package/dist/bigconsole/components/dashboard/WidgetPalette.js +46 -46
  30. package/dist/bigconsole/components/datasink/AlertExecutionHistory.js +37 -37
  31. package/dist/bigconsole/components/datasink/AlertRuleEditor.js +66 -66
  32. package/dist/bigconsole/components/datasink/CacheTTLSelector.js +35 -35
  33. package/dist/bigconsole/components/datasink/DataSinkCard.js +29 -29
  34. package/dist/bigconsole/components/datasink/DataSinkDeleteDialog.js +18 -18
  35. package/dist/bigconsole/components/datasink/DataSinkRefreshDialog.js +20 -20
  36. package/dist/bigconsole/components/datasink/ImportDataGuideDialog.js +47 -47
  37. package/dist/bigconsole/components/datasink/ImportDataSinkDialog.js +91 -91
  38. package/dist/bigconsole/components/datasink/RefreshTypeSelector.js +35 -35
  39. package/dist/bigconsole/components/parser/DataSinkSelector.js +40 -40
  40. package/dist/bigconsole/components/parser/ImportParserDialog.js +120 -120
  41. package/dist/bigconsole/components/parser/ImportParserDialog.js.map +1 -1
  42. package/dist/bigconsole/components/parser/ParserSelector.js +51 -51
  43. package/dist/bigconsole/components/pipeline/PipelineScopePicker.js +10 -10
  44. package/dist/bigconsole/components/preview/AiPreviewMiniPanel.js +27 -27
  45. package/dist/bigconsole/components/preview/AiPreviewPanel.js +30 -30
  46. package/dist/bigconsole/components/widgets/ActionConfigDialog.js +4 -4
  47. package/dist/bigconsole/components/widgets/DrilldownConfigPanel.js +93 -93
  48. package/dist/bigconsole/components/widgets/DrilldownConfigPanel.js.map +1 -1
  49. package/dist/bigconsole/components/widgets/PropertiesPanel.d.ts.map +1 -1
  50. package/dist/bigconsole/components/widgets/PropertiesPanel.js +507 -499
  51. package/dist/bigconsole/components/widgets/PropertiesPanel.js.map +1 -1
  52. package/dist/bigconsole/components/widgets/calendar/CalendarWidget.js +4 -4
  53. package/dist/bigconsole/components/widgets/chart/ChartConfig.js +48 -48
  54. package/dist/bigconsole/components/widgets/comments/WidgetComments.js +46 -46
  55. package/dist/bigconsole/components/widgets/event-logs/EventLogViewer.js +61 -61
  56. package/dist/bigconsole/components/widgets/form/FormWidget.js +36 -36
  57. package/dist/bigconsole/components/widgets/gauge/GaugeConfig.js +42 -42
  58. package/dist/bigconsole/components/widgets/heatmap/HeatmapConfig.js +25 -25
  59. package/dist/bigconsole/components/widgets/kanban/KanbanWidget.js +4 -4
  60. package/dist/bigconsole/components/widgets/list/ListConfig.js +30 -30
  61. package/dist/bigconsole/components/widgets/metric-card/MetricCardConfig.js +38 -38
  62. package/dist/bigconsole/components/widgets/metric-card/SparklineChart.js +9 -9
  63. package/dist/bigconsole/components/widgets/metric-card/TrendIndicator.js +9 -9
  64. package/dist/bigconsole/components/widgets/pivot-table/PivotTableWidget.js +49 -49
  65. package/dist/bigconsole/components/widgets/progress/ProgressConfig.js +39 -39
  66. package/dist/bigconsole/components/widgets/table/TableConfig.js +73 -73
  67. package/dist/bigconsole/components/widgets/webhook-config/WebhookConfigPanel.js +64 -64
  68. package/dist/bigconsole/components/workflow/WorkflowOpenGuard.js +15 -15
  69. package/dist/bigconsole/components/workflow/WorkflowSinkLinkForm.js +15 -15
  70. package/dist/bigconsole/customWidget/CustomWidgetBuilderSheet.js +9 -9
  71. package/dist/bigconsole/customWidget/CustomWidgetProposalPanel.js +67 -67
  72. package/dist/bigconsole/customWidget/CustomWidgetProposalPanel.js.map +1 -1
  73. package/dist/bigconsole/hooks/useDashboardLayoutBaseline.d.ts +15 -0
  74. package/dist/bigconsole/hooks/useDashboardLayoutBaseline.d.ts.map +1 -0
  75. package/dist/bigconsole/hooks/useDashboardLayoutBaseline.js +34 -0
  76. package/dist/bigconsole/hooks/useDashboardLayoutBaseline.js.map +1 -0
  77. package/dist/bigconsole/hooks/useDrilldown.js +3 -3
  78. package/dist/bigconsole/hooks/useGlobalFilters.js +3 -3
  79. package/dist/bigconsole/pages/AdminPage.js +130 -130
  80. package/dist/bigconsole/pages/DashboardBuilderPage.d.ts.map +1 -1
  81. package/dist/bigconsole/pages/DashboardBuilderPage.js +72 -60
  82. package/dist/bigconsole/pages/DashboardBuilderPage.js.map +1 -1
  83. package/dist/bigconsole/pages/DashboardViewPage.js +1 -1
  84. package/dist/bigconsole/pages/DataSinkAlertsPage.d.ts.map +1 -1
  85. package/dist/bigconsole/pages/DataSinkAlertsPage.js +30 -23
  86. package/dist/bigconsole/pages/DataSinkAlertsPage.js.map +1 -1
  87. package/dist/bigconsole/pages/DataSinkBuilderPage.d.ts.map +1 -1
  88. package/dist/bigconsole/pages/DataSinkBuilderPage.js +273 -264
  89. package/dist/bigconsole/pages/DataSinkBuilderPage.js.map +1 -1
  90. package/dist/bigconsole/pages/DataSinkViewPage.js +1 -1
  91. package/dist/bigconsole/pages/DataSinksPage.js +1 -1
  92. package/dist/bigconsole/pages/DataSourcesPage.js +5 -5
  93. package/dist/bigconsole/pages/InsightsPage.d.ts.map +1 -1
  94. package/dist/bigconsole/pages/InsightsPage.js +279 -265
  95. package/dist/bigconsole/pages/InsightsPage.js.map +1 -1
  96. package/dist/bigconsole/pages/OverviewPage.js +47 -47
  97. package/dist/bigconsole/pages/OverviewPage.js.map +1 -1
  98. package/dist/bigconsole/pages/ParserBuilderPage.d.ts.map +1 -1
  99. package/dist/bigconsole/pages/ParserBuilderPage.js +229 -213
  100. package/dist/bigconsole/pages/ParserBuilderPage.js.map +1 -1
  101. package/dist/bigconsole/pages/ParserViewPage.js +97 -97
  102. package/dist/bigconsole/pages/PipelineBuilderPage.d.ts.map +1 -1
  103. package/dist/bigconsole/pages/PipelineBuilderPage.js +256 -214
  104. package/dist/bigconsole/pages/PipelineBuilderPage.js.map +1 -1
  105. package/dist/bigconsole/pages/PipelineViewPage.js +146 -146
  106. package/dist/bigconsole/pages/PipelinesPage.js +88 -88
  107. package/dist/bigconsole/pages/PipelinesPage.js.map +1 -1
  108. package/dist/bigconsole/pages/SettingsPage.d.ts.map +1 -1
  109. package/dist/bigconsole/pages/SettingsPage.js +179 -173
  110. package/dist/bigconsole/pages/SettingsPage.js.map +1 -1
  111. package/dist/bigconsole/pages/WidgetDemoPage.js +42 -42
  112. package/dist/bigconsole/pages/WorkflowViewPage.js +1 -1
  113. package/dist/bigconsole/pages/WorkflowsPage.js +141 -141
  114. package/dist/bigconsole/pages/WorkflowsPage.js.map +1 -1
  115. package/dist/bigconsole/providers/BigConsoleQuotaProvider.js +12 -12
  116. package/dist/bigconsole/renderers/adapters/AdaptiveCardRenderer.js +34 -34
  117. package/dist/bigconsole/renderers/adapters/BlockKitRenderer.js +79 -79
  118. package/dist/bigconsole/renderers/adapters/CustomRenderer.js +46 -46
  119. package/dist/bigconsole/store/widgetStore.d.ts +12 -0
  120. package/dist/bigconsole/store/widgetStore.d.ts.map +1 -1
  121. package/dist/bigconsole/store/widgetStore.js +3 -1
  122. package/dist/bigconsole/store/widgetStore.js.map +1 -1
  123. package/dist/bigconsole/utils/formDirty.d.ts +83 -0
  124. package/dist/bigconsole/utils/formDirty.d.ts.map +1 -0
  125. package/dist/bigconsole/utils/formDirty.js +23 -0
  126. package/dist/bigconsole/utils/formDirty.js.map +1 -0
  127. package/dist/bigconsole/utils/insightsDraft.d.ts +30 -0
  128. package/dist/bigconsole/utils/insightsDraft.d.ts.map +1 -0
  129. package/dist/bigconsole/utils/insightsDraft.js +10 -0
  130. package/dist/bigconsole/utils/insightsDraft.js.map +1 -0
  131. package/dist/bigconsole/utils/layoutBaseline.d.ts +131 -0
  132. package/dist/bigconsole/utils/layoutBaseline.d.ts.map +1 -0
  133. package/dist/bigconsole/utils/layoutBaseline.js +98 -0
  134. package/dist/bigconsole/utils/layoutBaseline.js.map +1 -0
  135. package/dist/bigconsole/utils/widgetFormDirty.d.ts +17 -0
  136. package/dist/bigconsole/utils/widgetFormDirty.d.ts.map +1 -0
  137. package/dist/bigconsole/utils/widgetFormDirty.js +15 -0
  138. package/dist/bigconsole/utils/widgetFormDirty.js.map +1 -0
  139. package/dist/translations/en.d.ts.map +1 -1
  140. package/package.json +1 -1
@@ -7,8 +7,8 @@ import { localProposalSource as ne } from "./proposalSource.js";
7
7
  import { memo as a, useCallback as o, useEffect as re, useMemo as s, useState as c } from "react";
8
8
  import { AlertTriangle as l, Check as u, Loader2 as d, RefreshCw as ie, Sparkles as ae, X as oe } from "lucide-react";
9
9
  import { Button as f, Input as p, Label as m, Textarea as se } from "@burdenoff/fe-libs/ui";
10
- import { Fragment as ce, jsx as h, jsxs as g } from "react/jsx-runtime";
11
- import { useI18n as _ } from "@burdenoff/fe-libs/shared/providers/shell/I18nProvider";
10
+ import { useI18n as ce } from "@burdenoff/fe-libs/shared/providers/shell/I18nProvider";
11
+ import { Fragment as h, jsx as g, jsxs as _ } from "react/jsx-runtime";
12
12
  //#region src/bigconsole/customWidget/CustomWidgetProposalPanel.tsx
13
13
  var v = "w-full", y = {
14
14
  loading: "neutral",
@@ -61,7 +61,7 @@ function S(e) {
61
61
  return 0;
62
62
  }
63
63
  var C = a(function({ sinks: a, parsers: C, resolvePreviewData: w, onApprove: T, onClose: E, source: D = ne }) {
64
- let { t: O } = _(), k = s(() => ({
64
+ let { t: O } = ce(), k = s(() => ({
65
65
  loading: {
66
66
  title: O("bigconsole.widget.customWidget.preview.loading.title", "Building preview…"),
67
67
  body: O("bigconsole.widget.customWidget.preview.loading.body", "Running the parser against the selected DataSink.")
@@ -160,43 +160,43 @@ var C = a(function({ sinks: a, parsers: C, resolvePreviewData: w, onApprove: T,
160
160
  E,
161
161
  O
162
162
  ]), Q = J.issues.filter((e) => e.severity === "error"), $ = J.issues.filter((e) => e.severity === "warning");
163
- return /* @__PURE__ */ h("section", {
163
+ return /* @__PURE__ */ g("section", {
164
164
  "data-testid": "custom-widget-proposal-panel",
165
165
  "aria-label": O("bigconsole.widget.customWidget.panel.ariaLabel", "AI custom widget builder"),
166
166
  className: "flex h-full w-full min-w-0 flex-col overflow-y-auto p-4",
167
- children: /* @__PURE__ */ g(t, { children: [/* @__PURE__ */ h(e, {
167
+ children: /* @__PURE__ */ _(t, { children: [/* @__PURE__ */ g(e, {
168
168
  title: O("bigconsole.widget.customWidget.prompt.label", "Describe the widget you need"),
169
169
  description: "Plain English. The assistant proposes a typed widget you can edit before anything is saved.",
170
- icon: /* @__PURE__ */ h(ae, {
170
+ icon: /* @__PURE__ */ g(ae, {
171
171
  className: "size-4",
172
172
  "aria-hidden": "true"
173
173
  }),
174
- actions: /* @__PURE__ */ g("div", {
174
+ actions: /* @__PURE__ */ _("div", {
175
175
  className: "flex flex-wrap gap-2",
176
- children: [/* @__PURE__ */ g(f, {
176
+ children: [/* @__PURE__ */ _(f, {
177
177
  "data-testid": "custom-widget-generate",
178
178
  onClick: ue,
179
179
  disabled: !A.trim() || N,
180
- children: [N ? /* @__PURE__ */ h(d, {
180
+ children: [N ? /* @__PURE__ */ g(d, {
181
181
  className: "mr-2 size-4 animate-spin",
182
182
  "aria-hidden": "true"
183
183
  }) : null, j ? O("bigconsole.widget.customWidget.action.regenerate", "Regenerate") : O("bigconsole.widget.customWidget.action.generateProposal", "Generate proposal")]
184
- }), j ? /* @__PURE__ */ g(f, {
184
+ }), j ? /* @__PURE__ */ _(f, {
185
185
  variant: "outline",
186
186
  "data-testid": "custom-widget-reject",
187
187
  onClick: de,
188
- children: [/* @__PURE__ */ h(oe, {
188
+ children: [/* @__PURE__ */ g(oe, {
189
189
  className: "mr-1 size-4",
190
190
  "aria-hidden": "true"
191
191
  }), O("bigconsole.widget.customWidget.action.reject", "Reject")]
192
192
  }) : null]
193
193
  }),
194
- children: /* @__PURE__ */ g("div", {
194
+ children: /* @__PURE__ */ _("div", {
195
195
  className: "flex flex-col gap-2",
196
- children: [/* @__PURE__ */ h(m, {
196
+ children: [/* @__PURE__ */ g(m, {
197
197
  htmlFor: "custom-widget-prompt",
198
198
  children: "What should it show?"
199
- }), /* @__PURE__ */ h(se, {
199
+ }), /* @__PURE__ */ g(se, {
200
200
  id: "custom-widget-prompt",
201
201
  "data-testid": "custom-widget-prompt",
202
202
  value: A,
@@ -205,95 +205,95 @@ var C = a(function({ sinks: a, parsers: C, resolvePreviewData: w, onApprove: T,
205
205
  rows: 3
206
206
  })]
207
207
  })
208
- }), j ? /* @__PURE__ */ g(ce, { children: [
209
- /* @__PURE__ */ g(e, {
208
+ }), j ? /* @__PURE__ */ _(h, { children: [
209
+ /* @__PURE__ */ _(e, {
210
210
  emphasis: "primary",
211
211
  tone: y[Y],
212
212
  title: k[Y].title,
213
213
  description: "Rendered by the real widget renderer against the real parser output — what you approve is what you get.",
214
- meta: /* @__PURE__ */ h("span", {
214
+ meta: /* @__PURE__ */ g("span", {
215
215
  "data-testid": "custom-widget-preview-state",
216
216
  "data-state": Y,
217
217
  className: "rounded-pill bg-bg-sunken px-2 py-0.5 text-xs text-text-secondary",
218
218
  children: Y
219
219
  }),
220
- children: [/* @__PURE__ */ h("div", {
220
+ children: [/* @__PURE__ */ g("div", {
221
221
  "data-testid": "custom-widget-preview",
222
222
  className: "min-h-[220px] min-w-0 rounded-card border border-[var(--color-border-subtle)] bg-bg-surface p-2",
223
- children: Y === "loading" ? /* @__PURE__ */ g("div", {
223
+ children: Y === "loading" ? /* @__PURE__ */ _("div", {
224
224
  className: "flex h-[200px] items-center justify-center text-text-secondary",
225
- children: [/* @__PURE__ */ h(d, {
225
+ children: [/* @__PURE__ */ g(d, {
226
226
  className: "mr-2 size-4 animate-spin",
227
227
  "aria-hidden": "true"
228
- }), /* @__PURE__ */ h("span", {
228
+ }), /* @__PURE__ */ g("span", {
229
229
  className: "text-sm",
230
230
  children: k.loading.body
231
231
  })]
232
- }) : Y === "valid" ? /* @__PURE__ */ h("div", {
232
+ }) : Y === "valid" ? /* @__PURE__ */ g("div", {
233
233
  className: "h-[200px]",
234
- children: /* @__PURE__ */ h(n, {
234
+ children: /* @__PURE__ */ g(n, {
235
235
  widgetId: `preview-${j.id}`,
236
- children: /* @__PURE__ */ h(ee, {
236
+ children: /* @__PURE__ */ g(ee, {
237
237
  widget: b(j, O("bigconsole.widget.customWidget.untitledWidget", "Untitled widget")),
238
238
  data: z
239
239
  })
240
240
  })
241
- }) : /* @__PURE__ */ g("div", {
241
+ }) : /* @__PURE__ */ _("div", {
242
242
  className: "flex h-[200px] flex-col items-center justify-center gap-2 px-4 text-center",
243
243
  children: [
244
- /* @__PURE__ */ h(l, {
244
+ /* @__PURE__ */ g(l, {
245
245
  className: "size-5 text-[var(--color-status-warning-text)]",
246
246
  "aria-hidden": "true"
247
247
  }),
248
- /* @__PURE__ */ h("p", {
248
+ /* @__PURE__ */ g("p", {
249
249
  className: "text-sm text-text-secondary",
250
250
  children: k[Y].body
251
251
  }),
252
- Y === "access-error" ? /* @__PURE__ */ g(f, {
252
+ Y === "access-error" ? /* @__PURE__ */ _(f, {
253
253
  size: "sm",
254
254
  variant: "outline",
255
255
  onClick: () => X((e) => ({ ...e })),
256
- children: [/* @__PURE__ */ h(ie, {
256
+ children: [/* @__PURE__ */ g(ie, {
257
257
  className: "mr-1 size-3.5",
258
258
  "aria-hidden": "true"
259
259
  }), O("bigconsole.widget.customWidget.action.retry", "Retry")]
260
260
  }) : null
261
261
  ]
262
262
  })
263
- }), j.rationale ? /* @__PURE__ */ h("p", {
263
+ }), j.rationale ? /* @__PURE__ */ g("p", {
264
264
  className: "mt-2 text-xs text-text-secondary",
265
265
  "data-testid": "custom-widget-rationale",
266
266
  children: j.rationale
267
267
  }) : null]
268
268
  }),
269
- Q.length > 0 || $.length > 0 ? /* @__PURE__ */ h(e, {
269
+ Q.length > 0 || $.length > 0 ? /* @__PURE__ */ g(e, {
270
270
  tone: Q.length > 0 ? "danger" : "warning",
271
271
  padding: "compact",
272
272
  headingLevel: 3,
273
273
  title: Q.length > 0 ? `${Q.length} ${Q.length === 1 ? "problem" : "problems"} to fix before approving` : `${$.length} ${$.length === 1 ? "warning" : "warnings"}`,
274
- children: /* @__PURE__ */ h("ul", {
274
+ children: /* @__PURE__ */ g("ul", {
275
275
  "data-testid": "custom-widget-issues",
276
276
  className: "flex min-w-0 flex-col gap-1",
277
- children: [...Q, ...$].map((e) => /* @__PURE__ */ h("li", {
277
+ children: [...Q, ...$].map((e) => /* @__PURE__ */ g("li", {
278
278
  "data-severity": e.severity,
279
279
  className: `break-words text-xs ${e.severity === "error" ? "text-[var(--color-status-error-text)]" : "text-[var(--color-status-warning-text)]"}`,
280
280
  children: e.message
281
281
  }, `${e.code}-${e.path ?? ""}`))
282
282
  })
283
283
  }) : null,
284
- /* @__PURE__ */ h(e, {
284
+ /* @__PURE__ */ g(e, {
285
285
  title: O("bigconsole.widget.customWidget.proposal.heading", "Proposal"),
286
286
  description: "Every field is editable — the assistant proposes, you dispose.",
287
287
  "data-testid": "custom-widget-proposal-fields",
288
- children: /* @__PURE__ */ g("div", {
288
+ children: /* @__PURE__ */ _("div", {
289
289
  className: "flex flex-col gap-3",
290
290
  children: [
291
- /* @__PURE__ */ g("div", {
291
+ /* @__PURE__ */ _("div", {
292
292
  className: "flex flex-col gap-1",
293
- children: [/* @__PURE__ */ h(m, {
293
+ children: [/* @__PURE__ */ g(m, {
294
294
  htmlFor: "custom-widget-title",
295
295
  children: O("bigconsole.widget.customWidget.field.title", "Title")
296
- }), /* @__PURE__ */ h(p, {
296
+ }), /* @__PURE__ */ g(p, {
297
297
  id: "custom-widget-title",
298
298
  "data-testid": "custom-widget-title",
299
299
  className: v,
@@ -307,12 +307,12 @@ var C = a(function({ sinks: a, parsers: C, resolvePreviewData: w, onApprove: T,
307
307
  }))
308
308
  })]
309
309
  }),
310
- /* @__PURE__ */ g("div", {
310
+ /* @__PURE__ */ _("div", {
311
311
  className: "flex flex-col gap-1",
312
- children: [/* @__PURE__ */ h(m, {
312
+ children: [/* @__PURE__ */ g(m, {
313
313
  htmlFor: "custom-widget-type",
314
314
  children: O("bigconsole.widget.customWidget.field.widgetType", "Widget type")
315
- }), /* @__PURE__ */ h("select", {
315
+ }), /* @__PURE__ */ g("select", {
316
316
  id: "custom-widget-type",
317
317
  "data-testid": "custom-widget-type",
318
318
  value: j.widgetType,
@@ -321,18 +321,18 @@ var C = a(function({ sinks: a, parsers: C, resolvePreviewData: w, onApprove: T,
321
321
  widgetType: e.target.value
322
322
  })),
323
323
  className: "w-full rounded-md border border-[var(--color-border-default)] bg-bg-surface px-3 py-2 text-sm text-text-primary focus:outline-none focus:ring-2 focus:ring-action-primary-bg",
324
- children: te.map((e) => /* @__PURE__ */ h("option", {
324
+ children: te.map((e) => /* @__PURE__ */ g("option", {
325
325
  value: e,
326
326
  children: e
327
327
  }, e))
328
328
  })]
329
329
  }),
330
- /* @__PURE__ */ g("div", {
330
+ /* @__PURE__ */ _("div", {
331
331
  className: "flex flex-col gap-1",
332
- children: [/* @__PURE__ */ h(m, {
332
+ children: [/* @__PURE__ */ g(m, {
333
333
  htmlFor: "custom-widget-sink",
334
334
  children: O("bigconsole.widget.customWidget.field.dataSink", "DataSink")
335
- }), /* @__PURE__ */ g("select", {
335
+ }), /* @__PURE__ */ _("select", {
336
336
  id: "custom-widget-sink",
337
337
  "data-testid": "custom-widget-sink",
338
338
  value: j.binding.dataSinkId,
@@ -348,21 +348,21 @@ var C = a(function({ sinks: a, parsers: C, resolvePreviewData: w, onApprove: T,
348
348
  }));
349
349
  },
350
350
  className: "w-full rounded-md border border-[var(--color-border-default)] bg-bg-surface px-3 py-2 text-sm text-text-primary focus:outline-none focus:ring-2 focus:ring-action-primary-bg",
351
- children: [/* @__PURE__ */ h("option", {
351
+ children: [/* @__PURE__ */ g("option", {
352
352
  value: "",
353
353
  children: O("bigconsole.widget.customWidget.field.selectDataSink", "Select a DataSink…")
354
- }), a.map((e) => /* @__PURE__ */ h("option", {
354
+ }), a.map((e) => /* @__PURE__ */ g("option", {
355
355
  value: e.id,
356
356
  children: e.name || e.key
357
357
  }, e.id))]
358
358
  })]
359
359
  }),
360
- /* @__PURE__ */ g("div", {
360
+ /* @__PURE__ */ _("div", {
361
361
  className: "flex flex-col gap-1",
362
- children: [/* @__PURE__ */ h(m, {
362
+ children: [/* @__PURE__ */ g(m, {
363
363
  htmlFor: "custom-widget-parser",
364
364
  children: O("bigconsole.widget.customWidget.field.parser", "Parser")
365
- }), /* @__PURE__ */ g("select", {
365
+ }), /* @__PURE__ */ _("select", {
366
366
  id: "custom-widget-parser",
367
367
  "data-testid": "custom-widget-parser",
368
368
  value: j.binding.parserId ?? "",
@@ -378,29 +378,29 @@ var C = a(function({ sinks: a, parsers: C, resolvePreviewData: w, onApprove: T,
378
378
  }));
379
379
  },
380
380
  className: "w-full rounded-md border border-[var(--color-border-default)] bg-bg-surface px-3 py-2 text-sm text-text-primary focus:outline-none focus:ring-2 focus:ring-action-primary-bg",
381
- children: [/* @__PURE__ */ h("option", {
381
+ children: [/* @__PURE__ */ g("option", {
382
382
  value: "",
383
383
  children: O("bigconsole.widget.customWidget.field.noParser", "No parser")
384
- }), C.map((e) => /* @__PURE__ */ h("option", {
384
+ }), C.map((e) => /* @__PURE__ */ g("option", {
385
385
  value: e.id,
386
386
  children: e.name
387
387
  }, e.id))]
388
388
  })]
389
389
  }),
390
- Object.keys(j.fieldMapping).length > 0 ? /* @__PURE__ */ g("fieldset", {
390
+ Object.keys(j.fieldMapping).length > 0 ? /* @__PURE__ */ _("fieldset", {
391
391
  className: "flex flex-col gap-2",
392
392
  "data-testid": "custom-widget-field-mapping",
393
393
  children: [
394
- /* @__PURE__ */ h("legend", {
394
+ /* @__PURE__ */ g("legend", {
395
395
  className: "text-sm font-medium text-text-primary",
396
396
  children: O("bigconsole.widget.customWidget.fieldMapping.legend", "Field mapping")
397
397
  }),
398
- Object.entries(j.fieldMapping).map(([e, t]) => /* @__PURE__ */ g("div", {
398
+ Object.entries(j.fieldMapping).map(([e, t]) => /* @__PURE__ */ _("div", {
399
399
  className: "flex flex-col gap-1",
400
- children: [/* @__PURE__ */ h(m, {
400
+ children: [/* @__PURE__ */ g(m, {
401
401
  htmlFor: `custom-widget-field-${e}`,
402
402
  children: e
403
- }), /* @__PURE__ */ h(p, {
403
+ }), /* @__PURE__ */ g(p, {
404
404
  id: `custom-widget-field-${e}`,
405
405
  "data-testid": `custom-widget-field-${e}`,
406
406
  value: t,
@@ -413,7 +413,7 @@ var C = a(function({ sinks: a, parsers: C, resolvePreviewData: w, onApprove: T,
413
413
  }))
414
414
  })]
415
415
  }, e)),
416
- q?.length ? /* @__PURE__ */ h("p", {
416
+ q?.length ? /* @__PURE__ */ g("p", {
417
417
  className: "break-words text-xs text-text-secondary",
418
418
  children: O("bigconsole.widget.customWidget.fieldMapping.available", "Available: {{fields}}", { fields: q.join(", ") })
419
419
  }) : null
@@ -422,44 +422,44 @@ var C = a(function({ sinks: a, parsers: C, resolvePreviewData: w, onApprove: T,
422
422
  ]
423
423
  })
424
424
  }),
425
- L ? /* @__PURE__ */ h(e, {
425
+ L ? /* @__PURE__ */ g(e, {
426
426
  tone: "danger",
427
427
  padding: "compact",
428
428
  headingLevel: 3,
429
429
  title: "Could not add the widget",
430
- children: /* @__PURE__ */ h("p", {
430
+ children: /* @__PURE__ */ g("p", {
431
431
  role: "alert",
432
432
  className: "break-words text-xs text-[var(--color-status-error-text)]",
433
433
  "data-testid": "custom-widget-approve-error",
434
434
  children: L
435
435
  })
436
436
  }) : null,
437
- /* @__PURE__ */ g("div", {
437
+ /* @__PURE__ */ _("div", {
438
438
  className: "flex flex-wrap gap-2 border-t border-[var(--color-border-subtle)] pt-3",
439
- children: [/* @__PURE__ */ g(f, {
439
+ children: [/* @__PURE__ */ _(f, {
440
440
  "data-testid": "custom-widget-approve",
441
441
  onClick: Z,
442
442
  disabled: !J.canApprove || F,
443
- children: [F ? /* @__PURE__ */ h(d, {
443
+ children: [F ? /* @__PURE__ */ g(d, {
444
444
  className: "mr-2 size-4 animate-spin",
445
445
  "aria-hidden": "true"
446
- }) : /* @__PURE__ */ h(u, {
446
+ }) : /* @__PURE__ */ g(u, {
447
447
  className: "mr-1 size-4",
448
448
  "aria-hidden": "true"
449
449
  }), O("bigconsole.widget.customWidget.action.approveAndAdd", "Approve & add to dashboard")]
450
- }), /* @__PURE__ */ h(f, {
450
+ }), /* @__PURE__ */ g(f, {
451
451
  variant: "outline",
452
452
  onClick: E,
453
453
  "data-testid": "custom-widget-cancel",
454
454
  children: O("bigconsole.widget.customWidget.action.cancel", "Cancel")
455
455
  })]
456
456
  })
457
- ] }) : /* @__PURE__ */ h(e, {
457
+ ] }) : /* @__PURE__ */ g(e, {
458
458
  emphasis: "muted",
459
459
  padding: "compact",
460
460
  title: "No proposal yet",
461
461
  headingLevel: 3,
462
- children: /* @__PURE__ */ h("p", {
462
+ children: /* @__PURE__ */ g("p", {
463
463
  className: "text-sm text-text-secondary",
464
464
  "data-testid": "custom-widget-empty",
465
465
  children: O("bigconsole.widget.customWidget.empty", "No proposal yet. Describe a widget and the assistant will propose one for you to review — nothing is added to the dashboard until you approve it.")
@@ -1 +1 @@
1
- {"version":3,"file":"CustomWidgetProposalPanel.js","names":[],"sources":["../../../src/bigconsole/customWidget/CustomWidgetProposalPanel.tsx"],"sourcesContent":["/**\n * Custom-widget proposal review panel (BOFF-5532).\n *\n * The authoring surface for the AI custom-widget builder: prompt in, typed\n * proposal out, reviewed and edited by a human, then approved into a real\n * widget. Nothing is written to the dashboard until the reviewer approves.\n *\n * Deliberate choices:\n * - The preview renders through the *real* widget renderer with the *real*\n * parser output, so what the reviewer approves is what they will get.\n * - Bindings are chosen from the user's permitted sinks and parsers only, and\n * the panel says plainly when a proposal points somewhere they cannot reach.\n * - Every proposal field is editable before approval — the AI proposes, the\n * reviewer disposes.\n */\n\nimport { type FC, memo, useCallback, useEffect, useMemo, useState } from 'react';\nimport { Button, Input, Label, Textarea } from '@burdenoff/fe-libs/ui';\nimport { useI18n } from '@burdenoff/fe-libs/shared/providers/shell/I18nProvider';\nimport { AlertTriangle, Check, Loader2, RefreshCw, Sparkles, X } from 'lucide-react';\n\nimport type { Widget } from '../types';\nimport { BigConsoleRenderer } from '../renderers/adapters/BigConsoleRenderer';\nimport { WidgetErrorBoundary } from '../components/widgets/WidgetErrorBoundary';\nimport {\n PROPOSABLE_WIDGET_TYPES,\n derivePreviewState,\n validateProposal,\n type CustomWidgetProposal,\n type ProposableWidgetType,\n type ProposalPreviewState,\n} from './proposalContract';\nimport {\n localProposalSource,\n type CustomWidgetProposalSource,\n type ProposalCandidateParser,\n type ProposalCandidateSink,\n} from './proposalSource';\nimport { Section, SectionStack, type SectionTone } from '../components/common';\n\n// ============================================================================\n// Types\n// ============================================================================\n\nexport interface CustomWidgetProposalPanelProps {\n /** Sinks the user may bind to (already RBAC-filtered by the API). */\n sinks: readonly ProposalCandidateSink[];\n /** Parsers the user may bind to. */\n parsers: readonly ProposalCandidateParser[];\n /**\n * Resolves the rows a binding would produce, for the preview. Should reject\n * (or throw) when the user cannot read the binding, so the panel can show the\n * access-failure state.\n */\n resolvePreviewData: (proposal: CustomWidgetProposal) => Promise<unknown>;\n /** Persists an approved proposal. Resolves once the widget exists. */\n onApprove: (proposal: CustomWidgetProposal) => Promise<void>;\n /** Closes the panel without persisting anything. */\n onClose: () => void;\n /** Swap for the GraphQL-backed source once BOFF-3021's backend half lands. */\n source?: CustomWidgetProposalSource;\n}\n\nconst FIELD_CLASSES = 'w-full';\n\n/**\n * The preview is the thing being judged, so its tone tracks the binding state.\n * The copy itself is translated per render (`previewCopy` below); only the tone\n * ladder is static.\n */\nconst PREVIEW_TONES: Record<ProposalPreviewState, SectionTone> = {\n loading: 'neutral',\n valid: 'neutral',\n 'no-data': 'warning',\n 'field-mismatch': 'warning',\n 'access-error': 'danger',\n};\n\n// ============================================================================\n// Helpers\n// ============================================================================\n\n/** Builds a throwaway Widget so the preview can use the production renderer. */\nfunction toDraftWidget(proposal: CustomWidgetProposal, untitledLabel: string): Widget {\n const now = new Date(0).toISOString();\n return {\n id: `preview-${proposal.id}`,\n pageId: 'preview',\n type: proposal.widgetType,\n title: proposal.presentation.title || untitledLabel,\n description: proposal.presentation.description,\n dataSinkId: proposal.binding.dataSinkId || null,\n parserId: proposal.binding.parserId || null,\n config: { ...proposal.presentation.config, ...proposal.fieldMapping },\n position: { x: 0, y: 0, width: 6, height: 4 },\n positionX: 0,\n positionY: 0,\n positionWidth: 6,\n positionHeight: 4,\n createdAt: now,\n updatedAt: now,\n } as Widget;\n}\n\n/** Field names present in the resolved preview payload, for mismatch checks. */\nfunction extractFields(data: unknown): string[] | undefined {\n if (Array.isArray(data)) {\n const first = data.find((row) => row && typeof row === 'object');\n return first ? Object.keys(first as Record<string, unknown>) : [];\n }\n if (data && typeof data === 'object') return Object.keys(data as Record<string, unknown>);\n return undefined;\n}\n\nfunction countRows(data: unknown): number {\n if (Array.isArray(data)) return data.length;\n if (data && typeof data === 'object') {\n const cells = (data as { cells?: unknown }).cells;\n if (Array.isArray(cells)) return cells.length;\n return Object.keys(data as Record<string, unknown>).length;\n }\n return 0;\n}\n\n// ============================================================================\n// Component\n// ============================================================================\n\nexport const CustomWidgetProposalPanel: FC<CustomWidgetProposalPanelProps> = memo(function CustomWidgetProposalPanel({\n sinks,\n parsers,\n resolvePreviewData,\n onApprove,\n onClose,\n source = localProposalSource,\n}) {\n const { t } = useI18n();\n\n const previewCopy = useMemo<Record<ProposalPreviewState, { title: string; body: string }>>(\n () => ({\n loading: {\n title: t('bigconsole.widget.customWidget.preview.loading.title', 'Building preview…'),\n body: t(\n 'bigconsole.widget.customWidget.preview.loading.body',\n 'Running the parser against the selected DataSink.'\n ),\n },\n valid: {\n title: t('bigconsole.widget.customWidget.preview.valid.title', 'Preview'),\n body: '',\n },\n 'no-data': {\n title: t('bigconsole.widget.customWidget.preview.noData.title', 'No data returned'),\n body: t(\n 'bigconsole.widget.customWidget.preview.noData.body',\n 'The binding is valid but the parser returned no rows. The widget will render empty until the sink has data.'\n ),\n },\n 'field-mismatch': {\n title: t('bigconsole.widget.customWidget.preview.fieldMismatch.title', 'Field mismatch'),\n body: t(\n 'bigconsole.widget.customWidget.preview.fieldMismatch.body',\n 'Some mapped fields are not present in the parser output. Fix the mapping below before approving.'\n ),\n },\n 'access-error': {\n title: t('bigconsole.widget.customWidget.preview.accessError.title', 'Cannot read this data'),\n body: t(\n 'bigconsole.widget.customWidget.preview.accessError.body',\n 'You do not have access to the proposed DataSink or parser in this workspace.'\n ),\n },\n }),\n [t]\n );\n\n const [prompt, setPrompt] = useState('');\n const [proposal, setProposal] = useState<CustomWidgetProposal | null>(null);\n const [isProposing, setIsProposing] = useState(false);\n const [isApproving, setIsApproving] = useState(false);\n const [approveError, setApproveError] = useState<string | null>(null);\n\n const [previewData, setPreviewData] = useState<unknown>(undefined);\n const [previewLoading, setPreviewLoading] = useState(false);\n const [previewAccessError, setPreviewAccessError] = useState(false);\n\n const permittedDataSinkIds = useMemo(() => sinks.map((sink) => sink.id), [sinks]);\n const permittedParserIds = useMemo(() => parsers.map((parser) => parser.id), [parsers]);\n const availableFields = useMemo(() => extractFields(previewData), [previewData]);\n\n const validation = useMemo(\n () =>\n proposal\n ? validateProposal(proposal, { permittedDataSinkIds, permittedParserIds, availableFields })\n : { issues: [], canApprove: false },\n [proposal, permittedDataSinkIds, permittedParserIds, availableFields]\n );\n\n const previewState = useMemo(\n () =>\n derivePreviewState(validation, {\n loading: previewLoading,\n accessError: previewAccessError,\n rowCount: countRows(previewData),\n }),\n [validation, previewLoading, previewAccessError, previewData]\n );\n\n // Re-resolve the preview whenever the binding changes.\n const bindingKey = proposal\n ? `${proposal.binding.dataSinkId}|${proposal.binding.parserId ?? ''}|${proposal.binding.dataPath ?? ''}`\n : '';\n\n useEffect(() => {\n if (!proposal || !proposal.binding.dataSinkId) {\n setPreviewData(undefined);\n return;\n }\n let cancelled = false;\n setPreviewLoading(true);\n setPreviewAccessError(false);\n\n resolvePreviewData(proposal)\n .then((data) => {\n if (cancelled) return;\n setPreviewData(data);\n })\n .catch(() => {\n if (cancelled) return;\n setPreviewData(undefined);\n setPreviewAccessError(true);\n })\n .finally(() => {\n if (!cancelled) setPreviewLoading(false);\n });\n\n return () => {\n cancelled = true;\n };\n // `bindingKey` captures every input that changes the resolved data.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [bindingKey, resolvePreviewData]);\n\n const handlePropose = useCallback(async () => {\n if (!prompt.trim()) return;\n setIsProposing(true);\n setApproveError(null);\n try {\n const next = await source.propose({ prompt: prompt.trim(), sinks, parsers });\n setProposal(next);\n } finally {\n setIsProposing(false);\n }\n }, [prompt, source, sinks, parsers]);\n\n const patchProposal = useCallback((patch: (current: CustomWidgetProposal) => CustomWidgetProposal) => {\n setProposal((current) => (current ? patch(current) : current));\n }, []);\n\n const handleReject = useCallback(() => {\n setProposal(null);\n setPreviewData(undefined);\n setPreviewAccessError(false);\n setApproveError(null);\n }, []);\n\n const handleApprove = useCallback(async () => {\n if (!proposal || !validation.canApprove) return;\n setIsApproving(true);\n setApproveError(null);\n try {\n await onApprove(proposal);\n onClose();\n } catch (error) {\n setApproveError(\n error instanceof Error\n ? error.message\n : t('bigconsole.widget.customWidget.error.couldNotSaveWidget', 'Could not save the widget.')\n );\n } finally {\n setIsApproving(false);\n }\n }, [proposal, validation.canApprove, onApprove, onClose, t]);\n\n const errors = validation.issues.filter((issue) => issue.severity === 'error');\n const warnings = validation.issues.filter((issue) => issue.severity === 'warning');\n\n return (\n <section\n data-testid=\"custom-widget-proposal-panel\"\n aria-label={t('bigconsole.widget.customWidget.panel.ariaLabel', 'AI custom widget builder')}\n className=\"flex h-full w-full min-w-0 flex-col overflow-y-auto p-4\"\n >\n {/*\n Five stages — prompt → preview → validation → editable proposal →\n approval — used to be five bare divs separated by one uniform gap. Each\n is now a labelled section, and the preview (the thing the reviewer is\n actually judging) is the panel's single `primary` block (BOFF-5636).\n */}\n <SectionStack>\n {/* ---------- Prompt ---------- */}\n <Section\n title={t('bigconsole.widget.customWidget.prompt.label', 'Describe the widget you need')}\n description=\"Plain English. The assistant proposes a typed widget you can edit before anything is saved.\"\n icon={<Sparkles className=\"size-4\" aria-hidden=\"true\" />}\n actions={\n <div className=\"flex flex-wrap gap-2\">\n <Button\n data-testid=\"custom-widget-generate\"\n onClick={handlePropose}\n disabled={!prompt.trim() || isProposing}\n >\n {isProposing ? <Loader2 className=\"mr-2 size-4 animate-spin\" aria-hidden=\"true\" /> : null}\n {proposal\n ? t('bigconsole.widget.customWidget.action.regenerate', 'Regenerate')\n : t('bigconsole.widget.customWidget.action.generateProposal', 'Generate proposal')}\n </Button>\n {proposal ? (\n <Button variant=\"outline\" data-testid=\"custom-widget-reject\" onClick={handleReject}>\n <X className=\"mr-1 size-4\" aria-hidden=\"true\" />\n {t('bigconsole.widget.customWidget.action.reject', 'Reject')}\n </Button>\n ) : null}\n </div>\n }\n >\n <div className=\"flex flex-col gap-2\">\n <Label htmlFor=\"custom-widget-prompt\">What should it show?</Label>\n <Textarea\n id=\"custom-widget-prompt\"\n data-testid=\"custom-widget-prompt\"\n value={prompt}\n onChange={(event) => setPrompt(event.target.value)}\n placeholder={t(\n 'bigconsole.widget.customWidget.prompt.placeholder',\n 'e.g. a funnel of signup conversion stages'\n )}\n rows={3}\n />\n </div>\n </Section>\n\n {!proposal ? (\n <Section emphasis=\"muted\" padding=\"compact\" title=\"No proposal yet\" headingLevel={3}>\n <p className=\"text-sm text-text-secondary\" data-testid=\"custom-widget-empty\">\n {t(\n 'bigconsole.widget.customWidget.empty',\n 'No proposal yet. Describe a widget and the assistant will propose one for you to review — nothing is added to the dashboard until you approve it.'\n )}\n </p>\n </Section>\n ) : (\n <>\n {/* ---------- Preview ---------- */}\n <Section\n emphasis=\"primary\"\n tone={PREVIEW_TONES[previewState]}\n title={previewCopy[previewState].title}\n description=\"Rendered by the real widget renderer against the real parser output — what you approve is what you get.\"\n meta={\n <span\n data-testid=\"custom-widget-preview-state\"\n data-state={previewState}\n className=\"rounded-pill bg-bg-sunken px-2 py-0.5 text-xs text-text-secondary\"\n >\n {previewState}\n </span>\n }\n >\n <div\n data-testid=\"custom-widget-preview\"\n className=\"min-h-[220px] min-w-0 rounded-card border border-[var(--color-border-subtle)] bg-bg-surface p-2\"\n >\n {previewState === 'loading' ? (\n <div className=\"flex h-[200px] items-center justify-center text-text-secondary\">\n <Loader2 className=\"mr-2 size-4 animate-spin\" aria-hidden=\"true\" />\n <span className=\"text-sm\">{previewCopy.loading.body}</span>\n </div>\n ) : previewState === 'valid' ? (\n <div className=\"h-[200px]\">\n <WidgetErrorBoundary widgetId={`preview-${proposal.id}`}>\n <BigConsoleRenderer\n widget={toDraftWidget(\n proposal,\n t('bigconsole.widget.customWidget.untitledWidget', 'Untitled widget')\n )}\n data={previewData as Record<string, unknown>}\n />\n </WidgetErrorBoundary>\n </div>\n ) : (\n <div className=\"flex h-[200px] flex-col items-center justify-center gap-2 px-4 text-center\">\n <AlertTriangle className=\"size-5 text-[var(--color-status-warning-text)]\" aria-hidden=\"true\" />\n <p className=\"text-sm text-text-secondary\">{previewCopy[previewState].body}</p>\n {previewState === 'access-error' ? (\n <Button size=\"sm\" variant=\"outline\" onClick={() => patchProposal((current) => ({ ...current }))}>\n <RefreshCw className=\"mr-1 size-3.5\" aria-hidden=\"true\" />\n {t('bigconsole.widget.customWidget.action.retry', 'Retry')}\n </Button>\n ) : null}\n </div>\n )}\n </div>\n\n {proposal.rationale ? (\n <p className=\"mt-2 text-xs text-text-secondary\" data-testid=\"custom-widget-rationale\">\n {proposal.rationale}\n </p>\n ) : null}\n </Section>\n\n {/* ---------- Validation ---------- */}\n {errors.length > 0 || warnings.length > 0 ? (\n <Section\n tone={errors.length > 0 ? 'danger' : 'warning'}\n padding=\"compact\"\n headingLevel={3}\n title={\n errors.length > 0\n ? `${errors.length} ${errors.length === 1 ? 'problem' : 'problems'} to fix before approving`\n : `${warnings.length} ${warnings.length === 1 ? 'warning' : 'warnings'}`\n }\n >\n <ul data-testid=\"custom-widget-issues\" className=\"flex min-w-0 flex-col gap-1\">\n {[...errors, ...warnings].map((issue) => (\n <li\n key={`${issue.code}-${issue.path ?? ''}`}\n data-severity={issue.severity}\n className={`break-words text-xs ${\n issue.severity === 'error'\n ? 'text-[var(--color-status-error-text)]'\n : 'text-[var(--color-status-warning-text)]'\n }`}\n >\n {issue.message}\n </li>\n ))}\n </ul>\n </Section>\n ) : null}\n\n {/* ---------- Editable proposal ---------- */}\n <Section\n title={t('bigconsole.widget.customWidget.proposal.heading', 'Proposal')}\n description=\"Every field is editable — the assistant proposes, you dispose.\"\n data-testid=\"custom-widget-proposal-fields\"\n >\n <div className=\"flex flex-col gap-3\">\n <div className=\"flex flex-col gap-1\">\n <Label htmlFor=\"custom-widget-title\">\n {t('bigconsole.widget.customWidget.field.title', 'Title')}\n </Label>\n <Input\n id=\"custom-widget-title\"\n data-testid=\"custom-widget-title\"\n className={FIELD_CLASSES}\n value={proposal.presentation.title}\n onChange={(event) =>\n patchProposal((current) => ({\n ...current,\n presentation: { ...current.presentation, title: event.target.value },\n }))\n }\n />\n </div>\n\n <div className=\"flex flex-col gap-1\">\n <Label htmlFor=\"custom-widget-type\">\n {t('bigconsole.widget.customWidget.field.widgetType', 'Widget type')}\n </Label>\n <select\n id=\"custom-widget-type\"\n data-testid=\"custom-widget-type\"\n value={proposal.widgetType}\n onChange={(event) =>\n patchProposal((current) => ({\n ...current,\n widgetType: event.target.value as ProposableWidgetType,\n }))\n }\n className=\"w-full rounded-md border border-[var(--color-border-default)] bg-bg-surface px-3 py-2 text-sm text-text-primary focus:outline-none focus:ring-2 focus:ring-action-primary-bg\"\n >\n {PROPOSABLE_WIDGET_TYPES.map((type) => (\n <option key={type} value={type}>\n {type}\n </option>\n ))}\n </select>\n </div>\n\n <div className=\"flex flex-col gap-1\">\n <Label htmlFor=\"custom-widget-sink\">\n {t('bigconsole.widget.customWidget.field.dataSink', 'DataSink')}\n </Label>\n <select\n id=\"custom-widget-sink\"\n data-testid=\"custom-widget-sink\"\n value={proposal.binding.dataSinkId}\n onChange={(event) => {\n const sink = sinks.find((candidate) => candidate.id === event.target.value);\n patchProposal((current) => ({\n ...current,\n binding: { ...current.binding, dataSinkId: event.target.value, dataSinkKey: sink?.key },\n }));\n }}\n className=\"w-full rounded-md border border-[var(--color-border-default)] bg-bg-surface px-3 py-2 text-sm text-text-primary focus:outline-none focus:ring-2 focus:ring-action-primary-bg\"\n >\n <option value=\"\">\n {t('bigconsole.widget.customWidget.field.selectDataSink', 'Select a DataSink…')}\n </option>\n {sinks.map((sink) => (\n <option key={sink.id} value={sink.id}>\n {sink.name || sink.key}\n </option>\n ))}\n </select>\n </div>\n\n <div className=\"flex flex-col gap-1\">\n <Label htmlFor=\"custom-widget-parser\">\n {t('bigconsole.widget.customWidget.field.parser', 'Parser')}\n </Label>\n <select\n id=\"custom-widget-parser\"\n data-testid=\"custom-widget-parser\"\n value={proposal.binding.parserId ?? ''}\n onChange={(event) => {\n const parser = parsers.find((candidate) => candidate.id === event.target.value);\n patchProposal((current) => ({\n ...current,\n binding: {\n ...current.binding,\n parserId: event.target.value || undefined,\n parserName: parser?.name,\n },\n }));\n }}\n className=\"w-full rounded-md border border-[var(--color-border-default)] bg-bg-surface px-3 py-2 text-sm text-text-primary focus:outline-none focus:ring-2 focus:ring-action-primary-bg\"\n >\n <option value=\"\">{t('bigconsole.widget.customWidget.field.noParser', 'No parser')}</option>\n {parsers.map((parser) => (\n <option key={parser.id} value={parser.id}>\n {parser.name}\n </option>\n ))}\n </select>\n </div>\n\n {/* Field mapping */}\n {Object.keys(proposal.fieldMapping).length > 0 ? (\n <fieldset className=\"flex flex-col gap-2\" data-testid=\"custom-widget-field-mapping\">\n <legend className=\"text-sm font-medium text-text-primary\">\n {t('bigconsole.widget.customWidget.fieldMapping.legend', 'Field mapping')}\n </legend>\n {Object.entries(proposal.fieldMapping).map(([role, field]) => (\n <div key={role} className=\"flex flex-col gap-1\">\n <Label htmlFor={`custom-widget-field-${role}`}>{role}</Label>\n <Input\n id={`custom-widget-field-${role}`}\n data-testid={`custom-widget-field-${role}`}\n value={field}\n onChange={(event) =>\n patchProposal((current) => ({\n ...current,\n fieldMapping: { ...current.fieldMapping, [role]: event.target.value },\n }))\n }\n />\n </div>\n ))}\n {availableFields?.length ? (\n <p className=\"break-words text-xs text-text-secondary\">\n {t('bigconsole.widget.customWidget.fieldMapping.available', 'Available: {{fields}}', {\n fields: availableFields.join(', '),\n })}\n </p>\n ) : null}\n </fieldset>\n ) : null}\n </div>\n </Section>\n\n {/* ---------- Approval ---------- */}\n {approveError ? (\n <Section tone=\"danger\" padding=\"compact\" headingLevel={3} title=\"Could not add the widget\">\n <p\n role=\"alert\"\n className=\"break-words text-xs text-[var(--color-status-error-text)]\"\n data-testid=\"custom-widget-approve-error\"\n >\n {approveError}\n </p>\n </Section>\n ) : null}\n\n <div className=\"flex flex-wrap gap-2 border-t border-[var(--color-border-subtle)] pt-3\">\n <Button\n data-testid=\"custom-widget-approve\"\n onClick={handleApprove}\n disabled={!validation.canApprove || isApproving}\n >\n {isApproving ? (\n <Loader2 className=\"mr-2 size-4 animate-spin\" aria-hidden=\"true\" />\n ) : (\n <Check className=\"mr-1 size-4\" aria-hidden=\"true\" />\n )}\n {t('bigconsole.widget.customWidget.action.approveAndAdd', 'Approve & add to dashboard')}\n </Button>\n <Button variant=\"outline\" onClick={onClose} data-testid=\"custom-widget-cancel\">\n {t('bigconsole.widget.customWidget.action.cancel', 'Cancel')}\n </Button>\n </div>\n </>\n )}\n </SectionStack>\n </section>\n );\n});\n\nexport default CustomWidgetProposalPanel;\n"],"mappings":"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"}
1
+ {"version":3,"file":"CustomWidgetProposalPanel.js","names":[],"sources":["../../../src/bigconsole/customWidget/CustomWidgetProposalPanel.tsx"],"sourcesContent":["/**\n * Custom-widget proposal review panel (BOFF-5532).\n *\n * The authoring surface for the AI custom-widget builder: prompt in, typed\n * proposal out, reviewed and edited by a human, then approved into a real\n * widget. Nothing is written to the dashboard until the reviewer approves.\n *\n * Deliberate choices:\n * - The preview renders through the *real* widget renderer with the *real*\n * parser output, so what the reviewer approves is what they will get.\n * - Bindings are chosen from the user's permitted sinks and parsers only, and\n * the panel says plainly when a proposal points somewhere they cannot reach.\n * - Every proposal field is editable before approval — the AI proposes, the\n * reviewer disposes.\n */\n\nimport { type FC, memo, useCallback, useEffect, useMemo, useState } from 'react';\nimport { Button, Input, Label, Textarea } from '@burdenoff/fe-libs/ui';\nimport { useI18n } from '@burdenoff/fe-libs/shared/providers/shell/I18nProvider';\nimport { AlertTriangle, Check, Loader2, RefreshCw, Sparkles, X } from 'lucide-react';\n\nimport type { Widget } from '../types';\nimport { BigConsoleRenderer } from '../renderers/adapters/BigConsoleRenderer';\nimport { WidgetErrorBoundary } from '../components/widgets/WidgetErrorBoundary';\nimport {\n PROPOSABLE_WIDGET_TYPES,\n derivePreviewState,\n validateProposal,\n type CustomWidgetProposal,\n type ProposableWidgetType,\n type ProposalPreviewState,\n} from './proposalContract';\nimport {\n localProposalSource,\n type CustomWidgetProposalSource,\n type ProposalCandidateParser,\n type ProposalCandidateSink,\n} from './proposalSource';\nimport { Section, SectionStack, type SectionTone } from '../components/common';\n\n// ============================================================================\n// Types\n// ============================================================================\n\nexport interface CustomWidgetProposalPanelProps {\n /** Sinks the user may bind to (already RBAC-filtered by the API). */\n sinks: readonly ProposalCandidateSink[];\n /** Parsers the user may bind to. */\n parsers: readonly ProposalCandidateParser[];\n /**\n * Resolves the rows a binding would produce, for the preview. Should reject\n * (or throw) when the user cannot read the binding, so the panel can show the\n * access-failure state.\n */\n resolvePreviewData: (proposal: CustomWidgetProposal) => Promise<unknown>;\n /** Persists an approved proposal. Resolves once the widget exists. */\n onApprove: (proposal: CustomWidgetProposal) => Promise<void>;\n /** Closes the panel without persisting anything. */\n onClose: () => void;\n /** Swap for the GraphQL-backed source once BOFF-3021's backend half lands. */\n source?: CustomWidgetProposalSource;\n}\n\nconst FIELD_CLASSES = 'w-full';\n\n/**\n * The preview is the thing being judged, so its tone tracks the binding state.\n * The copy itself is translated per render (`previewCopy` below); only the tone\n * ladder is static.\n */\nconst PREVIEW_TONES: Record<ProposalPreviewState, SectionTone> = {\n loading: 'neutral',\n valid: 'neutral',\n 'no-data': 'warning',\n 'field-mismatch': 'warning',\n 'access-error': 'danger',\n};\n\n// ============================================================================\n// Helpers\n// ============================================================================\n\n/** Builds a throwaway Widget so the preview can use the production renderer. */\nfunction toDraftWidget(proposal: CustomWidgetProposal, untitledLabel: string): Widget {\n const now = new Date(0).toISOString();\n return {\n id: `preview-${proposal.id}`,\n pageId: 'preview',\n type: proposal.widgetType,\n title: proposal.presentation.title || untitledLabel,\n description: proposal.presentation.description,\n dataSinkId: proposal.binding.dataSinkId || null,\n parserId: proposal.binding.parserId || null,\n config: { ...proposal.presentation.config, ...proposal.fieldMapping },\n position: { x: 0, y: 0, width: 6, height: 4 },\n positionX: 0,\n positionY: 0,\n positionWidth: 6,\n positionHeight: 4,\n createdAt: now,\n updatedAt: now,\n } as Widget;\n}\n\n/** Field names present in the resolved preview payload, for mismatch checks. */\nfunction extractFields(data: unknown): string[] | undefined {\n if (Array.isArray(data)) {\n const first = data.find((row) => row && typeof row === 'object');\n return first ? Object.keys(first as Record<string, unknown>) : [];\n }\n if (data && typeof data === 'object') return Object.keys(data as Record<string, unknown>);\n return undefined;\n}\n\nfunction countRows(data: unknown): number {\n if (Array.isArray(data)) return data.length;\n if (data && typeof data === 'object') {\n const cells = (data as { cells?: unknown }).cells;\n if (Array.isArray(cells)) return cells.length;\n return Object.keys(data as Record<string, unknown>).length;\n }\n return 0;\n}\n\n// ============================================================================\n// Component\n// ============================================================================\n\nexport const CustomWidgetProposalPanel: FC<CustomWidgetProposalPanelProps> = memo(function CustomWidgetProposalPanel({\n sinks,\n parsers,\n resolvePreviewData,\n onApprove,\n onClose,\n source = localProposalSource,\n}) {\n const { t } = useI18n();\n\n const previewCopy = useMemo<Record<ProposalPreviewState, { title: string; body: string }>>(\n () => ({\n loading: {\n title: t('bigconsole.widget.customWidget.preview.loading.title', 'Building preview…'),\n body: t(\n 'bigconsole.widget.customWidget.preview.loading.body',\n 'Running the parser against the selected DataSink.'\n ),\n },\n valid: {\n title: t('bigconsole.widget.customWidget.preview.valid.title', 'Preview'),\n body: '',\n },\n 'no-data': {\n title: t('bigconsole.widget.customWidget.preview.noData.title', 'No data returned'),\n body: t(\n 'bigconsole.widget.customWidget.preview.noData.body',\n 'The binding is valid but the parser returned no rows. The widget will render empty until the sink has data.'\n ),\n },\n 'field-mismatch': {\n title: t('bigconsole.widget.customWidget.preview.fieldMismatch.title', 'Field mismatch'),\n body: t(\n 'bigconsole.widget.customWidget.preview.fieldMismatch.body',\n 'Some mapped fields are not present in the parser output. Fix the mapping below before approving.'\n ),\n },\n 'access-error': {\n title: t('bigconsole.widget.customWidget.preview.accessError.title', 'Cannot read this data'),\n body: t(\n 'bigconsole.widget.customWidget.preview.accessError.body',\n 'You do not have access to the proposed DataSink or parser in this workspace.'\n ),\n },\n }),\n [t]\n );\n\n const [prompt, setPrompt] = useState('');\n const [proposal, setProposal] = useState<CustomWidgetProposal | null>(null);\n const [isProposing, setIsProposing] = useState(false);\n const [isApproving, setIsApproving] = useState(false);\n const [approveError, setApproveError] = useState<string | null>(null);\n\n const [previewData, setPreviewData] = useState<unknown>(undefined);\n const [previewLoading, setPreviewLoading] = useState(false);\n const [previewAccessError, setPreviewAccessError] = useState(false);\n\n const permittedDataSinkIds = useMemo(() => sinks.map((sink) => sink.id), [sinks]);\n const permittedParserIds = useMemo(() => parsers.map((parser) => parser.id), [parsers]);\n const availableFields = useMemo(() => extractFields(previewData), [previewData]);\n\n const validation = useMemo(\n () =>\n proposal\n ? validateProposal(proposal, { permittedDataSinkIds, permittedParserIds, availableFields })\n : { issues: [], canApprove: false },\n [proposal, permittedDataSinkIds, permittedParserIds, availableFields]\n );\n\n const previewState = useMemo(\n () =>\n derivePreviewState(validation, {\n loading: previewLoading,\n accessError: previewAccessError,\n rowCount: countRows(previewData),\n }),\n [validation, previewLoading, previewAccessError, previewData]\n );\n\n // Re-resolve the preview whenever the binding changes.\n const bindingKey = proposal\n ? `${proposal.binding.dataSinkId}|${proposal.binding.parserId ?? ''}|${proposal.binding.dataPath ?? ''}`\n : '';\n\n useEffect(() => {\n if (!proposal || !proposal.binding.dataSinkId) {\n setPreviewData(undefined);\n return;\n }\n let cancelled = false;\n setPreviewLoading(true);\n setPreviewAccessError(false);\n\n resolvePreviewData(proposal)\n .then((data) => {\n if (cancelled) return;\n setPreviewData(data);\n })\n .catch(() => {\n if (cancelled) return;\n setPreviewData(undefined);\n setPreviewAccessError(true);\n })\n .finally(() => {\n if (!cancelled) setPreviewLoading(false);\n });\n\n return () => {\n cancelled = true;\n };\n // `bindingKey` captures every input that changes the resolved data.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [bindingKey, resolvePreviewData]);\n\n const handlePropose = useCallback(async () => {\n if (!prompt.trim()) return;\n setIsProposing(true);\n setApproveError(null);\n try {\n const next = await source.propose({ prompt: prompt.trim(), sinks, parsers });\n setProposal(next);\n } finally {\n setIsProposing(false);\n }\n }, [prompt, source, sinks, parsers]);\n\n const patchProposal = useCallback((patch: (current: CustomWidgetProposal) => CustomWidgetProposal) => {\n setProposal((current) => (current ? patch(current) : current));\n }, []);\n\n const handleReject = useCallback(() => {\n setProposal(null);\n setPreviewData(undefined);\n setPreviewAccessError(false);\n setApproveError(null);\n }, []);\n\n const handleApprove = useCallback(async () => {\n if (!proposal || !validation.canApprove) return;\n setIsApproving(true);\n setApproveError(null);\n try {\n await onApprove(proposal);\n onClose();\n } catch (error) {\n setApproveError(\n error instanceof Error\n ? error.message\n : t('bigconsole.widget.customWidget.error.couldNotSaveWidget', 'Could not save the widget.')\n );\n } finally {\n setIsApproving(false);\n }\n }, [proposal, validation.canApprove, onApprove, onClose, t]);\n\n const errors = validation.issues.filter((issue) => issue.severity === 'error');\n const warnings = validation.issues.filter((issue) => issue.severity === 'warning');\n\n return (\n <section\n data-testid=\"custom-widget-proposal-panel\"\n aria-label={t('bigconsole.widget.customWidget.panel.ariaLabel', 'AI custom widget builder')}\n className=\"flex h-full w-full min-w-0 flex-col overflow-y-auto p-4\"\n >\n {/*\n Five stages — prompt → preview → validation → editable proposal →\n approval — used to be five bare divs separated by one uniform gap. Each\n is now a labelled section, and the preview (the thing the reviewer is\n actually judging) is the panel's single `primary` block (BOFF-5636).\n */}\n <SectionStack>\n {/* ---------- Prompt ---------- */}\n <Section\n title={t('bigconsole.widget.customWidget.prompt.label', 'Describe the widget you need')}\n description=\"Plain English. The assistant proposes a typed widget you can edit before anything is saved.\"\n icon={<Sparkles className=\"size-4\" aria-hidden=\"true\" />}\n actions={\n <div className=\"flex flex-wrap gap-2\">\n <Button\n data-testid=\"custom-widget-generate\"\n onClick={handlePropose}\n disabled={!prompt.trim() || isProposing}\n >\n {isProposing ? <Loader2 className=\"mr-2 size-4 animate-spin\" aria-hidden=\"true\" /> : null}\n {proposal\n ? t('bigconsole.widget.customWidget.action.regenerate', 'Regenerate')\n : t('bigconsole.widget.customWidget.action.generateProposal', 'Generate proposal')}\n </Button>\n {proposal ? (\n <Button variant=\"outline\" data-testid=\"custom-widget-reject\" onClick={handleReject}>\n <X className=\"mr-1 size-4\" aria-hidden=\"true\" />\n {t('bigconsole.widget.customWidget.action.reject', 'Reject')}\n </Button>\n ) : null}\n </div>\n }\n >\n <div className=\"flex flex-col gap-2\">\n <Label htmlFor=\"custom-widget-prompt\">What should it show?</Label>\n <Textarea\n id=\"custom-widget-prompt\"\n data-testid=\"custom-widget-prompt\"\n value={prompt}\n onChange={(event) => setPrompt(event.target.value)}\n placeholder={t(\n 'bigconsole.widget.customWidget.prompt.placeholder',\n 'e.g. a funnel of signup conversion stages'\n )}\n rows={3}\n />\n </div>\n </Section>\n\n {!proposal ? (\n <Section emphasis=\"muted\" padding=\"compact\" title=\"No proposal yet\" headingLevel={3}>\n <p className=\"text-sm text-text-secondary\" data-testid=\"custom-widget-empty\">\n {t(\n 'bigconsole.widget.customWidget.empty',\n 'No proposal yet. Describe a widget and the assistant will propose one for you to review — nothing is added to the dashboard until you approve it.'\n )}\n </p>\n </Section>\n ) : (\n <>\n {/* ---------- Preview ---------- */}\n <Section\n emphasis=\"primary\"\n tone={PREVIEW_TONES[previewState]}\n title={previewCopy[previewState].title}\n description=\"Rendered by the real widget renderer against the real parser output — what you approve is what you get.\"\n meta={\n <span\n data-testid=\"custom-widget-preview-state\"\n data-state={previewState}\n className=\"rounded-pill bg-bg-sunken px-2 py-0.5 text-xs text-text-secondary\"\n >\n {previewState}\n </span>\n }\n >\n <div\n data-testid=\"custom-widget-preview\"\n className=\"min-h-[220px] min-w-0 rounded-card border border-[var(--color-border-subtle)] bg-bg-surface p-2\"\n >\n {previewState === 'loading' ? (\n <div className=\"flex h-[200px] items-center justify-center text-text-secondary\">\n <Loader2 className=\"mr-2 size-4 animate-spin\" aria-hidden=\"true\" />\n <span className=\"text-sm\">{previewCopy.loading.body}</span>\n </div>\n ) : previewState === 'valid' ? (\n <div className=\"h-[200px]\">\n <WidgetErrorBoundary widgetId={`preview-${proposal.id}`}>\n <BigConsoleRenderer\n widget={toDraftWidget(\n proposal,\n t('bigconsole.widget.customWidget.untitledWidget', 'Untitled widget')\n )}\n data={previewData as Record<string, unknown>}\n />\n </WidgetErrorBoundary>\n </div>\n ) : (\n <div className=\"flex h-[200px] flex-col items-center justify-center gap-2 px-4 text-center\">\n <AlertTriangle className=\"size-5 text-[var(--color-status-warning-text)]\" aria-hidden=\"true\" />\n <p className=\"text-sm text-text-secondary\">{previewCopy[previewState].body}</p>\n {previewState === 'access-error' ? (\n <Button size=\"sm\" variant=\"outline\" onClick={() => patchProposal((current) => ({ ...current }))}>\n <RefreshCw className=\"mr-1 size-3.5\" aria-hidden=\"true\" />\n {t('bigconsole.widget.customWidget.action.retry', 'Retry')}\n </Button>\n ) : null}\n </div>\n )}\n </div>\n\n {proposal.rationale ? (\n <p className=\"mt-2 text-xs text-text-secondary\" data-testid=\"custom-widget-rationale\">\n {proposal.rationale}\n </p>\n ) : null}\n </Section>\n\n {/* ---------- Validation ---------- */}\n {errors.length > 0 || warnings.length > 0 ? (\n <Section\n tone={errors.length > 0 ? 'danger' : 'warning'}\n padding=\"compact\"\n headingLevel={3}\n title={\n errors.length > 0\n ? `${errors.length} ${errors.length === 1 ? 'problem' : 'problems'} to fix before approving`\n : `${warnings.length} ${warnings.length === 1 ? 'warning' : 'warnings'}`\n }\n >\n <ul data-testid=\"custom-widget-issues\" className=\"flex min-w-0 flex-col gap-1\">\n {[...errors, ...warnings].map((issue) => (\n <li\n key={`${issue.code}-${issue.path ?? ''}`}\n data-severity={issue.severity}\n className={`break-words text-xs ${\n issue.severity === 'error'\n ? 'text-[var(--color-status-error-text)]'\n : 'text-[var(--color-status-warning-text)]'\n }`}\n >\n {issue.message}\n </li>\n ))}\n </ul>\n </Section>\n ) : null}\n\n {/* ---------- Editable proposal ---------- */}\n <Section\n title={t('bigconsole.widget.customWidget.proposal.heading', 'Proposal')}\n description=\"Every field is editable — the assistant proposes, you dispose.\"\n data-testid=\"custom-widget-proposal-fields\"\n >\n <div className=\"flex flex-col gap-3\">\n <div className=\"flex flex-col gap-1\">\n <Label htmlFor=\"custom-widget-title\">\n {t('bigconsole.widget.customWidget.field.title', 'Title')}\n </Label>\n <Input\n id=\"custom-widget-title\"\n data-testid=\"custom-widget-title\"\n className={FIELD_CLASSES}\n value={proposal.presentation.title}\n onChange={(event) =>\n patchProposal((current) => ({\n ...current,\n presentation: { ...current.presentation, title: event.target.value },\n }))\n }\n />\n </div>\n\n <div className=\"flex flex-col gap-1\">\n <Label htmlFor=\"custom-widget-type\">\n {t('bigconsole.widget.customWidget.field.widgetType', 'Widget type')}\n </Label>\n <select\n id=\"custom-widget-type\"\n data-testid=\"custom-widget-type\"\n value={proposal.widgetType}\n onChange={(event) =>\n patchProposal((current) => ({\n ...current,\n widgetType: event.target.value as ProposableWidgetType,\n }))\n }\n className=\"w-full rounded-md border border-[var(--color-border-default)] bg-bg-surface px-3 py-2 text-sm text-text-primary focus:outline-none focus:ring-2 focus:ring-action-primary-bg\"\n >\n {PROPOSABLE_WIDGET_TYPES.map((type) => (\n <option key={type} value={type}>\n {type}\n </option>\n ))}\n </select>\n </div>\n\n <div className=\"flex flex-col gap-1\">\n <Label htmlFor=\"custom-widget-sink\">\n {t('bigconsole.widget.customWidget.field.dataSink', 'DataSink')}\n </Label>\n <select\n id=\"custom-widget-sink\"\n data-testid=\"custom-widget-sink\"\n value={proposal.binding.dataSinkId}\n onChange={(event) => {\n const sink = sinks.find((candidate) => candidate.id === event.target.value);\n patchProposal((current) => ({\n ...current,\n binding: { ...current.binding, dataSinkId: event.target.value, dataSinkKey: sink?.key },\n }));\n }}\n className=\"w-full rounded-md border border-[var(--color-border-default)] bg-bg-surface px-3 py-2 text-sm text-text-primary focus:outline-none focus:ring-2 focus:ring-action-primary-bg\"\n >\n <option value=\"\">\n {t('bigconsole.widget.customWidget.field.selectDataSink', 'Select a DataSink…')}\n </option>\n {sinks.map((sink) => (\n <option key={sink.id} value={sink.id}>\n {sink.name || sink.key}\n </option>\n ))}\n </select>\n </div>\n\n <div className=\"flex flex-col gap-1\">\n <Label htmlFor=\"custom-widget-parser\">\n {t('bigconsole.widget.customWidget.field.parser', 'Parser')}\n </Label>\n <select\n id=\"custom-widget-parser\"\n data-testid=\"custom-widget-parser\"\n value={proposal.binding.parserId ?? ''}\n onChange={(event) => {\n const parser = parsers.find((candidate) => candidate.id === event.target.value);\n patchProposal((current) => ({\n ...current,\n binding: {\n ...current.binding,\n parserId: event.target.value || undefined,\n parserName: parser?.name,\n },\n }));\n }}\n className=\"w-full rounded-md border border-[var(--color-border-default)] bg-bg-surface px-3 py-2 text-sm text-text-primary focus:outline-none focus:ring-2 focus:ring-action-primary-bg\"\n >\n <option value=\"\">{t('bigconsole.widget.customWidget.field.noParser', 'No parser')}</option>\n {parsers.map((parser) => (\n <option key={parser.id} value={parser.id}>\n {parser.name}\n </option>\n ))}\n </select>\n </div>\n\n {/* Field mapping */}\n {Object.keys(proposal.fieldMapping).length > 0 ? (\n <fieldset className=\"flex flex-col gap-2\" data-testid=\"custom-widget-field-mapping\">\n <legend className=\"text-sm font-medium text-text-primary\">\n {t('bigconsole.widget.customWidget.fieldMapping.legend', 'Field mapping')}\n </legend>\n {Object.entries(proposal.fieldMapping).map(([role, field]) => (\n <div key={role} className=\"flex flex-col gap-1\">\n <Label htmlFor={`custom-widget-field-${role}`}>{role}</Label>\n <Input\n id={`custom-widget-field-${role}`}\n data-testid={`custom-widget-field-${role}`}\n value={field}\n onChange={(event) =>\n patchProposal((current) => ({\n ...current,\n fieldMapping: { ...current.fieldMapping, [role]: event.target.value },\n }))\n }\n />\n </div>\n ))}\n {availableFields?.length ? (\n <p className=\"break-words text-xs text-text-secondary\">\n {t('bigconsole.widget.customWidget.fieldMapping.available', 'Available: {{fields}}', {\n fields: availableFields.join(', '),\n })}\n </p>\n ) : null}\n </fieldset>\n ) : null}\n </div>\n </Section>\n\n {/* ---------- Approval ---------- */}\n {approveError ? (\n <Section tone=\"danger\" padding=\"compact\" headingLevel={3} title=\"Could not add the widget\">\n <p\n role=\"alert\"\n className=\"break-words text-xs text-[var(--color-status-error-text)]\"\n data-testid=\"custom-widget-approve-error\"\n >\n {approveError}\n </p>\n </Section>\n ) : null}\n\n <div className=\"flex flex-wrap gap-2 border-t border-[var(--color-border-subtle)] pt-3\">\n <Button\n data-testid=\"custom-widget-approve\"\n onClick={handleApprove}\n disabled={!validation.canApprove || isApproving}\n >\n {isApproving ? (\n <Loader2 className=\"mr-2 size-4 animate-spin\" aria-hidden=\"true\" />\n ) : (\n <Check className=\"mr-1 size-4\" aria-hidden=\"true\" />\n )}\n {t('bigconsole.widget.customWidget.action.approveAndAdd', 'Approve & add to dashboard')}\n </Button>\n <Button variant=\"outline\" onClick={onClose} data-testid=\"custom-widget-cancel\">\n {t('bigconsole.widget.customWidget.action.cancel', 'Cancel')}\n </Button>\n </div>\n </>\n )}\n </SectionStack>\n </section>\n );\n});\n\nexport default CustomWidgetProposalPanel;\n"],"mappings":"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"}
@@ -0,0 +1,15 @@
1
+ import { LayoutItem } from '../utils/layoutBaseline';
2
+
3
+ export interface DashboardLayoutBaseline {
4
+ /** The board's positions differ from the last loaded or saved ones, because a person placed something. */
5
+ dirty: boolean;
6
+ /**
7
+ * Re-take the baseline after the positions reached the server. `sent` is exactly
8
+ * what was sent; `editRevisionAtSave` is `layoutEditRevision` read BEFORE the
9
+ * request began, so a placement made while it was in flight stays dirty.
10
+ * Call it on success only: a failed save must leave the layout dirty.
11
+ */
12
+ markSaved: (sent: ReadonlyArray<LayoutItem>, editRevisionAtSave: number) => void;
13
+ }
14
+ export declare function useDashboardLayoutBaseline(dashboardId: string | undefined): DashboardLayoutBaseline;
15
+ //# sourceMappingURL=useDashboardLayoutBaseline.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"useDashboardLayoutBaseline.d.ts","sourceRoot":"","sources":["../../../src/bigconsole/hooks/useDashboardLayoutBaseline.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAIH,OAAO,EAML,KAAK,UAAU,EAChB,MAAM,yBAAyB,CAAC;AAEjC,MAAM,WAAW,uBAAuB;IACtC,0GAA0G;IAC1G,KAAK,EAAE,OAAO,CAAC;IACf;;;;;OAKG;IACH,SAAS,EAAE,CAAC,IAAI,EAAE,aAAa,CAAC,UAAU,CAAC,EAAE,kBAAkB,EAAE,MAAM,KAAK,IAAI,CAAC;CAClF;AAED,wBAAgB,0BAA0B,CAAC,WAAW,EAAE,MAAM,GAAG,SAAS,GAAG,uBAAuB,CA0BnG"}
@@ -0,0 +1,34 @@
1
+ import { useWidgetStore as e } from "../store/widgetStore.js";
2
+ import "../store/index.js";
3
+ import { INITIAL_LAYOUT_BASELINE as t, isLayoutBaselineDirty as n, layoutFromWidgets as r, normaliseLayout as i, reduceLayoutBaseline as a } from "../utils/layoutBaseline.js";
4
+ import { useCallback as o, useEffect as s, useMemo as c, useReducer as l, useState as u } from "react";
5
+ //#region src/bigconsole/hooks/useDashboardLayoutBaseline.ts
6
+ function d(d) {
7
+ let f = e((e) => e.widgets), p = e((e) => e.layoutEditRevision), [m, h] = l(a, t), g = c(() => r(f.values()), [f]), [_, v] = u(d);
8
+ _ !== d && (v(d), h({ type: "reset" })), s(() => {
9
+ h({
10
+ type: "observe",
11
+ layout: g,
12
+ editRevision: p
13
+ });
14
+ }, [
15
+ d,
16
+ g,
17
+ p
18
+ ]);
19
+ let y = o((e, t) => {
20
+ h({
21
+ type: "saved",
22
+ layout: i(e),
23
+ editRevision: t
24
+ });
25
+ }, []);
26
+ return {
27
+ dirty: n(m, g),
28
+ markSaved: y
29
+ };
30
+ }
31
+ //#endregion
32
+ export { d as useDashboardLayoutBaseline };
33
+
34
+ //# sourceMappingURL=useDashboardLayoutBaseline.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"useDashboardLayoutBaseline.js","names":[],"sources":["../../../src/bigconsole/hooks/useDashboardLayoutBaseline.ts"],"sourcesContent":["/**\n * useDashboardLayoutBaseline (BOFF-8060, N3/N4: the dashboard leave guard)\n *\n * The React shell around `utils/layoutBaseline.ts`: \"does the dashboard builder\n * hold moved or resized widgets that are not saved yet?\". The rules, and why a\n * layout event alone is not a dirty signal, are documented there; this file only\n * feeds the reducer from the widget store and exposes the answer.\n *\n * ★ It reads the store, never the grid. The canvas does not hand react-grid-layout\n * an `onLayoutChange` (`DashboardGrid` wires `onDragStop` / `onResizeStop` only,\n * with `compactType={null}`), so mount and compaction events never reach the store\n * at all; what does reach it is load, create, delete, save write-back (all\n * absorbed while the baseline floats) and the person's placements (which freeze\n * it, through `layoutEditRevision`).\n */\n\nimport { useCallback, useEffect, useMemo, useReducer, useState } from 'react';\nimport { useWidgetStore } from '../store';\nimport {\n INITIAL_LAYOUT_BASELINE,\n isLayoutBaselineDirty,\n layoutFromWidgets,\n normaliseLayout,\n reduceLayoutBaseline,\n type LayoutItem,\n} from '../utils/layoutBaseline';\n\nexport interface DashboardLayoutBaseline {\n /** The board's positions differ from the last loaded or saved ones, because a person placed something. */\n dirty: boolean;\n /**\n * Re-take the baseline after the positions reached the server. `sent` is exactly\n * what was sent; `editRevisionAtSave` is `layoutEditRevision` read BEFORE the\n * request began, so a placement made while it was in flight stays dirty.\n * Call it on success only: a failed save must leave the layout dirty.\n */\n markSaved: (sent: ReadonlyArray<LayoutItem>, editRevisionAtSave: number) => void;\n}\n\nexport function useDashboardLayoutBaseline(dashboardId: string | undefined): DashboardLayoutBaseline {\n const widgets = useWidgetStore((state) => state.widgets);\n const editRevision = useWidgetStore((state) => state.layoutEditRevision);\n const [baseline, dispatch] = useReducer(reduceLayoutBaseline, INITIAL_LAYOUT_BASELINE);\n\n const layout = useMemo(() => layoutFromWidgets(widgets.values()), [widgets]);\n\n // A different dashboard on the same mounted page (the route param changed):\n // forget the old baseline in the SAME render, so the old dashboard's frozen\n // baseline is never compared against the new one's widgets. This is React's\n // \"adjust state when a prop changes\" pattern; the effect below then re-seeds it.\n const [trackedDashboardId, setTrackedDashboardId] = useState(dashboardId);\n if (trackedDashboardId !== dashboardId) {\n setTrackedDashboardId(dashboardId);\n dispatch({ type: 'reset' });\n }\n\n useEffect(() => {\n dispatch({ type: 'observe', layout, editRevision });\n }, [dashboardId, layout, editRevision]);\n\n const markSaved = useCallback((sent: ReadonlyArray<LayoutItem>, editRevisionAtSave: number) => {\n dispatch({ type: 'saved', layout: normaliseLayout(sent), editRevision: editRevisionAtSave });\n }, []);\n\n return { dirty: isLayoutBaselineDirty(baseline, layout), markSaved };\n}\n"],"mappings":";;;;;AAuCA,SAAgB,EAA2B,GAA0D;CACnG,IAAM,IAAU,GAAgB,MAAU,EAAM,QAAQ,EAClD,IAAe,GAAgB,MAAU,EAAM,mBAAmB,EAClE,CAAC,GAAU,KAAY,EAAW,GAAsB,EAAwB,EAEhF,IAAS,QAAc,EAAkB,EAAQ,QAAQ,CAAC,EAAE,CAAC,EAAQ,CAAC,EAMtE,CAAC,GAAoB,KAAyB,EAAS,EAAY;AAMzE,CALI,MAAuB,MACzB,EAAsB,EAAY,EAClC,EAAS,EAAE,MAAM,SAAS,CAAC,GAG7B,QAAgB;AACd,IAAS;GAAE,MAAM;GAAW;GAAQ;GAAc,CAAC;IAClD;EAAC;EAAa;EAAQ;EAAa,CAAC;CAEvC,IAAM,IAAY,GAAa,GAAiC,MAA+B;AAC7F,IAAS;GAAE,MAAM;GAAS,QAAQ,EAAgB,EAAK;GAAE,cAAc;GAAoB,CAAC;IAC3F,EAAE,CAAC;AAEN,QAAO;EAAE,OAAO,EAAsB,GAAU,EAAO;EAAE;EAAW"}