@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
@@ -1,6 +1,6 @@
1
1
  import { memo as e, useMemo as t } from "react";
2
- import { jsx as n, jsxs as r } from "react/jsx-runtime";
3
- import { useI18n as i } from "@burdenoff/fe-libs/shared/providers/shell/I18nProvider";
2
+ import { useI18n as n } from "@burdenoff/fe-libs/shared/providers/shell/I18nProvider";
3
+ import { jsx as r, jsxs as i } from "react/jsx-runtime";
4
4
  //#region src/bigconsole/renderers/adapters/CustomRenderer.tsx
5
5
  var a = /* @__PURE__ */ new Map();
6
6
  function o(e, t) {
@@ -13,77 +13,77 @@ function c() {
13
13
  return Array.from(a.keys());
14
14
  }
15
15
  o("SimpleText", ({ data: e, props: t }) => {
16
- let { t: r } = i(), a = e.text || t?.text, o = t?.align || "left";
17
- return a ? /* @__PURE__ */ n("div", {
16
+ let { t: i } = n(), a = e.text || t?.text, o = t?.align || "left";
17
+ return a ? /* @__PURE__ */ r("div", {
18
18
  className: `p-4 text-${o}`,
19
- children: /* @__PURE__ */ n("p", {
19
+ children: /* @__PURE__ */ r("p", {
20
20
  className: "text-primary",
21
21
  children: a
22
22
  })
23
- }) : /* @__PURE__ */ n("div", {
23
+ }) : /* @__PURE__ */ r("div", {
24
24
  className: "flex items-center justify-center h-full p-4 text-secondary",
25
- children: /* @__PURE__ */ n("p", {
25
+ children: /* @__PURE__ */ r("p", {
26
26
  className: "text-sm",
27
- children: r("bigconsole.widget.renderer.custom.noData", "No data available")
27
+ children: i("bigconsole.widget.renderer.custom.noData", "No data available")
28
28
  })
29
29
  });
30
30
  }), o("Counter", ({ data: e, props: t }) => {
31
- let { t: a } = i(), o = e.value ?? t?.value, s = e.label || t?.label, c = t?.format || "number";
32
- if (o == null) return /* @__PURE__ */ n("div", {
31
+ let { t: a } = n(), o = e.value ?? t?.value, s = e.label || t?.label, c = t?.format || "number";
32
+ if (o == null) return /* @__PURE__ */ r("div", {
33
33
  className: "flex flex-col items-center justify-center h-full p-4 text-secondary",
34
- children: /* @__PURE__ */ n("p", {
34
+ children: /* @__PURE__ */ r("p", {
35
35
  className: "text-sm",
36
36
  children: a("bigconsole.widget.renderer.custom.noData", "No data available")
37
37
  })
38
38
  });
39
39
  let l = o;
40
- return /* @__PURE__ */ r("div", {
40
+ return /* @__PURE__ */ i("div", {
41
41
  className: "flex flex-col items-center justify-center h-full p-4 animate-in fade-in duration-200",
42
- children: [/* @__PURE__ */ n("span", {
42
+ children: [/* @__PURE__ */ r("span", {
43
43
  className: "text-3xl font-bold text-primary",
44
44
  children: c === "currency" ? new Intl.NumberFormat("en-US", {
45
45
  style: "currency",
46
46
  currency: "USD"
47
47
  }).format(l) : c === "percent" ? `${(l * 100).toFixed(1)}%` : new Intl.NumberFormat().format(l)
48
- }), s && /* @__PURE__ */ n("span", {
48
+ }), s && /* @__PURE__ */ r("span", {
49
49
  className: "text-sm text-secondary mt-1",
50
50
  children: s
51
51
  })]
52
52
  });
53
53
  }), o("Card", ({ data: e, props: t, onAction: a }) => {
54
- let { t: o } = i(), s = e.title || t?.title, c = e.description || t?.description || "";
55
- if (!s && !c) return /* @__PURE__ */ n("div", {
54
+ let { t: o } = n(), s = e.title || t?.title, c = e.description || t?.description || "";
55
+ if (!s && !c) return /* @__PURE__ */ r("div", {
56
56
  className: "flex items-center justify-center h-full p-4 text-secondary",
57
- children: /* @__PURE__ */ n("p", {
57
+ children: /* @__PURE__ */ r("p", {
58
58
  className: "text-sm",
59
59
  children: o("bigconsole.widget.renderer.custom.noData", "No data available")
60
60
  })
61
61
  });
62
62
  let l = e.image || t?.image, u = t?.actionLabel || o("bigconsole.widget.renderer.custom.viewDetails", "View Details"), d = t?.showAction !== !1;
63
- return /* @__PURE__ */ r("div", {
63
+ return /* @__PURE__ */ i("div", {
64
64
  className: "flex flex-col h-full bg-surface rounded-lg overflow-hidden animate-in fade-in duration-200",
65
65
  children: [
66
- l && /* @__PURE__ */ n("div", {
66
+ l && /* @__PURE__ */ r("div", {
67
67
  className: "aspect-video bg-bg-sunken",
68
- children: /* @__PURE__ */ n("img", {
68
+ children: /* @__PURE__ */ r("img", {
69
69
  src: l,
70
70
  alt: "",
71
71
  className: "w-full h-full object-cover"
72
72
  })
73
73
  }),
74
- /* @__PURE__ */ r("div", {
74
+ /* @__PURE__ */ i("div", {
75
75
  className: "flex-1 p-4",
76
- children: [s && /* @__PURE__ */ n("h3", {
76
+ children: [s && /* @__PURE__ */ r("h3", {
77
77
  className: "text-lg font-semibold text-primary",
78
78
  children: s
79
- }), c && /* @__PURE__ */ n("p", {
79
+ }), c && /* @__PURE__ */ r("p", {
80
80
  className: "text-sm text-secondary mt-1",
81
81
  children: c
82
82
  })]
83
83
  }),
84
- d && /* @__PURE__ */ n("div", {
84
+ d && /* @__PURE__ */ r("div", {
85
85
  className: "p-4 pt-0",
86
- children: /* @__PURE__ */ n("button", {
86
+ children: /* @__PURE__ */ r("button", {
87
87
  type: "button",
88
88
  onClick: () => a?.("click", { title: s }),
89
89
  className: "w-full px-4 py-2 text-sm font-medium text-white bg-action-primary hover:bg-action-primary/90 rounded-md transition-colors",
@@ -94,25 +94,25 @@ o("SimpleText", ({ data: e, props: t }) => {
94
94
  });
95
95
  });
96
96
  var l = ({ componentName: e }) => {
97
- let { t } = i();
98
- return /* @__PURE__ */ n("div", {
97
+ let { t } = n();
98
+ return /* @__PURE__ */ r("div", {
99
99
  className: "flex flex-col items-center justify-center h-full p-4 text-secondary animate-in fade-in duration-200",
100
- children: /* @__PURE__ */ r("div", {
100
+ children: /* @__PURE__ */ i("div", {
101
101
  className: "text-center",
102
102
  children: [
103
- /* @__PURE__ */ n("p", {
103
+ /* @__PURE__ */ r("p", {
104
104
  className: "font-medium",
105
105
  children: t("bigconsole.widget.renderer.custom.componentNotFound", "Component Not Found")
106
106
  }),
107
- /* @__PURE__ */ n("p", {
107
+ /* @__PURE__ */ r("p", {
108
108
  className: "text-sm mt-1",
109
109
  children: t("bigconsole.widget.renderer.custom.componentNotRegistered", "Component \"{{name}}\" is not registered", { name: e ?? "" })
110
110
  }),
111
- /* @__PURE__ */ r("div", {
111
+ /* @__PURE__ */ i("div", {
112
112
  className: "mt-4 text-xs",
113
- children: [/* @__PURE__ */ n("p", { children: t("bigconsole.widget.renderer.custom.availableComponents", "Available components:") }), /* @__PURE__ */ n("ul", {
113
+ children: [/* @__PURE__ */ r("p", { children: t("bigconsole.widget.renderer.custom.availableComponents", "Available components:") }), /* @__PURE__ */ r("ul", {
114
114
  className: "mt-1",
115
- children: c().map((e) => /* @__PURE__ */ n("li", {
115
+ children: c().map((e) => /* @__PURE__ */ r("li", {
116
116
  className: "text-link",
117
117
  children: e
118
118
  }, e))
@@ -122,44 +122,44 @@ var l = ({ componentName: e }) => {
122
122
  })
123
123
  });
124
124
  }, u = e(function({ widget: e, data: a, template: o, isLoading: c, error: u, onDrilldown: d, onAction: f }) {
125
- let { t: p } = i(), m = t(() => {
125
+ let { t: p } = n(), m = t(() => {
126
126
  if (o && typeof o == "object" && "componentName" in o) return o.componentName;
127
127
  let t = e.config;
128
128
  return t?.componentName || t?.componentId || "";
129
129
  }, [o, e.config]), h = t(() => o && typeof o == "object" && "props" in o ? o.props : {}, [o]), g = t(() => m ? s(m) : null, [m]);
130
- return c ? /* @__PURE__ */ n("div", {
130
+ return c ? /* @__PURE__ */ r("div", {
131
131
  className: "flex items-center justify-center h-full p-4 animate-in fade-in duration-200",
132
- children: /* @__PURE__ */ r("div", {
132
+ children: /* @__PURE__ */ i("div", {
133
133
  className: "animate-pulse space-y-2 w-full",
134
- children: [/* @__PURE__ */ n("div", { className: "h-4 bg-bg-sunken rounded w-3/4" }), /* @__PURE__ */ n("div", { className: "h-4 bg-bg-sunken rounded w-1/2" })]
134
+ children: [/* @__PURE__ */ r("div", { className: "h-4 bg-bg-sunken rounded w-3/4" }), /* @__PURE__ */ r("div", { className: "h-4 bg-bg-sunken rounded w-1/2" })]
135
135
  })
136
- }) : u ? /* @__PURE__ */ n("div", {
136
+ }) : u ? /* @__PURE__ */ r("div", {
137
137
  className: "flex items-center justify-center h-full p-4 text-red-600 dark:text-red-400",
138
- children: /* @__PURE__ */ r("div", {
138
+ children: /* @__PURE__ */ i("div", {
139
139
  className: "text-center",
140
- children: [/* @__PURE__ */ n("p", {
140
+ children: [/* @__PURE__ */ r("p", {
141
141
  className: "font-medium",
142
142
  children: p("bigconsole.widget.renderer.custom.errorRendering", "Error rendering component")
143
- }), /* @__PURE__ */ n("p", {
143
+ }), /* @__PURE__ */ r("p", {
144
144
  className: "text-sm mt-1",
145
145
  children: u
146
146
  })]
147
147
  })
148
- }) : m ? g ? /* @__PURE__ */ n(g, {
148
+ }) : m ? g ? /* @__PURE__ */ r(g, {
149
149
  widget: e,
150
150
  data: a,
151
151
  template: o,
152
152
  props: h,
153
153
  onAction: f,
154
154
  onDrilldown: d
155
- }) : /* @__PURE__ */ n(l, { componentName: m }) : /* @__PURE__ */ n("div", {
155
+ }) : /* @__PURE__ */ r(l, { componentName: m }) : /* @__PURE__ */ r("div", {
156
156
  className: "flex items-center justify-center h-full p-4 text-secondary animate-in fade-in duration-200",
157
- children: /* @__PURE__ */ r("div", {
157
+ children: /* @__PURE__ */ i("div", {
158
158
  className: "text-center",
159
- children: [/* @__PURE__ */ n("p", {
159
+ children: [/* @__PURE__ */ r("p", {
160
160
  className: "font-medium",
161
161
  children: p("bigconsole.widget.renderer.custom.noComponentSpecified", "No Component Specified")
162
- }), /* @__PURE__ */ n("p", {
162
+ }), /* @__PURE__ */ r("p", {
163
163
  className: "text-sm mt-1",
164
164
  children: p("bigconsole.widget.renderer.custom.setComponentName", "Set componentName in template or widget config")
165
165
  })]
@@ -20,6 +20,18 @@ interface WidgetState {
20
20
  clipboardWidgetId: string | null;
21
21
  clipboardAction: 'copy' | 'cut' | null;
22
22
  gridLayout: GridLayoutItem[];
23
+ /**
24
+ * Counts the placements a PERSON made: bumped by `updateWidgetPosition` and by
25
+ * nothing else. The canvas calls that action only when a drag or a resize ENDS
26
+ * (`DashboardCanvas.handleLayoutChange`), and the inspector's "Position & size"
27
+ * fields call it only on a typed change that actually moves the widget
28
+ * (`LayoutSection.commit`). Loading (`setWidgets`), creating (`addWidget`),
29
+ * saving (`updateWidget`) and deleting (`removeWidget`) leave it alone, so a
30
+ * reader can tell "the person moved something" from "the list was refreshed"
31
+ * without comparing positions (BOFF-8060, the dashboard leave guard).
32
+ * Never reset: a reader keeps the last value it saw across `reset()`.
33
+ */
34
+ layoutEditRevision: number;
23
35
  typeFilter: WidgetType | null;
24
36
  setWidgets: (widgets: Widget[]) => void;
25
37
  addWidget: (widget: Widget) => void;
@@ -1 +1 @@
1
- {"version":3,"file":"widgetStore.d.ts","sourceRoot":"","sources":["../../../src/bigconsole/store/widgetStore.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"widgetStore.d.ts","sourceRoot":"","sources":["../../../src/bigconsole/store/widgetStore.ts"],"names":[],"mappings":"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"}
@@ -20,6 +20,7 @@ var i = {
20
20
  typeFilter: null
21
21
  }, a = e()(t(n((e, t) => ({
22
22
  ...i,
23
+ layoutEditRevision: 0,
23
24
  setWidgets: (t) => e({
24
25
  widgets: new Map(t.map((e) => [e.id, e])),
25
26
  gridLayout: t.map((e) => ({
@@ -155,7 +156,8 @@ var i = {
155
156
  y: n.y,
156
157
  w: n.width,
157
158
  h: n.height
158
- } : e)
159
+ } : e),
160
+ layoutEditRevision: e.layoutEditRevision + 1
159
161
  };
160
162
  }),
161
163
  setGridLayout: (t) => e({ gridLayout: t }),
@@ -1 +1 @@
1
- {"version":3,"file":"widgetStore.js","names":[],"sources":["../../../src/bigconsole/store/widgetStore.ts"],"sourcesContent":["/**\n * Widget Store - Zustand state management for widgets\n *\n * Manages widget UI state, selection, and local optimistic updates.\n * Server state is managed by Apollo Client.\n */\n\nimport { create } from 'zustand';\nimport { devtools, persist } from 'zustand/middleware';\nimport { createProfileScopedStorage } from '@burdenoff/fe-libs/shared/utils';\nimport type { Widget, WidgetPosition, WidgetType, GridLayoutItem } from '../types';\n\n// ============================================================================\n// Types\n// ============================================================================\n\ninterface WidgetUIState {\n isLoading: boolean;\n isError: boolean;\n errorMessage?: string;\n}\n\ninterface WidgetState {\n // Widget data (cached from server)\n widgets: Map<string, Widget>;\n widgetUIStates: Map<string, WidgetUIState>;\n\n // Dirty tracking (widgets with unsaved changes)\n dirtyWidgetIds: Set<string>;\n\n // Selection state\n selectedWidgetId: string | null;\n hoveredWidgetId: string | null;\n\n // Edit mode\n isEditMode: boolean;\n isDragging: boolean;\n isResizing: boolean;\n\n // Panels\n configPanelOpen: boolean;\n addWidgetDialogOpen: boolean;\n widgetTypeSelectorOpen: boolean;\n\n // Clipboard\n clipboardWidgetId: string | null;\n clipboardAction: 'copy' | 'cut' | null;\n\n // Grid layout (for react-grid-layout)\n gridLayout: GridLayoutItem[];\n\n // Filter state\n typeFilter: WidgetType | null;\n\n // Actions\n setWidgets: (widgets: Widget[]) => void;\n addWidget: (widget: Widget) => void;\n updateWidget: (id: string, updates: Partial<Widget>) => void;\n removeWidget: (id: string) => void;\n\n // Selection actions\n selectWidget: (id: string | null) => void;\n hoverWidget: (id: string | null) => void;\n\n // Edit mode actions\n setEditMode: (enabled: boolean) => void;\n toggleEditMode: () => void;\n setDragging: (dragging: boolean) => void;\n setResizing: (resizing: boolean) => void;\n\n // Panel actions\n openConfigPanel: () => void;\n closeConfigPanel: () => void;\n toggleConfigPanel: () => void;\n openAddWidgetDialog: () => void;\n closeAddWidgetDialog: () => void;\n openWidgetTypeSelector: () => void;\n closeWidgetTypeSelector: () => void;\n\n // Clipboard actions\n copyWidget: (id: string) => void;\n cutWidget: (id: string) => void;\n pasteWidget: () => void;\n clearClipboard: () => void;\n\n // Position actions\n updateWidgetPosition: (id: string, position: WidgetPosition) => void;\n setGridLayout: (layout: GridLayoutItem[]) => void;\n\n // UI state actions\n setWidgetLoading: (id: string, loading: boolean) => void;\n setWidgetError: (id: string, error: string | null) => void;\n\n // Filter actions\n setTypeFilter: (type: WidgetType | null) => void;\n\n // Dirty tracking actions\n markWidgetDirty: (id: string) => void;\n markWidgetClean: (id: string) => void;\n clearAllDirty: () => void;\n getDirtyWidgets: () => Widget[];\n\n // Bulk actions\n clearSelection: () => void;\n reset: () => void;\n}\n\n// ============================================================================\n// Initial State\n// ============================================================================\n\nconst initialState = {\n widgets: new Map<string, Widget>(),\n widgetUIStates: new Map<string, WidgetUIState>(),\n dirtyWidgetIds: new Set<string>(),\n selectedWidgetId: null,\n hoveredWidgetId: null,\n isEditMode: false,\n isDragging: false,\n isResizing: false,\n configPanelOpen: false,\n addWidgetDialogOpen: false,\n widgetTypeSelectorOpen: false,\n clipboardWidgetId: null,\n clipboardAction: null,\n gridLayout: [],\n typeFilter: null,\n};\n\n// ============================================================================\n// Store\n// ============================================================================\n\nexport const useWidgetStore = create<WidgetState>()(\n devtools(\n persist(\n (set, get) => ({\n ...initialState,\n\n // Widget data actions\n setWidgets: (widgets) =>\n set({\n widgets: new Map(widgets.map((w) => [w.id, w])),\n gridLayout: widgets.map((w) => ({\n i: w.id,\n x: w.positionX,\n y: w.positionY,\n w: w.positionWidth,\n h: w.positionHeight,\n minW: 2,\n minH: 2,\n })),\n }),\n\n addWidget: (widget) =>\n set((state) => {\n // Normalize position fields\n const normalizedWidget = {\n ...widget,\n positionX: widget.positionX ?? widget.position?.x ?? 0,\n positionY: widget.positionY ?? widget.position?.y ?? 0,\n positionWidth: widget.positionWidth ?? widget.position?.width ?? 4,\n positionHeight: widget.positionHeight ?? widget.position?.height ?? 4,\n };\n const newWidgets = new Map(state.widgets);\n newWidgets.set(widget.id, normalizedWidget);\n return {\n widgets: newWidgets,\n gridLayout: [\n ...state.gridLayout,\n {\n i: widget.id,\n x: normalizedWidget.positionX,\n y: normalizedWidget.positionY,\n w: normalizedWidget.positionWidth,\n h: normalizedWidget.positionHeight,\n minW: 2,\n minH: 2,\n },\n ],\n };\n }),\n\n updateWidget: (id, updates) =>\n set((state) => {\n const newWidgets = new Map(state.widgets);\n const existing = newWidgets.get(id);\n if (existing) {\n newWidgets.set(id, { ...existing, ...updates });\n }\n return { widgets: newWidgets };\n }),\n\n removeWidget: (id) =>\n set((state) => {\n const newWidgets = new Map(state.widgets);\n newWidgets.delete(id);\n const newUIStates = new Map(state.widgetUIStates);\n newUIStates.delete(id);\n return {\n widgets: newWidgets,\n widgetUIStates: newUIStates,\n gridLayout: state.gridLayout.filter((item) => item.i !== id),\n selectedWidgetId: state.selectedWidgetId === id ? null : state.selectedWidgetId,\n };\n }),\n\n // Selection actions\n selectWidget: (id) =>\n set({\n selectedWidgetId: id,\n configPanelOpen: id !== null,\n }),\n\n hoverWidget: (id) => set({ hoveredWidgetId: id }),\n\n // Edit mode actions\n setEditMode: (enabled) => set({ isEditMode: enabled }),\n toggleEditMode: () => set((state) => ({ isEditMode: !state.isEditMode })),\n setDragging: (dragging) => set({ isDragging: dragging }),\n setResizing: (resizing) => set({ isResizing: resizing }),\n\n // Panel actions\n openConfigPanel: () => set({ configPanelOpen: true }),\n closeConfigPanel: () => set({ configPanelOpen: false }),\n toggleConfigPanel: () => set((state) => ({ configPanelOpen: !state.configPanelOpen })),\n openAddWidgetDialog: () => set({ addWidgetDialogOpen: true }),\n closeAddWidgetDialog: () => set({ addWidgetDialogOpen: false }),\n openWidgetTypeSelector: () => set({ widgetTypeSelectorOpen: true }),\n closeWidgetTypeSelector: () => set({ widgetTypeSelectorOpen: false }),\n\n // Clipboard actions\n copyWidget: (id) => set({ clipboardWidgetId: id, clipboardAction: 'copy' }),\n cutWidget: (id) => set({ clipboardWidgetId: id, clipboardAction: 'cut' }),\n pasteWidget: () =>\n set((state) => {\n const { clipboardWidgetId, clipboardAction, widgets } = state;\n if (!clipboardWidgetId) return state;\n\n const sourceWidget = widgets.get(clipboardWidgetId);\n if (!sourceWidget) return state;\n\n // Create a copy of the widget with new ID and offset position\n const newWidget: Widget = {\n ...sourceWidget,\n id: crypto.randomUUID(),\n title: `${sourceWidget.title} (Copy)`,\n positionX: (sourceWidget.positionX ?? 0) + 1,\n positionY: (sourceWidget.positionY ?? 0) + 1,\n position: sourceWidget.position\n ? {\n ...sourceWidget.position,\n x: (sourceWidget.position.x ?? 0) + 1,\n y: (sourceWidget.position.y ?? 0) + 1,\n }\n : { x: 0, y: 0, width: 4, height: 3 },\n createdAt: new Date().toISOString(),\n updatedAt: new Date().toISOString(),\n };\n\n const newWidgets = new Map(widgets);\n newWidgets.set(newWidget.id, newWidget);\n\n // If cut, remove the original\n if (clipboardAction === 'cut') {\n newWidgets.delete(clipboardWidgetId);\n }\n\n return {\n widgets: newWidgets,\n clipboardWidgetId: clipboardAction === 'cut' ? null : clipboardWidgetId,\n clipboardAction: clipboardAction === 'cut' ? null : clipboardAction,\n gridLayout: [\n ...state.gridLayout.filter((item) => clipboardAction !== 'cut' || item.i !== clipboardWidgetId),\n {\n i: newWidget.id,\n x: newWidget.positionX ?? 0,\n y: newWidget.positionY ?? 0,\n w: newWidget.positionWidth ?? 4,\n h: newWidget.positionHeight ?? 4,\n minW: 2,\n minH: 2,\n },\n ],\n };\n }),\n clearClipboard: () => set({ clipboardWidgetId: null, clipboardAction: null }),\n\n // Position actions\n updateWidgetPosition: (id, position) =>\n set((state) => {\n const newWidgets = new Map(state.widgets);\n const existing = newWidgets.get(id);\n if (existing) {\n newWidgets.set(id, {\n ...existing,\n position,\n positionX: position.x,\n positionY: position.y,\n positionWidth: position.width,\n positionHeight: position.height,\n });\n }\n return {\n widgets: newWidgets,\n gridLayout: state.gridLayout.map((item) =>\n item.i === id\n ? {\n ...item,\n x: position.x,\n y: position.y,\n w: position.width,\n h: position.height,\n }\n : item\n ),\n };\n }),\n\n setGridLayout: (layout) => set({ gridLayout: layout }),\n\n // UI state actions\n setWidgetLoading: (id, loading) =>\n set((state) => {\n const newUIStates = new Map(state.widgetUIStates);\n const existing = newUIStates.get(id) || {\n isLoading: false,\n isError: false,\n };\n newUIStates.set(id, { ...existing, isLoading: loading });\n return { widgetUIStates: newUIStates };\n }),\n\n setWidgetError: (id, error) =>\n set((state) => {\n const newUIStates = new Map(state.widgetUIStates);\n const existing = newUIStates.get(id) || {\n isLoading: false,\n isError: false,\n };\n newUIStates.set(id, {\n ...existing,\n isError: error !== null,\n errorMessage: error || undefined,\n });\n return { widgetUIStates: newUIStates };\n }),\n\n // Filter actions\n setTypeFilter: (type) => set({ typeFilter: type }),\n\n // Dirty tracking actions\n markWidgetDirty: (id) =>\n set((state) => {\n const newDirty = new Set(state.dirtyWidgetIds);\n newDirty.add(id);\n return { dirtyWidgetIds: newDirty };\n }),\n\n markWidgetClean: (id) =>\n set((state) => {\n const newDirty = new Set(state.dirtyWidgetIds);\n newDirty.delete(id);\n return { dirtyWidgetIds: newDirty };\n }),\n\n clearAllDirty: () => set({ dirtyWidgetIds: new Set<string>() }),\n\n getDirtyWidgets: () => {\n const state = get();\n return Array.from(state.dirtyWidgetIds)\n .map((id) => state.widgets.get(id))\n .filter((w): w is Widget => w !== undefined);\n },\n\n // Bulk actions\n clearSelection: () =>\n set({\n selectedWidgetId: null,\n hoveredWidgetId: null,\n configPanelOpen: false,\n }),\n\n reset: () => set(initialState),\n }),\n {\n name: 'bigconsole-widget-store',\n storage: createProfileScopedStorage(),\n partialize: (state) => ({\n // Only persist UI preferences, not data\n isEditMode: state.isEditMode,\n typeFilter: state.typeFilter,\n }),\n }\n ),\n { name: 'WidgetStore' }\n )\n);\n\n// ============================================================================\n// Selectors\n// ============================================================================\n\nexport const useSelectedWidget = () => {\n const selectedWidgetId = useWidgetStore((state) => state.selectedWidgetId);\n const widgets = useWidgetStore((state) => state.widgets);\n return selectedWidgetId ? widgets.get(selectedWidgetId) : null;\n};\n\nexport const useWidgetById = (id: string) => {\n return useWidgetStore((state) => state.widgets.get(id));\n};\n\n// Stable default UI state to prevent infinite re-renders\nconst DEFAULT_UI_STATE: WidgetUIState = { isLoading: false, isError: false };\n\nexport const useWidgetUIState = (id: string) => {\n return useWidgetStore((state) => state.widgetUIStates.get(id) || DEFAULT_UI_STATE);\n};\n\nexport const useWidgetsArray = () => {\n return useWidgetStore((state) => Array.from(state.widgets.values()));\n};\n\nexport const useFilteredWidgets = () => {\n const widgets = useWidgetStore((state) => Array.from(state.widgets.values()));\n const typeFilter = useWidgetStore((state) => state.typeFilter);\n\n if (!typeFilter) return widgets;\n return widgets.filter((w) => w.type === typeFilter);\n};\n\nexport const useIsWidgetSelected = (id: string) => {\n return useWidgetStore((state) => state.selectedWidgetId === id);\n};\n\nexport const useHasClipboard = () => {\n return useWidgetStore((state) => state.clipboardWidgetId !== null);\n};\n\nexport const useHasDirtyWidgets = () => {\n return useWidgetStore((state) => state.dirtyWidgetIds.size > 0);\n};\n\nexport const useDirtyWidgetIds = () => {\n return useWidgetStore((state) => state.dirtyWidgetIds);\n};\n"],"mappings":"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"}
1
+ {"version":3,"file":"widgetStore.js","names":[],"sources":["../../../src/bigconsole/store/widgetStore.ts"],"sourcesContent":["/**\n * Widget Store - Zustand state management for widgets\n *\n * Manages widget UI state, selection, and local optimistic updates.\n * Server state is managed by Apollo Client.\n */\n\nimport { create } from 'zustand';\nimport { devtools, persist } from 'zustand/middleware';\nimport { createProfileScopedStorage } from '@burdenoff/fe-libs/shared/utils';\nimport type { Widget, WidgetPosition, WidgetType, GridLayoutItem } from '../types';\n\n// ============================================================================\n// Types\n// ============================================================================\n\ninterface WidgetUIState {\n isLoading: boolean;\n isError: boolean;\n errorMessage?: string;\n}\n\ninterface WidgetState {\n // Widget data (cached from server)\n widgets: Map<string, Widget>;\n widgetUIStates: Map<string, WidgetUIState>;\n\n // Dirty tracking (widgets with unsaved changes)\n dirtyWidgetIds: Set<string>;\n\n // Selection state\n selectedWidgetId: string | null;\n hoveredWidgetId: string | null;\n\n // Edit mode\n isEditMode: boolean;\n isDragging: boolean;\n isResizing: boolean;\n\n // Panels\n configPanelOpen: boolean;\n addWidgetDialogOpen: boolean;\n widgetTypeSelectorOpen: boolean;\n\n // Clipboard\n clipboardWidgetId: string | null;\n clipboardAction: 'copy' | 'cut' | null;\n\n // Grid layout (for react-grid-layout)\n gridLayout: GridLayoutItem[];\n\n /**\n * Counts the placements a PERSON made: bumped by `updateWidgetPosition` and by\n * nothing else. The canvas calls that action only when a drag or a resize ENDS\n * (`DashboardCanvas.handleLayoutChange`), and the inspector's \"Position & size\"\n * fields call it only on a typed change that actually moves the widget\n * (`LayoutSection.commit`). Loading (`setWidgets`), creating (`addWidget`),\n * saving (`updateWidget`) and deleting (`removeWidget`) leave it alone, so a\n * reader can tell \"the person moved something\" from \"the list was refreshed\"\n * without comparing positions (BOFF-8060, the dashboard leave guard).\n * Never reset: a reader keeps the last value it saw across `reset()`.\n */\n layoutEditRevision: number;\n\n // Filter state\n typeFilter: WidgetType | null;\n\n // Actions\n setWidgets: (widgets: Widget[]) => void;\n addWidget: (widget: Widget) => void;\n updateWidget: (id: string, updates: Partial<Widget>) => void;\n removeWidget: (id: string) => void;\n\n // Selection actions\n selectWidget: (id: string | null) => void;\n hoverWidget: (id: string | null) => void;\n\n // Edit mode actions\n setEditMode: (enabled: boolean) => void;\n toggleEditMode: () => void;\n setDragging: (dragging: boolean) => void;\n setResizing: (resizing: boolean) => void;\n\n // Panel actions\n openConfigPanel: () => void;\n closeConfigPanel: () => void;\n toggleConfigPanel: () => void;\n openAddWidgetDialog: () => void;\n closeAddWidgetDialog: () => void;\n openWidgetTypeSelector: () => void;\n closeWidgetTypeSelector: () => void;\n\n // Clipboard actions\n copyWidget: (id: string) => void;\n cutWidget: (id: string) => void;\n pasteWidget: () => void;\n clearClipboard: () => void;\n\n // Position actions\n updateWidgetPosition: (id: string, position: WidgetPosition) => void;\n setGridLayout: (layout: GridLayoutItem[]) => void;\n\n // UI state actions\n setWidgetLoading: (id: string, loading: boolean) => void;\n setWidgetError: (id: string, error: string | null) => void;\n\n // Filter actions\n setTypeFilter: (type: WidgetType | null) => void;\n\n // Dirty tracking actions\n markWidgetDirty: (id: string) => void;\n markWidgetClean: (id: string) => void;\n clearAllDirty: () => void;\n getDirtyWidgets: () => Widget[];\n\n // Bulk actions\n clearSelection: () => void;\n reset: () => void;\n}\n\n// ============================================================================\n// Initial State\n// ============================================================================\n\nconst initialState = {\n widgets: new Map<string, Widget>(),\n widgetUIStates: new Map<string, WidgetUIState>(),\n dirtyWidgetIds: new Set<string>(),\n selectedWidgetId: null,\n hoveredWidgetId: null,\n isEditMode: false,\n isDragging: false,\n isResizing: false,\n configPanelOpen: false,\n addWidgetDialogOpen: false,\n widgetTypeSelectorOpen: false,\n clipboardWidgetId: null,\n clipboardAction: null,\n gridLayout: [],\n typeFilter: null,\n};\n\n// ============================================================================\n// Store\n// ============================================================================\n\nexport const useWidgetStore = create<WidgetState>()(\n devtools(\n persist(\n (set, get) => ({\n ...initialState,\n // Not in `initialState` on purpose: `reset()` must not rewind it (see the field).\n layoutEditRevision: 0,\n\n // Widget data actions\n setWidgets: (widgets) =>\n set({\n widgets: new Map(widgets.map((w) => [w.id, w])),\n gridLayout: widgets.map((w) => ({\n i: w.id,\n x: w.positionX,\n y: w.positionY,\n w: w.positionWidth,\n h: w.positionHeight,\n minW: 2,\n minH: 2,\n })),\n }),\n\n addWidget: (widget) =>\n set((state) => {\n // Normalize position fields\n const normalizedWidget = {\n ...widget,\n positionX: widget.positionX ?? widget.position?.x ?? 0,\n positionY: widget.positionY ?? widget.position?.y ?? 0,\n positionWidth: widget.positionWidth ?? widget.position?.width ?? 4,\n positionHeight: widget.positionHeight ?? widget.position?.height ?? 4,\n };\n const newWidgets = new Map(state.widgets);\n newWidgets.set(widget.id, normalizedWidget);\n return {\n widgets: newWidgets,\n gridLayout: [\n ...state.gridLayout,\n {\n i: widget.id,\n x: normalizedWidget.positionX,\n y: normalizedWidget.positionY,\n w: normalizedWidget.positionWidth,\n h: normalizedWidget.positionHeight,\n minW: 2,\n minH: 2,\n },\n ],\n };\n }),\n\n updateWidget: (id, updates) =>\n set((state) => {\n const newWidgets = new Map(state.widgets);\n const existing = newWidgets.get(id);\n if (existing) {\n newWidgets.set(id, { ...existing, ...updates });\n }\n return { widgets: newWidgets };\n }),\n\n removeWidget: (id) =>\n set((state) => {\n const newWidgets = new Map(state.widgets);\n newWidgets.delete(id);\n const newUIStates = new Map(state.widgetUIStates);\n newUIStates.delete(id);\n return {\n widgets: newWidgets,\n widgetUIStates: newUIStates,\n gridLayout: state.gridLayout.filter((item) => item.i !== id),\n selectedWidgetId: state.selectedWidgetId === id ? null : state.selectedWidgetId,\n };\n }),\n\n // Selection actions\n selectWidget: (id) =>\n set({\n selectedWidgetId: id,\n configPanelOpen: id !== null,\n }),\n\n hoverWidget: (id) => set({ hoveredWidgetId: id }),\n\n // Edit mode actions\n setEditMode: (enabled) => set({ isEditMode: enabled }),\n toggleEditMode: () => set((state) => ({ isEditMode: !state.isEditMode })),\n setDragging: (dragging) => set({ isDragging: dragging }),\n setResizing: (resizing) => set({ isResizing: resizing }),\n\n // Panel actions\n openConfigPanel: () => set({ configPanelOpen: true }),\n closeConfigPanel: () => set({ configPanelOpen: false }),\n toggleConfigPanel: () => set((state) => ({ configPanelOpen: !state.configPanelOpen })),\n openAddWidgetDialog: () => set({ addWidgetDialogOpen: true }),\n closeAddWidgetDialog: () => set({ addWidgetDialogOpen: false }),\n openWidgetTypeSelector: () => set({ widgetTypeSelectorOpen: true }),\n closeWidgetTypeSelector: () => set({ widgetTypeSelectorOpen: false }),\n\n // Clipboard actions\n copyWidget: (id) => set({ clipboardWidgetId: id, clipboardAction: 'copy' }),\n cutWidget: (id) => set({ clipboardWidgetId: id, clipboardAction: 'cut' }),\n pasteWidget: () =>\n set((state) => {\n const { clipboardWidgetId, clipboardAction, widgets } = state;\n if (!clipboardWidgetId) return state;\n\n const sourceWidget = widgets.get(clipboardWidgetId);\n if (!sourceWidget) return state;\n\n // Create a copy of the widget with new ID and offset position\n const newWidget: Widget = {\n ...sourceWidget,\n id: crypto.randomUUID(),\n title: `${sourceWidget.title} (Copy)`,\n positionX: (sourceWidget.positionX ?? 0) + 1,\n positionY: (sourceWidget.positionY ?? 0) + 1,\n position: sourceWidget.position\n ? {\n ...sourceWidget.position,\n x: (sourceWidget.position.x ?? 0) + 1,\n y: (sourceWidget.position.y ?? 0) + 1,\n }\n : { x: 0, y: 0, width: 4, height: 3 },\n createdAt: new Date().toISOString(),\n updatedAt: new Date().toISOString(),\n };\n\n const newWidgets = new Map(widgets);\n newWidgets.set(newWidget.id, newWidget);\n\n // If cut, remove the original\n if (clipboardAction === 'cut') {\n newWidgets.delete(clipboardWidgetId);\n }\n\n return {\n widgets: newWidgets,\n clipboardWidgetId: clipboardAction === 'cut' ? null : clipboardWidgetId,\n clipboardAction: clipboardAction === 'cut' ? null : clipboardAction,\n gridLayout: [\n ...state.gridLayout.filter((item) => clipboardAction !== 'cut' || item.i !== clipboardWidgetId),\n {\n i: newWidget.id,\n x: newWidget.positionX ?? 0,\n y: newWidget.positionY ?? 0,\n w: newWidget.positionWidth ?? 4,\n h: newWidget.positionHeight ?? 4,\n minW: 2,\n minH: 2,\n },\n ],\n };\n }),\n clearClipboard: () => set({ clipboardWidgetId: null, clipboardAction: null }),\n\n // Position actions\n updateWidgetPosition: (id, position) =>\n set((state) => {\n const newWidgets = new Map(state.widgets);\n const existing = newWidgets.get(id);\n if (existing) {\n newWidgets.set(id, {\n ...existing,\n position,\n positionX: position.x,\n positionY: position.y,\n positionWidth: position.width,\n positionHeight: position.height,\n });\n }\n return {\n widgets: newWidgets,\n gridLayout: state.gridLayout.map((item) =>\n item.i === id\n ? {\n ...item,\n x: position.x,\n y: position.y,\n w: position.width,\n h: position.height,\n }\n : item\n ),\n layoutEditRevision: state.layoutEditRevision + 1,\n };\n }),\n\n setGridLayout: (layout) => set({ gridLayout: layout }),\n\n // UI state actions\n setWidgetLoading: (id, loading) =>\n set((state) => {\n const newUIStates = new Map(state.widgetUIStates);\n const existing = newUIStates.get(id) || {\n isLoading: false,\n isError: false,\n };\n newUIStates.set(id, { ...existing, isLoading: loading });\n return { widgetUIStates: newUIStates };\n }),\n\n setWidgetError: (id, error) =>\n set((state) => {\n const newUIStates = new Map(state.widgetUIStates);\n const existing = newUIStates.get(id) || {\n isLoading: false,\n isError: false,\n };\n newUIStates.set(id, {\n ...existing,\n isError: error !== null,\n errorMessage: error || undefined,\n });\n return { widgetUIStates: newUIStates };\n }),\n\n // Filter actions\n setTypeFilter: (type) => set({ typeFilter: type }),\n\n // Dirty tracking actions\n markWidgetDirty: (id) =>\n set((state) => {\n const newDirty = new Set(state.dirtyWidgetIds);\n newDirty.add(id);\n return { dirtyWidgetIds: newDirty };\n }),\n\n markWidgetClean: (id) =>\n set((state) => {\n const newDirty = new Set(state.dirtyWidgetIds);\n newDirty.delete(id);\n return { dirtyWidgetIds: newDirty };\n }),\n\n clearAllDirty: () => set({ dirtyWidgetIds: new Set<string>() }),\n\n getDirtyWidgets: () => {\n const state = get();\n return Array.from(state.dirtyWidgetIds)\n .map((id) => state.widgets.get(id))\n .filter((w): w is Widget => w !== undefined);\n },\n\n // Bulk actions\n clearSelection: () =>\n set({\n selectedWidgetId: null,\n hoveredWidgetId: null,\n configPanelOpen: false,\n }),\n\n reset: () => set(initialState),\n }),\n {\n name: 'bigconsole-widget-store',\n storage: createProfileScopedStorage(),\n partialize: (state) => ({\n // Only persist UI preferences, not data\n isEditMode: state.isEditMode,\n typeFilter: state.typeFilter,\n }),\n }\n ),\n { name: 'WidgetStore' }\n )\n);\n\n// ============================================================================\n// Selectors\n// ============================================================================\n\nexport const useSelectedWidget = () => {\n const selectedWidgetId = useWidgetStore((state) => state.selectedWidgetId);\n const widgets = useWidgetStore((state) => state.widgets);\n return selectedWidgetId ? widgets.get(selectedWidgetId) : null;\n};\n\nexport const useWidgetById = (id: string) => {\n return useWidgetStore((state) => state.widgets.get(id));\n};\n\n// Stable default UI state to prevent infinite re-renders\nconst DEFAULT_UI_STATE: WidgetUIState = { isLoading: false, isError: false };\n\nexport const useWidgetUIState = (id: string) => {\n return useWidgetStore((state) => state.widgetUIStates.get(id) || DEFAULT_UI_STATE);\n};\n\nexport const useWidgetsArray = () => {\n return useWidgetStore((state) => Array.from(state.widgets.values()));\n};\n\nexport const useFilteredWidgets = () => {\n const widgets = useWidgetStore((state) => Array.from(state.widgets.values()));\n const typeFilter = useWidgetStore((state) => state.typeFilter);\n\n if (!typeFilter) return widgets;\n return widgets.filter((w) => w.type === typeFilter);\n};\n\nexport const useIsWidgetSelected = (id: string) => {\n return useWidgetStore((state) => state.selectedWidgetId === id);\n};\n\nexport const useHasClipboard = () => {\n return useWidgetStore((state) => state.clipboardWidgetId !== null);\n};\n\nexport const useHasDirtyWidgets = () => {\n return useWidgetStore((state) => state.dirtyWidgetIds.size > 0);\n};\n\nexport const useDirtyWidgetIds = () => {\n return useWidgetStore((state) => state.dirtyWidgetIds);\n};\n"],"mappings":"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"}
@@ -0,0 +1,83 @@
1
+ /**
2
+ * Form dirty helpers (BOFF-8060, N3 leave guard).
3
+ *
4
+ * Pure functions, no React, so the "is this editor holding an unsaved draft"
5
+ * decision is testable without a DOM. Each editor keeps a baseline of the
6
+ * values it seeded or last saved, and reports `isDirty(current, baseline)` to
7
+ * `useUnsavedChanges`.
8
+ *
9
+ * Two rules keep the signal honest:
10
+ * - A missing baseline (`null`/`undefined`) is CLEAN. An editor whose seed has
11
+ * not landed yet has nothing to lose, and reporting dirty there would make a
12
+ * freshly opened page refuse the assistant's navigation.
13
+ * - Comparison is by value (stable JSON, sorted keys, `undefined` dropped), so
14
+ * object identity, key order and a refetch that returns equal data never
15
+ * count as an edit.
16
+ */
17
+ /** JSON with object keys sorted at every depth, so key order never matters. */
18
+ export declare function stableStringify(value: unknown): string;
19
+ /**
20
+ * True when `current` differs by value from `baseline`. A missing baseline is
21
+ * clean (see the module note).
22
+ */
23
+ export declare function isDirty(current: unknown, baseline: unknown): boolean;
24
+ /**
25
+ * Whether an edit form should be (re)seeded from the record the store now holds.
26
+ *
27
+ * A record can arrive twice: first as a partial list item (no expression, ttl, version), then as
28
+ * the full record after the detail fetch. The second arrival must refresh a form the person has
29
+ * not touched, otherwise the partial values would be saved over the stored ones. It must never
30
+ * overwrite an edit in progress.
31
+ * - `seededId`: the id of the record the form was last seeded from (null = never seeded);
32
+ * - `form` / `baseline`: the current form and the form as last seeded or saved.
33
+ * Seed when the record is a different one, or when the form still equals its baseline (a missing
34
+ * baseline counts as clean). Skip only when the same record is loaded and the form is dirty.
35
+ */
36
+ export declare function shouldReseedForm(seededId: string | null, recordId: string, form: unknown, baseline: unknown): boolean;
37
+ /** A grid item: react-grid-layout names the id `i`, our own widgets use `id`. */
38
+ export type LayoutItem = {
39
+ readonly id: string;
40
+ readonly x: number;
41
+ readonly y: number;
42
+ readonly w: number;
43
+ readonly h: number;
44
+ } | {
45
+ readonly i: string;
46
+ readonly x: number;
47
+ readonly y: number;
48
+ readonly w: number;
49
+ readonly h: number;
50
+ };
51
+ /** Item id to `x,y,w,h`, keys in sorted order. */
52
+ export type NormalisedLayout = Readonly<Record<string, string>>;
53
+ /**
54
+ * Reduces a layout to the four numbers per item that a person can change by
55
+ * dragging or resizing. Array order, extra grid bookkeeping fields (`moved`,
56
+ * `static`, min/max) and item order never matter.
57
+ */
58
+ export declare function normaliseLayout(items: readonly LayoutItem[]): NormalisedLayout;
59
+ type LayoutInput = readonly LayoutItem[] | NormalisedLayout;
60
+ /** True when the layout differs by value from the baseline. No baseline is clean. */
61
+ export declare function layoutDirty(current: LayoutInput, baseline: LayoutInput | null | undefined): boolean;
62
+ /**
63
+ * react-grid-layout fires `onLayoutChange` on mount and again whenever it
64
+ * compacts, with no person involved, so "a change event happened" is not a
65
+ * dirty signal. The baseline therefore FLOATS (follows every layout change)
66
+ * until the first user gesture (drag or resize stop), then FREEZES; from then on
67
+ * `layoutDirty(current, tracker.baseline)` is the answer. Reloading the
68
+ * dashboard or a successful save starts a fresh tracker; a failed save keeps
69
+ * the old one, so the draft stays dirty.
70
+ */
71
+ export interface LayoutTracker {
72
+ readonly baseline: NormalisedLayout | null;
73
+ readonly frozen: boolean;
74
+ }
75
+ export declare const EMPTY_LAYOUT_TRACKER: LayoutTracker;
76
+ /** A fresh floating tracker, for after a load or a successful save. */
77
+ export declare function resetLayoutTracker(items: readonly LayoutItem[]): LayoutTracker;
78
+ /** A layout-change event: moves the baseline while floating, ignored once frozen. */
79
+ export declare function trackLayoutChange(tracker: LayoutTracker, items: readonly LayoutItem[]): LayoutTracker;
80
+ /** The first user gesture: stops the baseline following layout changes. */
81
+ export declare function freezeLayoutTracker(tracker: LayoutTracker): LayoutTracker;
82
+ export {};
83
+ //# sourceMappingURL=formDirty.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"formDirty.d.ts","sourceRoot":"","sources":["../../../src/bigconsole/utils/formDirty.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAEH,+EAA+E;AAC/E,wBAAgB,eAAe,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,CAEtD;AAkBD;;;GAGG;AACH,wBAAgB,OAAO,CAAC,OAAO,EAAE,OAAO,EAAE,QAAQ,EAAE,OAAO,GAAG,OAAO,CAKpE;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,gBAAgB,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI,EAAE,QAAQ,EAAE,MAAM,EAAE,IAAI,EAAE,OAAO,EAAE,QAAQ,EAAE,OAAO,GAAG,OAAO,CAKrH;AAMD,iFAAiF;AACjF,MAAM,MAAM,UAAU,GAClB;IAAE,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,CAAC,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,CAAC,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,CAAC,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,CAAC,EAAE,MAAM,CAAA;CAAE,GACvG;IAAE,QAAQ,CAAC,CAAC,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,CAAC,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,CAAC,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,CAAC,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC;AAE3G,kDAAkD;AAClD,MAAM,MAAM,gBAAgB,GAAG,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC;AAMhE;;;;GAIG;AACH,wBAAgB,eAAe,CAAC,KAAK,EAAE,SAAS,UAAU,EAAE,GAAG,gBAAgB,CAK9E;AAED,KAAK,WAAW,GAAG,SAAS,UAAU,EAAE,GAAG,gBAAgB,CAAC;AAU5D,qFAAqF;AACrF,wBAAgB,WAAW,CAAC,OAAO,EAAE,WAAW,EAAE,QAAQ,EAAE,WAAW,GAAG,IAAI,GAAG,SAAS,GAAG,OAAO,CAKnG;AAMD;;;;;;;;GAQG;AACH,MAAM,WAAW,aAAa;IAC5B,QAAQ,CAAC,QAAQ,EAAE,gBAAgB,GAAG,IAAI,CAAC;IAC3C,QAAQ,CAAC,MAAM,EAAE,OAAO,CAAC;CAC1B;AAED,eAAO,MAAM,oBAAoB,EAAE,aAAiD,CAAC;AAErF,uEAAuE;AACvE,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,SAAS,UAAU,EAAE,GAAG,aAAa,CAE9E;AAED,qFAAqF;AACrF,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,aAAa,EAAE,KAAK,EAAE,SAAS,UAAU,EAAE,GAAG,aAAa,CAKrG;AAED,2EAA2E;AAC3E,wBAAgB,mBAAmB,CAAC,OAAO,EAAE,aAAa,GAAG,aAAa,CAKzE"}
@@ -0,0 +1,23 @@
1
+ //#region src/bigconsole/utils/formDirty.ts
2
+ function e(e) {
3
+ return JSON.stringify(t(e));
4
+ }
5
+ function t(e) {
6
+ if (Array.isArray(e)) return e.map(t);
7
+ if (typeof e == "object" && e) {
8
+ let n = e, r = {};
9
+ for (let e of Object.keys(n).sort()) r[e] = t(n[e]);
10
+ return r;
11
+ }
12
+ return e;
13
+ }
14
+ function n(t, n) {
15
+ return n == null ? !1 : e(t) !== e(n);
16
+ }
17
+ function r(e, t, r, i) {
18
+ return e === t ? !n(r, i) : !0;
19
+ }
20
+ //#endregion
21
+ export { n as isDirty, r as shouldReseedForm };
22
+
23
+ //# sourceMappingURL=formDirty.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"formDirty.js","names":[],"sources":["../../../src/bigconsole/utils/formDirty.ts"],"sourcesContent":["/**\n * Form dirty helpers (BOFF-8060, N3 leave guard).\n *\n * Pure functions, no React, so the \"is this editor holding an unsaved draft\"\n * decision is testable without a DOM. Each editor keeps a baseline of the\n * values it seeded or last saved, and reports `isDirty(current, baseline)` to\n * `useUnsavedChanges`.\n *\n * Two rules keep the signal honest:\n * - A missing baseline (`null`/`undefined`) is CLEAN. An editor whose seed has\n * not landed yet has nothing to lose, and reporting dirty there would make a\n * freshly opened page refuse the assistant's navigation.\n * - Comparison is by value (stable JSON, sorted keys, `undefined` dropped), so\n * object identity, key order and a refetch that returns equal data never\n * count as an edit.\n */\n\n/** JSON with object keys sorted at every depth, so key order never matters. */\nexport function stableStringify(value: unknown): string {\n return JSON.stringify(sortKeys(value));\n}\n\nfunction sortKeys(value: unknown): unknown {\n if (Array.isArray(value)) {\n return value.map(sortKeys);\n }\n if (value !== null && typeof value === 'object') {\n // Narrowed to a non-null, non-array object above; JSON-shaped form state only.\n const source = value as Record<string, unknown>;\n const sorted: Record<string, unknown> = {};\n for (const key of Object.keys(source).sort()) {\n sorted[key] = sortKeys(source[key]);\n }\n return sorted;\n }\n return value;\n}\n\n/**\n * True when `current` differs by value from `baseline`. A missing baseline is\n * clean (see the module note).\n */\nexport function isDirty(current: unknown, baseline: unknown): boolean {\n if (baseline === null || baseline === undefined) {\n return false;\n }\n return stableStringify(current) !== stableStringify(baseline);\n}\n\n/**\n * Whether an edit form should be (re)seeded from the record the store now holds.\n *\n * A record can arrive twice: first as a partial list item (no expression, ttl, version), then as\n * the full record after the detail fetch. The second arrival must refresh a form the person has\n * not touched, otherwise the partial values would be saved over the stored ones. It must never\n * overwrite an edit in progress.\n * - `seededId`: the id of the record the form was last seeded from (null = never seeded);\n * - `form` / `baseline`: the current form and the form as last seeded or saved.\n * Seed when the record is a different one, or when the form still equals its baseline (a missing\n * baseline counts as clean). Skip only when the same record is loaded and the form is dirty.\n */\nexport function shouldReseedForm(seededId: string | null, recordId: string, form: unknown, baseline: unknown): boolean {\n if (seededId !== recordId) {\n return true;\n }\n return !isDirty(form, baseline);\n}\n\n// ============================================================================\n// GRID LAYOUT\n// ============================================================================\n\n/** A grid item: react-grid-layout names the id `i`, our own widgets use `id`. */\nexport type LayoutItem =\n | { readonly id: string; readonly x: number; readonly y: number; readonly w: number; readonly h: number }\n | { readonly i: string; readonly x: number; readonly y: number; readonly w: number; readonly h: number };\n\n/** Item id to `x,y,w,h`, keys in sorted order. */\nexport type NormalisedLayout = Readonly<Record<string, string>>;\n\nfunction layoutItemId(item: LayoutItem): string {\n return 'id' in item ? item.id : item.i;\n}\n\n/**\n * Reduces a layout to the four numbers per item that a person can change by\n * dragging or resizing. Array order, extra grid bookkeeping fields (`moved`,\n * `static`, min/max) and item order never matter.\n */\nexport function normaliseLayout(items: readonly LayoutItem[]): NormalisedLayout {\n const entries = items\n .map((item) => [layoutItemId(item), `${item.x},${item.y},${item.w},${item.h}`] as const)\n .sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0));\n return Object.fromEntries(entries);\n}\n\ntype LayoutInput = readonly LayoutItem[] | NormalisedLayout;\n\nfunction isLayoutItems(layout: LayoutInput): layout is readonly LayoutItem[] {\n return Array.isArray(layout);\n}\n\nfunction toNormalised(layout: LayoutInput): NormalisedLayout {\n return isLayoutItems(layout) ? normaliseLayout(layout) : layout;\n}\n\n/** True when the layout differs by value from the baseline. No baseline is clean. */\nexport function layoutDirty(current: LayoutInput, baseline: LayoutInput | null | undefined): boolean {\n if (baseline === null || baseline === undefined) {\n return false;\n }\n return isDirty(toNormalised(current), toNormalised(baseline));\n}\n\n// ============================================================================\n// LAYOUT BASELINE: FLOAT, THEN FREEZE\n// ============================================================================\n\n/**\n * react-grid-layout fires `onLayoutChange` on mount and again whenever it\n * compacts, with no person involved, so \"a change event happened\" is not a\n * dirty signal. The baseline therefore FLOATS (follows every layout change)\n * until the first user gesture (drag or resize stop), then FREEZES; from then on\n * `layoutDirty(current, tracker.baseline)` is the answer. Reloading the\n * dashboard or a successful save starts a fresh tracker; a failed save keeps\n * the old one, so the draft stays dirty.\n */\nexport interface LayoutTracker {\n readonly baseline: NormalisedLayout | null;\n readonly frozen: boolean;\n}\n\nexport const EMPTY_LAYOUT_TRACKER: LayoutTracker = { baseline: null, frozen: false };\n\n/** A fresh floating tracker, for after a load or a successful save. */\nexport function resetLayoutTracker(items: readonly LayoutItem[]): LayoutTracker {\n return { baseline: normaliseLayout(items), frozen: false };\n}\n\n/** A layout-change event: moves the baseline while floating, ignored once frozen. */\nexport function trackLayoutChange(tracker: LayoutTracker, items: readonly LayoutItem[]): LayoutTracker {\n if (tracker.frozen) {\n return tracker;\n }\n return { baseline: normaliseLayout(items), frozen: false };\n}\n\n/** The first user gesture: stops the baseline following layout changes. */\nexport function freezeLayoutTracker(tracker: LayoutTracker): LayoutTracker {\n if (tracker.frozen || tracker.baseline === null) {\n return tracker;\n }\n return { baseline: tracker.baseline, frozen: true };\n}\n"],"mappings":";AAkBA,SAAgB,EAAgB,GAAwB;AACtD,QAAO,KAAK,UAAU,EAAS,EAAM,CAAC;;AAGxC,SAAS,EAAS,GAAyB;AACzC,KAAI,MAAM,QAAQ,EAAM,CACtB,QAAO,EAAM,IAAI,EAAS;AAE5B,KAAsB,OAAO,KAAU,YAAnC,GAA6C;EAE/C,IAAM,IAAS,GACT,IAAkC,EAAE;AAC1C,OAAK,IAAM,KAAO,OAAO,KAAK,EAAO,CAAC,MAAM,CAC1C,GAAO,KAAO,EAAS,EAAO,GAAK;AAErC,SAAO;;AAET,QAAO;;AAOT,SAAgB,EAAQ,GAAkB,GAA4B;AAIpE,QAHI,KAAa,OACR,KAEF,EAAgB,EAAQ,KAAK,EAAgB,EAAS;;AAe/D,SAAgB,EAAiB,GAAyB,GAAkB,GAAe,GAA4B;AAIrH,QAHI,MAAa,IAGV,CAAC,EAAQ,GAAM,EAAS,GAFtB"}
@@ -0,0 +1,30 @@
1
+ /**
2
+ * Does the Insights page hold a description the person has typed and not submitted?
3
+ * (BOFF-8060, N3/N4 leave guard.)
4
+ *
5
+ * The page has two draft textareas: the "Describe your dashboard" prompt and the refine
6
+ * modal's "What should change?". Both are cleared by the page itself when their
7
+ * submission is accepted (and when the refine modal is reopened), so the last submitted
8
+ * value is always the empty string and "unsubmitted" reads as "non-empty". Pure
9
+ * functions, no React, so the rule is testable without a DOM.
10
+ *
11
+ * - Whitespace-only text is clean: the page will not submit it, and nothing is lost.
12
+ * - Text being submitted right now is clean: it already left the page (the mutation
13
+ * keeps running after the person navigates). If the submission fails the page keeps
14
+ * the text and clears the in-flight flag, so the draft reads dirty again.
15
+ * - The refine text only counts while the refine modal is open.
16
+ */
17
+ export interface InsightsDraftState {
18
+ /** The "Describe your dashboard" textarea. */
19
+ readonly promptText: string;
20
+ /** A generation request for `promptText` is in flight. */
21
+ readonly isGenerating: boolean;
22
+ /** The refine modal is open. */
23
+ readonly refineOpen: boolean;
24
+ /** The refine modal's "What should change?" textarea. */
25
+ readonly refineText: string;
26
+ /** A refine request for the open generation is in flight. */
27
+ readonly refineBusy: boolean;
28
+ }
29
+ export declare function isInsightsDraftDirty(state: InsightsDraftState): boolean;
30
+ //# sourceMappingURL=insightsDraft.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"insightsDraft.d.ts","sourceRoot":"","sources":["../../../src/bigconsole/utils/insightsDraft.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAEH,MAAM,WAAW,kBAAkB;IACjC,8CAA8C;IAC9C,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,0DAA0D;IAC1D,QAAQ,CAAC,YAAY,EAAE,OAAO,CAAC;IAC/B,gCAAgC;IAChC,QAAQ,CAAC,UAAU,EAAE,OAAO,CAAC;IAC7B,yDAAyD;IACzD,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,6DAA6D;IAC7D,QAAQ,CAAC,UAAU,EAAE,OAAO,CAAC;CAC9B;AAID,wBAAgB,oBAAoB,CAAC,KAAK,EAAE,kBAAkB,GAAG,OAAO,CAIvE"}
@@ -0,0 +1,10 @@
1
+ //#region src/bigconsole/utils/insightsDraft.ts
2
+ var e = (e) => e.trim() !== "";
3
+ function t(t) {
4
+ let n = e(t.promptText) && !t.isGenerating, r = t.refineOpen && e(t.refineText) && !t.refineBusy;
5
+ return n || r;
6
+ }
7
+ //#endregion
8
+ export { t as isInsightsDraftDirty };
9
+
10
+ //# sourceMappingURL=insightsDraft.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"insightsDraft.js","names":[],"sources":["../../../src/bigconsole/utils/insightsDraft.ts"],"sourcesContent":["/**\n * Does the Insights page hold a description the person has typed and not submitted?\n * (BOFF-8060, N3/N4 leave guard.)\n *\n * The page has two draft textareas: the \"Describe your dashboard\" prompt and the refine\n * modal's \"What should change?\". Both are cleared by the page itself when their\n * submission is accepted (and when the refine modal is reopened), so the last submitted\n * value is always the empty string and \"unsubmitted\" reads as \"non-empty\". Pure\n * functions, no React, so the rule is testable without a DOM.\n *\n * - Whitespace-only text is clean: the page will not submit it, and nothing is lost.\n * - Text being submitted right now is clean: it already left the page (the mutation\n * keeps running after the person navigates). If the submission fails the page keeps\n * the text and clears the in-flight flag, so the draft reads dirty again.\n * - The refine text only counts while the refine modal is open.\n */\n\nexport interface InsightsDraftState {\n /** The \"Describe your dashboard\" textarea. */\n readonly promptText: string;\n /** A generation request for `promptText` is in flight. */\n readonly isGenerating: boolean;\n /** The refine modal is open. */\n readonly refineOpen: boolean;\n /** The refine modal's \"What should change?\" textarea. */\n readonly refineText: string;\n /** A refine request for the open generation is in flight. */\n readonly refineBusy: boolean;\n}\n\nconst hasText = (text: string): boolean => text.trim() !== '';\n\nexport function isInsightsDraftDirty(state: InsightsDraftState): boolean {\n const promptUnsubmitted = hasText(state.promptText) && !state.isGenerating;\n const refineUnsubmitted = state.refineOpen && hasText(state.refineText) && !state.refineBusy;\n return promptUnsubmitted || refineUnsubmitted;\n}\n"],"mappings":";AA8BA,IAAM,KAAW,MAA0B,EAAK,MAAM,KAAK;AAE3D,SAAgB,EAAqB,GAAoC;CACvE,IAAM,IAAoB,EAAQ,EAAM,WAAW,IAAI,CAAC,EAAM,cACxD,IAAoB,EAAM,cAAc,EAAQ,EAAM,WAAW,IAAI,CAAC,EAAM;AAClF,QAAO,KAAqB"}