@burdenoff/microfe-bigconsole 2026.613.5 → 2026.613.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -8,64 +8,65 @@ import "../../context/index.js";
8
8
  import { getWidgetDefinition as i } from "../widgets/WidgetRegistry.js";
9
9
  import a from "../../hooks/useWidgetCatalog.js";
10
10
  import o from "../../hooks/useWidgetOperations.js";
11
- import { useInstalledWidgets as s } from "../../hooks/useInstalledWidgets.js";
12
- import c from "./DashboardGrid.js";
13
- import l from "./DashboardToolbar.js";
14
- import ee from "./GlobalFiltersBar/GlobalFiltersBar.js";
11
+ import s from "../../hooks/useDashboardImageExport.js";
12
+ import { useInstalledWidgets as c } from "../../hooks/useInstalledWidgets.js";
13
+ import l from "./DashboardGrid.js";
14
+ import ee from "./DashboardToolbar.js";
15
+ import te from "./GlobalFiltersBar/GlobalFiltersBar.js";
15
16
  import "./GlobalFiltersBar/index.js";
16
17
  import u from "./PageTabsManager/PageTabsManager.js";
17
18
  import "./PageTabsManager/index.js";
18
- import d from "./ShareDialog/ShareDialog.js";
19
+ import ne from "./ShareDialog/ShareDialog.js";
19
20
  import "./ShareDialog/index.js";
20
- import te from "./WidgetPalette.js";
21
- import ne from "../widgets/WidgetConfigDialog.js";
22
- import re from "../widgets/PropertiesPanel.js";
23
- import { calculatePositionFromDrop as ie, findNextAvailablePosition as ae } from "../../utils/widgetPositioning.js";
24
- import { memo as f, useCallback as p, useEffect as oe, useMemo as se, useRef as ce, useState as m } from "react";
25
- import { jsx as h, jsxs as g } from "react/jsx-runtime";
21
+ import re from "./WidgetPalette.js";
22
+ import ie from "../widgets/WidgetConfigDialog.js";
23
+ import ae from "../widgets/PropertiesPanel.js";
24
+ import { calculatePositionFromDrop as oe, findNextAvailablePosition as se } from "../../utils/widgetPositioning.js";
25
+ import { memo as d, useCallback as f, useEffect as p, useMemo as ce, useRef as m, useState as h } from "react";
26
+ import { jsx as g, jsxs as _ } from "react/jsx-runtime";
26
27
  //#region src/bigconsole/components/dashboard/DashboardCanvas.tsx
27
- var _ = f(function({ dashboardId: f, pageId: _, title: v, onPaste: le, onSave: ue, isSaving: de = !1, onExport: y, pages: b = [], activePageId: fe, onPageChange: x, onPagesReorder: S, onPageAdd: C, onPageRename: w, onPageDuplicate: pe, onPageDelete: me, showFilters: he = !0, showPageTabs: ge = !0, enableCrossFilter: _e = !0 }) {
28
- let T = ce(null), [E, ve] = m(1200), [ye, D] = m(!1), [be, O] = m(!1), [k, A] = m(!1), [j, M] = m(!1), [N, P] = m(null), [F, I] = m(null), [xe, L] = m(null), [R, z] = m(null), B = t((e) => e.viewMode === "edit"), { workspaceId: V, apiGatewayUrl: H, authToken: U } = r(), { installedWidgets: W, loading: Se } = s({
29
- workspaceId: V,
30
- apiGatewayUrl: H,
31
- authToken: U,
32
- enabled: B
33
- }), { definitions: Ce } = a({
34
- workspaceId: V,
35
- apiGatewayUrl: H,
36
- authToken: U,
37
- enabled: B
38
- }), G = t((e) => e.zoomLevel), K = t((e) => e.setGlobalFilterValues), we = p((e) => {
39
- K(e);
40
- }, [K]), { createWidget: q } = o(_, f, { skipFetch: !0 }), J = e((e) => e.widgets), Y = se(() => {
41
- let e = Array.from(J.values());
42
- return _ ? e.filter((e) => e.pageId === _) : e;
43
- }, [J, _]), Te = e((e) => e.selectedWidgetId), X = e((e) => e.selectWidget), Z = e((e) => e.addWidget), Q = e((e) => e.updateWidgetPosition), Ee = e((e) => e.configPanelOpen), De = e((e) => e.closeConfigPanel);
44
- oe(() => {
28
+ var v = d(function({ dashboardId: d, pageId: v, title: y, onPaste: le, onSave: ue, isSaving: de = !1, onExport: fe, pages: b = [], activePageId: pe, onPageChange: me, onPagesReorder: x, onPageAdd: he, onPageRename: ge, onPageDuplicate: S, onPageDelete: _e, showFilters: ve = !0, showPageTabs: ye = !0, enableCrossFilter: be = !0 }) {
29
+ let C = m(null), w = m(null), [T, xe] = h(1200), [Se, E] = h(!1), [Ce, D] = h(!1), [O, k] = h(!1), [A, j] = h(!1), [we, M] = h(null), [N, P] = h(null), [Te, F] = h(null), [I, L] = h(null), R = t((e) => e.viewMode === "edit"), { workspaceId: z, apiGatewayUrl: B, authToken: V } = r(), { installedWidgets: H, loading: Ee } = c({
30
+ workspaceId: z,
31
+ apiGatewayUrl: B,
32
+ authToken: V,
33
+ enabled: R
34
+ }), { definitions: De } = a({
35
+ workspaceId: z,
36
+ apiGatewayUrl: B,
37
+ authToken: V,
38
+ enabled: R
39
+ }), U = t((e) => e.zoomLevel), W = t((e) => e.setGlobalFilterValues), Oe = f((e) => {
40
+ W(e);
41
+ }, [W]), { createWidget: G } = o(v, d, { skipFetch: !0 }), K = e((e) => e.widgets), q = ce(() => {
42
+ let e = Array.from(K.values());
43
+ return v ? e.filter((e) => e.pageId === v) : e;
44
+ }, [K, v]), ke = e((e) => e.selectedWidgetId), J = e((e) => e.selectWidget), Y = e((e) => e.addWidget), X = e((e) => e.updateWidgetPosition), Ae = e((e) => e.configPanelOpen), je = e((e) => e.closeConfigPanel);
45
+ p(() => {
45
46
  let e = () => {
46
- T.current && ve(T.current.offsetWidth);
47
+ C.current && xe(C.current.offsetWidth);
47
48
  };
48
49
  return e(), window.addEventListener("resize", e), () => window.removeEventListener("resize", e);
49
50
  }, []);
50
- let Oe = p((e) => {
51
+ let Me = f((e) => {
51
52
  e.forEach((e) => {
52
- Q(e.i, {
53
+ X(e.i, {
53
54
  x: e.x,
54
55
  y: e.y,
55
56
  width: e.w,
56
57
  height: e.h
57
58
  });
58
59
  });
59
- }, [Q]), ke = p((e) => {
60
- X(e);
61
- }, [X]), $ = p(async (e) => {
62
- let t = R || (e.type ? i(e.type) : null), n = {
60
+ }, [X]), Ne = f((e) => {
61
+ J(e);
62
+ }, [J]), Z = f(async (e) => {
63
+ let t = I || (e.type ? i(e.type) : null), n = {
63
64
  width: t?.defaultSize?.width ?? 4,
64
65
  height: t?.defaultSize?.height ?? 4
65
66
  }, r;
66
- if (F) r = F;
67
+ if (N) r = N;
67
68
  else {
68
- let { x: e, y: t } = ae(Y, n, { cols: 12 });
69
+ let { x: e, y: t } = se(q, n, { cols: 12 });
69
70
  r = {
70
71
  x: e,
71
72
  y: t,
@@ -74,10 +75,10 @@ var _ = f(function({ dashboardId: f, pageId: _, title: v, onPaste: le, onSave: u
74
75
  };
75
76
  }
76
77
  let a = e;
77
- if (_) {
78
- let t = await q({
79
- pageId: _,
80
- dashboardId: f,
78
+ if (v) {
79
+ let t = await G({
80
+ pageId: v,
81
+ dashboardId: d,
81
82
  type: e.type,
82
83
  title: e.title || "Untitled Widget",
83
84
  description: e.description,
@@ -90,14 +91,14 @@ var _ = f(function({ dashboardId: f, pageId: _, title: v, onPaste: le, onSave: u
90
91
  parserId: a.parserId
91
92
  });
92
93
  if (t) {
93
- X(t.id), D(!1), I(null), L(null), z(null);
94
+ J(t.id), E(!1), P(null), F(null), L(null);
94
95
  return;
95
96
  }
96
97
  }
97
98
  let o = {
98
99
  id: crypto.randomUUID(),
99
- dashboardId: f,
100
- pageId: _ || "local",
100
+ dashboardId: d,
101
+ pageId: v || "local",
101
102
  type: e.type,
102
103
  title: e.title || "Untitled Widget",
103
104
  description: e.description,
@@ -116,22 +117,22 @@ var _ = f(function({ dashboardId: f, pageId: _, title: v, onPaste: le, onSave: u
116
117
  createdAt: (/* @__PURE__ */ new Date()).toISOString(),
117
118
  updatedAt: (/* @__PURE__ */ new Date()).toISOString()
118
119
  };
119
- Z(o), X(o.id), D(!1), I(null), L(null), z(null);
120
+ Y(o), J(o.id), E(!1), P(null), F(null), L(null);
120
121
  }, [
121
- f,
122
- _,
123
- q,
124
- Z,
125
- X,
122
+ d,
123
+ v,
124
+ G,
126
125
  Y,
127
- F,
128
- R
129
- ]), Ae = p((e) => {
130
- M(!0), P(e);
131
- }, []), je = p(() => {
132
- M(!1), P(null);
133
- }, []), Me = p((e) => {
134
- e.preventDefault(), M(!1), P(null);
126
+ J,
127
+ q,
128
+ N,
129
+ I
130
+ ]), Pe = f((e) => {
131
+ j(!0), M(e);
132
+ }, []), Fe = f(() => {
133
+ j(!1), M(null);
134
+ }, []), Ie = f((e) => {
135
+ e.preventDefault(), j(!1), M(null);
135
136
  try {
136
137
  let t = e.dataTransfer.getData("application/json");
137
138
  if (!t) return;
@@ -143,92 +144,102 @@ var _ = f(function({ dashboardId: f, pageId: _, title: v, onPaste: le, onSave: u
143
144
  defaultConfig: n.defaultConfig,
144
145
  defaultSize: n.defaultSize || r.defaultSize
145
146
  });
146
- let o = T.current?.getBoundingClientRect();
147
+ let o = C.current?.getBoundingClientRect();
147
148
  if (!o) return;
148
149
  let s = {
149
150
  width: r.defaultSize.width,
150
151
  height: r.defaultSize.height
151
- }, { x: c, y: l } = ie(e.clientX, e.clientY, o, E, s, { cols: 12 });
152
- I({
152
+ }, { x: c, y: l } = oe(e.clientX, e.clientY, o, T, s, { cols: 12 });
153
+ P({
153
154
  x: c,
154
155
  y: l,
155
156
  width: s.width,
156
157
  height: s.height
157
- }), L(n.type), z(a), D(!0);
158
+ }), F(n.type), L(a), E(!0);
158
159
  } catch (e) {
159
160
  console.error("Failed to handle widget drop:", e);
160
161
  }
161
- }, [E]), Ne = p((e) => {
162
+ }, [T]), Le = f((e) => {
162
163
  e.preventDefault(), e.dataTransfer.dropEffect = "copy";
163
- }, []), Pe = p((e, t) => {
164
+ }, []), Re = f((e, t) => {
164
165
  let n = t || i(e);
165
- $({
166
+ Z({
166
167
  type: e,
167
168
  title: n.name,
168
169
  config: n.defaultConfig
169
- }), A(!1);
170
- }, [$]);
171
- return /* @__PURE__ */ g("div", {
170
+ }), k(!1);
171
+ }, [Z]), ze = f(() => {
172
+ k((e) => !e);
173
+ }, []), { exportPNG: Q, exportPDF: $, isExporting: Be } = s({
174
+ containerRef: w,
175
+ name: y || "dashboard"
176
+ });
177
+ return /* @__PURE__ */ _("div", {
172
178
  className: "flex flex-col h-full min-w-0 overflow-hidden",
173
179
  children: [
174
- /* @__PURE__ */ h(l, {
175
- dashboardId: f,
176
- title: v,
177
- onAddWidget: () => D(!0),
178
- onTogglePalette: p(() => {
179
- A((e) => !e);
180
- }, []),
181
- isPaletteOpen: k,
180
+ /* @__PURE__ */ g(ee, {
181
+ dashboardId: d,
182
+ title: y,
183
+ onAddWidget: () => E(!0),
184
+ onTogglePalette: ze,
185
+ isPaletteOpen: O,
182
186
  onPaste: le,
183
187
  onSave: ue,
184
188
  isSaving: de,
185
- onExport: y,
186
- onShare: () => O(!0)
189
+ onExport: fe,
190
+ onExportImage: f(() => {
191
+ Q();
192
+ }, [Q]),
193
+ onExportPdf: f(() => {
194
+ $();
195
+ }, [$]),
196
+ isExportingImage: Be,
197
+ onShare: () => D(!0)
187
198
  }),
188
- he && /* @__PURE__ */ h(ee, {
189
- dashboardId: f,
190
- onFilterChange: we,
199
+ ve && /* @__PURE__ */ g(te, {
200
+ dashboardId: d,
201
+ onFilterChange: Oe,
191
202
  collapsible: !0
192
203
  }),
193
- ge && (B || b.length > 0) && /* @__PURE__ */ h(u, {
204
+ ye && (R || b.length > 0) && /* @__PURE__ */ g(u, {
194
205
  pages: b,
195
- activePageId: fe || b[0]?.id,
196
- editable: B,
197
- onPageSelect: x || (() => {}),
198
- onPagesReorder: S ? (e) => {
199
- S(e.map((e) => b.find((t) => t.id === e)).filter(Boolean));
206
+ activePageId: pe || b[0]?.id,
207
+ editable: R,
208
+ onPageSelect: me || (() => {}),
209
+ onPagesReorder: x ? (e) => {
210
+ x(e.map((e) => b.find((t) => t.id === e)).filter(Boolean));
200
211
  } : void 0,
201
- onPageCreate: C,
202
- onPageRename: w,
203
- onPageDuplicate: pe,
204
- onPageDelete: me
212
+ onPageCreate: he,
213
+ onPageRename: ge,
214
+ onPageDuplicate: S,
215
+ onPageDelete: _e
205
216
  }),
206
- /* @__PURE__ */ g("div", {
217
+ /* @__PURE__ */ _("div", {
207
218
  className: "flex flex-1 min-w-0 overflow-hidden",
208
- children: [B && /* @__PURE__ */ h(te, {
209
- isOpen: k,
210
- onClose: () => A(!1),
211
- onWidgetSelect: Pe,
212
- onDragStart: Ae,
213
- onDragEnd: je,
214
- installedWidgets: W,
215
- installedWidgetsLoading: Se,
216
- definitions: Ce
217
- }), /* @__PURE__ */ g("div", {
218
- ref: T,
219
+ children: [R && /* @__PURE__ */ g(re, {
220
+ isOpen: O,
221
+ onClose: () => k(!1),
222
+ onWidgetSelect: Re,
223
+ onDragStart: Pe,
224
+ onDragEnd: Fe,
225
+ installedWidgets: H,
226
+ installedWidgetsLoading: Ee,
227
+ definitions: De
228
+ }), /* @__PURE__ */ _("div", {
229
+ ref: C,
219
230
  className: "flex-1 min-w-0 overflow-auto bg-bg-muted",
220
- onDrop: Me,
221
- onDragOver: Ne,
231
+ onDrop: Ie,
232
+ onDragOver: Le,
222
233
  onClick: () => {
223
- B && X(null);
234
+ R && J(null);
224
235
  },
225
236
  style: {
226
237
  position: "relative",
227
- border: j ? "2px dashed var(--color-action-primary-bg)" : "none",
228
- backgroundColor: j ? "var(--color-bg-hover)" : void 0,
238
+ border: A ? "2px dashed var(--color-action-primary-bg)" : "none",
239
+ backgroundColor: A ? "var(--color-bg-hover)" : void 0,
229
240
  transition: "border-color 0.2s, background-color 0.2s"
230
241
  },
231
- children: [j && /* @__PURE__ */ h("div", {
242
+ children: [A && /* @__PURE__ */ g("div", {
232
243
  style: {
233
244
  position: "absolute",
234
245
  top: "50%",
@@ -242,7 +253,7 @@ var _ = f(function({ dashboardId: f, pageId: _, title: v, onPaste: le, onSave: u
242
253
  pointerEvents: "none",
243
254
  zIndex: 100
244
255
  },
245
- children: /* @__PURE__ */ g("p", {
256
+ children: /* @__PURE__ */ _("p", {
246
257
  style: {
247
258
  fontSize: "var(--font-size-body-md)",
248
259
  color: "var(--color-text-primary)",
@@ -250,38 +261,39 @@ var _ = f(function({ dashboardId: f, pageId: _, title: v, onPaste: le, onSave: u
250
261
  },
251
262
  children: [
252
263
  "Drop to add ",
253
- /* @__PURE__ */ h("strong", { children: N }),
264
+ /* @__PURE__ */ g("strong", { children: we }),
254
265
  " widget"
255
266
  ]
256
267
  })
257
- }), /* @__PURE__ */ h("div", {
268
+ }), /* @__PURE__ */ g("div", {
269
+ ref: w,
258
270
  className: "min-h-full origin-top-left transition-transform",
259
271
  style: {
260
- transform: `scale(${G / 100})`,
261
- width: `${100 / (G / 100)}%`
272
+ transform: `scale(${U / 100})`,
273
+ width: `${100 / (U / 100)}%`
262
274
  },
263
- children: Y.length > 0 ? /* @__PURE__ */ h(n, {
264
- enabled: _e,
275
+ children: q.length > 0 ? /* @__PURE__ */ g(n, {
276
+ enabled: be,
265
277
  onCrossFilter: (e) => {},
266
- children: /* @__PURE__ */ h(c, {
267
- widgets: Y,
268
- isEditMode: B,
269
- width: E,
270
- onLayoutChange: Oe,
271
- onWidgetSelect: ke,
272
- selectedWidgetId: Te
278
+ children: /* @__PURE__ */ g(l, {
279
+ widgets: q,
280
+ isEditMode: R,
281
+ width: T,
282
+ onLayoutChange: Me,
283
+ onWidgetSelect: Ne,
284
+ selectedWidgetId: ke
273
285
  })
274
- }) : /* @__PURE__ */ g("div", {
286
+ }) : /* @__PURE__ */ _("div", {
275
287
  className: "flex flex-col items-center justify-center min-h-[400px] p-8",
276
288
  children: [
277
- /* @__PURE__ */ h("div", {
289
+ /* @__PURE__ */ g("div", {
278
290
  className: "w-16 h-16 mb-4 flex items-center justify-center rounded-full bg-bg-surface",
279
- children: /* @__PURE__ */ h("svg", {
291
+ children: /* @__PURE__ */ g("svg", {
280
292
  className: "w-8 h-8 text-text-secondary",
281
293
  fill: "none",
282
294
  stroke: "currentColor",
283
295
  viewBox: "0 0 24 24",
284
- children: /* @__PURE__ */ h("path", {
296
+ children: /* @__PURE__ */ g("path", {
285
297
  strokeLinecap: "round",
286
298
  strokeLinejoin: "round",
287
299
  strokeWidth: 2,
@@ -289,24 +301,24 @@ var _ = f(function({ dashboardId: f, pageId: _, title: v, onPaste: le, onSave: u
289
301
  })
290
302
  })
291
303
  }),
292
- /* @__PURE__ */ h("h3", {
304
+ /* @__PURE__ */ g("h3", {
293
305
  className: "text-lg font-medium text-text-primary mb-2",
294
306
  children: "No widgets yet"
295
307
  }),
296
- /* @__PURE__ */ h("p", {
308
+ /* @__PURE__ */ g("p", {
297
309
  className: "text-sm text-text-secondary mb-4 text-center max-w-[300px]",
298
310
  children: "Add widgets to build your dashboard. Click the \"Add Widget\" button to get started."
299
311
  }),
300
- B && /* @__PURE__ */ g("button", {
312
+ R && /* @__PURE__ */ _("button", {
301
313
  type: "button",
302
- onClick: () => D(!0),
314
+ onClick: () => E(!0),
303
315
  className: "\n inline-flex items-center gap-2\n px-4 py-2\n text-sm font-medium\n text-action-primary-fg\n bg-action-primary-bg\n hover:bg-action-primary-bgHover\n rounded-md\n transition-colors\n ",
304
- children: [/* @__PURE__ */ h("svg", {
316
+ children: [/* @__PURE__ */ g("svg", {
305
317
  className: "w-4 h-4",
306
318
  fill: "none",
307
319
  stroke: "currentColor",
308
320
  viewBox: "0 0 24 24",
309
- children: /* @__PURE__ */ h("path", {
321
+ children: /* @__PURE__ */ g("path", {
310
322
  strokeLinecap: "round",
311
323
  strokeLinejoin: "round",
312
324
  strokeWidth: 2,
@@ -319,31 +331,31 @@ var _ = f(function({ dashboardId: f, pageId: _, title: v, onPaste: le, onSave: u
319
331
  })]
320
332
  })]
321
333
  }),
322
- /* @__PURE__ */ h(re, {
323
- isOpen: Ee,
324
- onClose: De
334
+ /* @__PURE__ */ g(ae, {
335
+ isOpen: Ae,
336
+ onClose: je
325
337
  }),
326
- /* @__PURE__ */ h(ne, {
327
- isOpen: ye,
338
+ /* @__PURE__ */ g(ie, {
339
+ isOpen: Se,
328
340
  onClose: () => {
329
- D(!1), I(null), L(null), z(null);
341
+ E(!1), P(null), F(null), L(null);
330
342
  },
331
- onSave: $,
332
- preselectedType: xe || void 0,
333
- preselectedDefinition: R || void 0,
334
- dashboardId: f,
335
- installedWidgets: W
343
+ onSave: Z,
344
+ preselectedType: Te || void 0,
345
+ preselectedDefinition: I || void 0,
346
+ dashboardId: d,
347
+ installedWidgets: H
336
348
  }),
337
- /* @__PURE__ */ h(d, {
338
- dashboardId: f,
339
- dashboardName: v || "Dashboard",
340
- isOpen: be,
341
- onClose: () => O(!1)
349
+ /* @__PURE__ */ g(ne, {
350
+ dashboardId: d,
351
+ dashboardName: y || "Dashboard",
352
+ isOpen: Ce,
353
+ onClose: () => D(!1)
342
354
  })
343
355
  ]
344
356
  });
345
357
  });
346
358
  //#endregion
347
- export { _ as default };
359
+ export { v as default };
348
360
 
349
361
  //# sourceMappingURL=DashboardCanvas.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"DashboardCanvas.js","names":[],"sources":["../../../../src/bigconsole/components/dashboard/DashboardCanvas.tsx"],"sourcesContent":["/**\n * DashboardCanvas Component\n *\n * Main canvas for dashboard with grid layout and widget rendering.\n * Phase 2: Added GlobalFiltersBar and PageTabsManager integration.\n * Phase 3: Added WidgetPalette sidebar for drag-and-drop widget creation.\n */\n\nimport { type FC, memo, useRef, useState, useCallback, useEffect, useMemo } from 'react';\nimport type { Layout } from 'react-grid-layout';\nimport type { Widget, WidgetType } from '../../types';\nimport { useDashboardStore, useWidgetStore } from '../../store';\nimport { useWidgetOperations } from '../../hooks/useWidgetOperations';\nimport { useInstalledWidgets } from '../../hooks/useInstalledWidgets';\nimport { useWidgetCatalog } from '../../hooks/useWidgetCatalog';\nimport { useBigConsole } from '../../context';\nimport { DashboardGrid } from './DashboardGrid';\nimport { DashboardToolbar } from './DashboardToolbar';\nimport { GlobalFiltersBar } from './GlobalFiltersBar';\nimport { PageTabsManager, type DashboardPageTab } from './PageTabsManager';\nimport { ShareDialog } from './ShareDialog';\nimport { WidgetPalette } from './WidgetPalette';\nimport { WidgetConfigDialog } from '../widgets/WidgetConfigDialog';\nimport { PropertiesPanel } from '../widgets/PropertiesPanel';\nimport { getWidgetDefinition, type WidgetDefinition } from '../widgets/WidgetRegistry';\n// Phase 2: Cross-filtering context\nimport { CrossFilterProvider } from '../../contexts';\n// Smart widget positioning\nimport { findNextAvailablePosition, calculatePositionFromDrop } from '../../utils/widgetPositioning';\n\n// ============================================================================\n// Types\n// ============================================================================\n\nexport interface DashboardCanvasProps {\n /** Dashboard ID */\n dashboardId: string;\n /** Page ID (for loading widgets) */\n pageId?: string;\n /** Dashboard title */\n title?: string;\n /** Callback to paste widget */\n onPaste?: () => void;\n /** Callback when dashboard is saved */\n onSave?: () => void;\n /** Whether save is in progress */\n isSaving?: boolean;\n /** Callback to export dashboard */\n onExport?: () => void;\n /** Dashboard pages for PageTabsManager */\n pages?: DashboardPageTab[];\n /** Active page ID */\n activePageId?: string;\n /** Callback when page changes */\n onPageChange?: (pageId: string) => void;\n /** Callback when pages are reordered */\n onPagesReorder?: (pages: DashboardPageTab[]) => void;\n /** Callback when page is added */\n onPageAdd?: () => void;\n /** Callback when page is renamed */\n onPageRename?: (pageId: string, newName: string) => void;\n /** Callback when page is duplicated */\n onPageDuplicate?: (pageId: string) => void;\n /** Callback when page is deleted */\n onPageDelete?: (pageId: string) => void;\n /** Whether to show global filters bar */\n showFilters?: boolean;\n /** Whether to show page tabs */\n showPageTabs?: boolean;\n /** Whether to enable cross-filtering (widget clicks update filters) */\n enableCrossFilter?: boolean;\n}\n\n// ============================================================================\n// Component\n// ============================================================================\n\nexport const DashboardCanvas: FC<DashboardCanvasProps> = memo(function DashboardCanvas({\n dashboardId,\n pageId,\n title,\n onPaste,\n onSave,\n isSaving = false,\n onExport,\n pages = [],\n activePageId,\n onPageChange,\n onPagesReorder,\n onPageAdd,\n onPageRename,\n onPageDuplicate,\n onPageDelete,\n showFilters = true,\n showPageTabs = true,\n enableCrossFilter = true,\n}) {\n const containerRef = useRef<HTMLDivElement>(null);\n const [containerWidth, setContainerWidth] = useState(1200);\n const [isAddDialogOpen, setIsAddDialogOpen] = useState(false);\n const [isShareDialogOpen, setIsShareDialogOpen] = useState(false);\n const [isPaletteOpen, setIsPaletteOpen] = useState(false);\n const [isDraggingWidget, setIsDraggingWidget] = useState(false);\n const [dragWidgetType, setDragWidgetType] = useState<WidgetType | null>(null);\n // State for drag & drop widget creation (stores pending position and type)\n const [pendingDropPosition, setPendingDropPosition] = useState<{\n x: number;\n y: number;\n width: number;\n height: number;\n } | null>(null);\n const [pendingDropType, setPendingDropType] = useState<WidgetType | null>(null);\n // Stores the full WidgetDefinition for installed/store widgets dropped from the palette,\n // so the config dialog can use the store metadata instead of the generic 'custom' definition.\n const [pendingDropDefinition, setPendingDropDefinition] = useState<WidgetDefinition | null>(null);\n\n // Store state\n const isEditMode = useDashboardStore((state) => state.viewMode === 'edit');\n\n // Fetch installed widgets only in edit mode — the palette and add-widget dialog\n // are not usable in read-only view, so skip the request for viewer-only sessions.\n const { workspaceId, apiGatewayUrl, authToken } = useBigConsole();\n const { installedWidgets, loading: installedWidgetsLoading } = useInstalledWidgets({\n workspaceId,\n apiGatewayUrl,\n authToken,\n enabled: isEditMode,\n });\n // Built-in widget catalog from the backend (getWidgetTypes); falls back to the\n // bundled registry while loading or if the supergraph hasn't adopted it yet.\n const { definitions: widgetCatalog } = useWidgetCatalog({\n workspaceId,\n apiGatewayUrl,\n authToken,\n enabled: isEditMode,\n });\n const zoomLevel = useDashboardStore((state) => state.zoomLevel);\n const setGlobalFilterValues = useDashboardStore((state) => state.setGlobalFilterValues);\n\n // Handle filter changes - update store for widget access\n const handleFilterChange = useCallback(\n (filterValues: Record<string, unknown>) => {\n // Update global filter values in dashboard store\n setGlobalFilterValues(filterValues);\n\n // Widgets can now access filter values via useGlobalFilterValues() hook\n // When backend supports filter parameters, widget queries will include these values\n },\n [setGlobalFilterValues]\n );\n\n // Widget operations hook (connects to backend) - skipFetch since parent page fetches\n const { createWidget: createWidgetApi } = useWidgetOperations(pageId, dashboardId, { skipFetch: true });\n\n const widgetsMap = useWidgetStore((state) => state.widgets);\n // Filter widgets to only show widgets for the current page\n const widgetsArray = useMemo(() => {\n const allWidgets = Array.from(widgetsMap.values());\n if (!pageId) return allWidgets;\n return allWidgets.filter((widget) => widget.pageId === pageId);\n }, [widgetsMap, pageId]);\n const selectedWidgetId = useWidgetStore((state) => state.selectedWidgetId);\n const selectWidget = useWidgetStore((state) => state.selectWidget);\n const addWidgetToStore = useWidgetStore((state) => state.addWidget);\n const updateWidgetPosition = useWidgetStore((state) => state.updateWidgetPosition);\n const configPanelOpen = useWidgetStore((state) => state.configPanelOpen);\n const closeConfigPanel = useWidgetStore((state) => state.closeConfigPanel);\n\n // Handle resize\n useEffect(() => {\n const updateWidth = () => {\n if (containerRef.current) {\n setContainerWidth(containerRef.current.offsetWidth);\n }\n };\n\n updateWidth();\n window.addEventListener('resize', updateWidth);\n return () => window.removeEventListener('resize', updateWidth);\n }, []);\n\n // Handle layout change\n const handleLayoutChange = useCallback(\n (layout: Layout[]) => {\n layout.forEach((item) => {\n updateWidgetPosition(item.i, {\n x: item.x,\n y: item.y,\n width: item.w,\n height: item.h,\n });\n });\n },\n [updateWidgetPosition]\n );\n\n // Handle widget select\n const handleWidgetSelect = useCallback(\n (widgetId: string) => {\n selectWidget(widgetId);\n },\n [selectWidget]\n );\n\n // Handle add widget - tries API first, falls back to local store\n const handleAddWidget = useCallback(\n async (widgetData: Partial<Widget>) => {\n // Get widget definition for default size; prefer the pending drop definition\n // which preserves installed/store widget metadata over the generic registry lookup.\n const definition = pendingDropDefinition || (widgetData.type ? getWidgetDefinition(widgetData.type) : null);\n const widgetSize = {\n width: definition?.defaultSize?.width ?? 4,\n height: definition?.defaultSize?.height ?? 4,\n };\n\n // Use pending drop position if available (from drag & drop), otherwise use smart placement\n let position: { x: number; y: number; width: number; height: number };\n if (pendingDropPosition) {\n position = pendingDropPosition;\n } else {\n // Use smart placement algorithm - horizontal first, then next row\n const { x, y } = findNextAvailablePosition(widgetsArray, widgetSize, { cols: 12 });\n position = {\n x,\n y,\n width: widgetSize.width,\n height: widgetSize.height,\n };\n }\n\n // Extract dataSinkId and parserId from widgetData (passed from WidgetConfigDialog)\n const extendedData = widgetData as Partial<Widget> & { dataSinkId?: string; parserId?: string };\n\n // Try to create via API first\n if (pageId) {\n const widget = await createWidgetApi({\n pageId,\n dashboardId,\n type: widgetData.type!,\n title: widgetData.title || 'Untitled Widget',\n description: widgetData.description,\n // v2.0: Renderer and template support\n renderer: widgetData.renderer,\n template: widgetData.template,\n config: widgetData.config || {},\n position,\n refreshInterval: widgetData.refreshInterval ?? undefined,\n // v2.0: Data source fields (DataSink → Parser → Widget)\n dataSinkId: extendedData.dataSinkId,\n parserId: extendedData.parserId,\n });\n\n if (widget) {\n selectWidget(widget.id);\n setIsAddDialogOpen(false);\n // Clear pending drop state\n setPendingDropPosition(null);\n setPendingDropType(null);\n setPendingDropDefinition(null);\n return;\n }\n }\n\n // Fallback to local store if API fails or no pageId\n const newWidget: Widget = {\n id: crypto.randomUUID(),\n dashboardId,\n pageId: pageId || 'local',\n type: widgetData.type!,\n title: widgetData.title || 'Untitled Widget',\n description: widgetData.description,\n // v2.0: Renderer and template support\n renderer: widgetData.renderer,\n template: widgetData.template,\n refreshInterval: widgetData.refreshInterval,\n config: widgetData.config || {},\n position,\n positionX: position.x,\n positionY: position.y,\n positionWidth: position.width,\n positionHeight: position.height,\n // v2.0: Data source fields (DataSink → Parser → Widget)\n dataSinkId: extendedData.dataSinkId,\n parserId: extendedData.parserId,\n translations: [],\n createdAt: new Date().toISOString(),\n updatedAt: new Date().toISOString(),\n };\n\n addWidgetToStore(newWidget);\n selectWidget(newWidget.id);\n setIsAddDialogOpen(false);\n // Clear pending drop state\n setPendingDropPosition(null);\n setPendingDropType(null);\n setPendingDropDefinition(null);\n },\n [\n dashboardId,\n pageId,\n createWidgetApi,\n addWidgetToStore,\n selectWidget,\n widgetsArray,\n pendingDropPosition,\n pendingDropDefinition,\n ]\n );\n\n // Handle drag start from palette\n const handlePaletteDragStart = useCallback((type: WidgetType) => {\n setIsDraggingWidget(true);\n setDragWidgetType(type);\n }, []);\n\n // Handle drag end from palette\n const handlePaletteDragEnd = useCallback(() => {\n setIsDraggingWidget(false);\n setDragWidgetType(null);\n }, []);\n\n // Handle drop on canvas - opens config dialog instead of creating widget directly\n const handleCanvasDrop = useCallback(\n (e: React.DragEvent) => {\n e.preventDefault();\n setIsDraggingWidget(false);\n setDragWidgetType(null);\n\n try {\n const data = e.dataTransfer.getData('application/json');\n if (!data) return;\n\n const widgetData = JSON.parse(data);\n const definition = getWidgetDefinition(widgetData.type);\n\n // If the dropped data includes defaultConfig (from WidgetPalette drag serialization),\n // reconstruct a full WidgetDefinition so installed/store widget identity is preserved.\n let droppedDefinition: WidgetDefinition | null = null;\n if (widgetData.defaultConfig && widgetData.category) {\n droppedDefinition = {\n ...definition,\n name: widgetData.name || definition.name,\n category: widgetData.category,\n defaultConfig: widgetData.defaultConfig,\n defaultSize: widgetData.defaultSize || definition.defaultSize,\n };\n }\n\n // Calculate drop position relative to canvas\n const canvasRect = containerRef.current?.getBoundingClientRect();\n if (!canvasRect) return;\n\n // Use utility to calculate grid position from drop coordinates\n const widgetSize = {\n width: definition.defaultSize.width,\n height: definition.defaultSize.height,\n };\n\n const { x: gridX, y: gridY } = calculatePositionFromDrop(\n e.clientX,\n e.clientY,\n canvasRect,\n containerWidth,\n widgetSize,\n { cols: 12 }\n );\n\n const position = {\n x: gridX,\n y: gridY,\n width: widgetSize.width,\n height: widgetSize.height,\n };\n\n // Store the drop position, type, and full definition, then open the config dialog\n setPendingDropPosition(position);\n setPendingDropType(widgetData.type as WidgetType);\n setPendingDropDefinition(droppedDefinition);\n setIsAddDialogOpen(true);\n } catch (error) {\n console.error('Failed to handle widget drop:', error);\n }\n },\n [containerWidth]\n );\n\n // Handle drag over to allow drop\n const handleCanvasDragOver = useCallback((e: React.DragEvent) => {\n e.preventDefault();\n e.dataTransfer.dropEffect = 'copy';\n }, []);\n\n // Handle widget select from palette (quick add without drag)\n const handlePaletteWidgetSelect = useCallback(\n (type: WidgetType, paletteDefinition?: WidgetDefinition) => {\n const definition = paletteDefinition || getWidgetDefinition(type);\n handleAddWidget({\n type,\n title: definition.name,\n config: definition.defaultConfig,\n });\n setIsPaletteOpen(false);\n },\n [handleAddWidget]\n );\n\n // Toggle palette\n const togglePalette = useCallback(() => {\n setIsPaletteOpen((prev) => !prev);\n }, []);\n\n return (\n <div className=\"flex flex-col h-full min-w-0 overflow-hidden\">\n {/* Toolbar */}\n <DashboardToolbar\n dashboardId={dashboardId}\n title={title}\n onAddWidget={() => setIsAddDialogOpen(true)}\n onTogglePalette={togglePalette}\n isPaletteOpen={isPaletteOpen}\n onPaste={onPaste}\n onSave={onSave}\n isSaving={isSaving}\n onExport={onExport}\n onShare={() => setIsShareDialogOpen(true)}\n />\n\n {/* Global Filters Bar */}\n {showFilters && <GlobalFiltersBar dashboardId={dashboardId} onFilterChange={handleFilterChange} collapsible />}\n\n {/* Page Tabs - always show in edit mode so user can add first page */}\n {showPageTabs && (isEditMode || pages.length > 0) && (\n <PageTabsManager\n pages={pages}\n activePageId={activePageId || pages[0]?.id}\n editable={isEditMode}\n onPageSelect={onPageChange || (() => {})}\n onPagesReorder={\n onPagesReorder\n ? (pageIds) => {\n // Convert string[] to DashboardPageTab[] for the callback\n const reorderedPages = pageIds.map((id) => pages.find((p) => p.id === id)!).filter(Boolean);\n onPagesReorder(reorderedPages);\n }\n : undefined\n }\n onPageCreate={onPageAdd}\n onPageRename={onPageRename}\n onPageDuplicate={onPageDuplicate}\n onPageDelete={onPageDelete}\n />\n )}\n\n {/* Main content area with palette and canvas */}\n <div className=\"flex flex-1 min-w-0 overflow-hidden\">\n {/* Widget Palette Sidebar */}\n {isEditMode && (\n <WidgetPalette\n isOpen={isPaletteOpen}\n onClose={() => setIsPaletteOpen(false)}\n onWidgetSelect={handlePaletteWidgetSelect}\n onDragStart={handlePaletteDragStart}\n onDragEnd={handlePaletteDragEnd}\n installedWidgets={installedWidgets}\n installedWidgetsLoading={installedWidgetsLoading}\n definitions={widgetCatalog}\n />\n )}\n\n {/* Canvas area */}\n <div\n ref={containerRef}\n className=\"flex-1 min-w-0 overflow-auto bg-bg-muted\"\n onDrop={handleCanvasDrop}\n onDragOver={handleCanvasDragOver}\n onClick={() => {\n if (isEditMode) {\n selectWidget(null);\n }\n }}\n style={{\n position: 'relative',\n border: isDraggingWidget ? '2px dashed var(--color-action-primary-bg)' : 'none',\n backgroundColor: isDraggingWidget ? 'var(--color-bg-hover)' : undefined,\n transition: 'border-color 0.2s, background-color 0.2s',\n }}\n >\n {/* Drop zone indicator */}\n {isDraggingWidget && (\n <div\n style={{\n position: 'absolute',\n top: '50%',\n left: '50%',\n transform: 'translate(-50%, -50%)',\n padding: 'var(--space-pagePadding)',\n backgroundColor: 'var(--color-bg-surface)',\n borderRadius: 'var(--radius-card)',\n boxShadow: 'var(--shadow-elevation-2)',\n textAlign: 'center',\n pointerEvents: 'none',\n zIndex: 100,\n }}\n >\n <p style={{ fontSize: 'var(--font-size-body-md)', color: 'var(--color-text-primary)', margin: 0 }}>\n Drop to add <strong>{dragWidgetType}</strong> widget\n </p>\n </div>\n )}\n\n <div\n className=\"min-h-full origin-top-left transition-transform\"\n style={{\n transform: `scale(${zoomLevel / 100})`,\n width: `${100 / (zoomLevel / 100)}%`,\n }}\n >\n {widgetsArray.length > 0 ? (\n <CrossFilterProvider\n enabled={enableCrossFilter}\n onCrossFilter={(_filters) => {\n // Cross-filter applied - widget queries can access filter values\n }}\n >\n <DashboardGrid\n widgets={widgetsArray}\n isEditMode={isEditMode}\n width={containerWidth}\n onLayoutChange={handleLayoutChange}\n onWidgetSelect={handleWidgetSelect}\n selectedWidgetId={selectedWidgetId}\n />\n </CrossFilterProvider>\n ) : (\n <div className=\"flex flex-col items-center justify-center min-h-[400px] p-8\">\n <div className=\"w-16 h-16 mb-4 flex items-center justify-center rounded-full bg-bg-surface\">\n <svg className=\"w-8 h-8 text-text-secondary\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n <path\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n strokeWidth={2}\n d=\"M4 5a1 1 0 011-1h14a1 1 0 011 1v2a1 1 0 01-1 1H5a1 1 0 01-1-1V5zM4 13a1 1 0 011-1h6a1 1 0 011 1v6a1 1 0 01-1 1H5a1 1 0 01-1-1v-6zM16 13a1 1 0 011-1h2a1 1 0 011 1v6a1 1 0 01-1 1h-2a1 1 0 01-1-1v-6z\"\n />\n </svg>\n </div>\n <h3 className=\"text-lg font-medium text-text-primary mb-2\">No widgets yet</h3>\n <p className=\"text-sm text-text-secondary mb-4 text-center max-w-[300px]\">\n Add widgets to build your dashboard. Click the &quot;Add Widget&quot; button to get started.\n </p>\n {isEditMode && (\n <button\n type=\"button\"\n onClick={() => setIsAddDialogOpen(true)}\n className=\"\n inline-flex items-center gap-2\n px-4 py-2\n text-sm font-medium\n text-action-primary-fg\n bg-action-primary-bg\n hover:bg-action-primary-bgHover\n rounded-md\n transition-colors\n \"\n >\n <svg className=\"w-4 h-4\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n <path strokeLinecap=\"round\" strokeLinejoin=\"round\" strokeWidth={2} d=\"M12 4v16m8-8H4\" />\n </svg>\n Add Widget\n </button>\n )}\n </div>\n )}\n </div>\n </div>\n </div>\n\n {/* Properties Panel */}\n <PropertiesPanel isOpen={configPanelOpen} onClose={closeConfigPanel} />\n\n {/* Add Widget Dialog */}\n <WidgetConfigDialog\n isOpen={isAddDialogOpen}\n onClose={() => {\n setIsAddDialogOpen(false);\n // Clear pending drop state when dialog is closed\n setPendingDropPosition(null);\n setPendingDropType(null);\n setPendingDropDefinition(null);\n }}\n onSave={handleAddWidget}\n preselectedType={pendingDropType || undefined}\n preselectedDefinition={pendingDropDefinition || undefined}\n dashboardId={dashboardId}\n installedWidgets={installedWidgets}\n />\n\n {/* Share Dialog */}\n <ShareDialog\n dashboardId={dashboardId}\n dashboardName={title || 'Dashboard'}\n isOpen={isShareDialogOpen}\n onClose={() => setIsShareDialogOpen(false)}\n />\n </div>\n );\n});\n\nexport default DashboardCanvas;\n"],"mappings":"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"}
1
+ {"version":3,"file":"DashboardCanvas.js","names":[],"sources":["../../../../src/bigconsole/components/dashboard/DashboardCanvas.tsx"],"sourcesContent":["/**\n * DashboardCanvas Component\n *\n * Main canvas for dashboard with grid layout and widget rendering.\n * Phase 2: Added GlobalFiltersBar and PageTabsManager integration.\n * Phase 3: Added WidgetPalette sidebar for drag-and-drop widget creation.\n */\n\nimport { type FC, memo, useRef, useState, useCallback, useEffect, useMemo } from 'react';\nimport type { Layout } from 'react-grid-layout';\nimport type { Widget, WidgetType } from '../../types';\nimport { useDashboardStore, useWidgetStore } from '../../store';\nimport { useWidgetOperations } from '../../hooks/useWidgetOperations';\nimport { useInstalledWidgets } from '../../hooks/useInstalledWidgets';\nimport { useWidgetCatalog } from '../../hooks/useWidgetCatalog';\nimport { useDashboardImageExport } from '../../hooks/useDashboardImageExport';\nimport { useBigConsole } from '../../context';\nimport { DashboardGrid } from './DashboardGrid';\nimport { DashboardToolbar } from './DashboardToolbar';\nimport { GlobalFiltersBar } from './GlobalFiltersBar';\nimport { PageTabsManager, type DashboardPageTab } from './PageTabsManager';\nimport { ShareDialog } from './ShareDialog';\nimport { WidgetPalette } from './WidgetPalette';\nimport { WidgetConfigDialog } from '../widgets/WidgetConfigDialog';\nimport { PropertiesPanel } from '../widgets/PropertiesPanel';\nimport { getWidgetDefinition, type WidgetDefinition } from '../widgets/WidgetRegistry';\n// Phase 2: Cross-filtering context\nimport { CrossFilterProvider } from '../../contexts';\n// Smart widget positioning\nimport { findNextAvailablePosition, calculatePositionFromDrop } from '../../utils/widgetPositioning';\n\n// ============================================================================\n// Types\n// ============================================================================\n\nexport interface DashboardCanvasProps {\n /** Dashboard ID */\n dashboardId: string;\n /** Page ID (for loading widgets) */\n pageId?: string;\n /** Dashboard title */\n title?: string;\n /** Callback to paste widget */\n onPaste?: () => void;\n /** Callback when dashboard is saved */\n onSave?: () => void;\n /** Whether save is in progress */\n isSaving?: boolean;\n /** Callback to export dashboard */\n onExport?: () => void;\n /** Dashboard pages for PageTabsManager */\n pages?: DashboardPageTab[];\n /** Active page ID */\n activePageId?: string;\n /** Callback when page changes */\n onPageChange?: (pageId: string) => void;\n /** Callback when pages are reordered */\n onPagesReorder?: (pages: DashboardPageTab[]) => void;\n /** Callback when page is added */\n onPageAdd?: () => void;\n /** Callback when page is renamed */\n onPageRename?: (pageId: string, newName: string) => void;\n /** Callback when page is duplicated */\n onPageDuplicate?: (pageId: string) => void;\n /** Callback when page is deleted */\n onPageDelete?: (pageId: string) => void;\n /** Whether to show global filters bar */\n showFilters?: boolean;\n /** Whether to show page tabs */\n showPageTabs?: boolean;\n /** Whether to enable cross-filtering (widget clicks update filters) */\n enableCrossFilter?: boolean;\n}\n\n// ============================================================================\n// Component\n// ============================================================================\n\nexport const DashboardCanvas: FC<DashboardCanvasProps> = memo(function DashboardCanvas({\n dashboardId,\n pageId,\n title,\n onPaste,\n onSave,\n isSaving = false,\n onExport,\n pages = [],\n activePageId,\n onPageChange,\n onPagesReorder,\n onPageAdd,\n onPageRename,\n onPageDuplicate,\n onPageDelete,\n showFilters = true,\n showPageTabs = true,\n enableCrossFilter = true,\n}) {\n const containerRef = useRef<HTMLDivElement>(null);\n // Inner board element (the grid surface) — rasterized for PNG/PDF board-pack export.\n const boardRef = useRef<HTMLDivElement>(null);\n const [containerWidth, setContainerWidth] = useState(1200);\n const [isAddDialogOpen, setIsAddDialogOpen] = useState(false);\n const [isShareDialogOpen, setIsShareDialogOpen] = useState(false);\n const [isPaletteOpen, setIsPaletteOpen] = useState(false);\n const [isDraggingWidget, setIsDraggingWidget] = useState(false);\n const [dragWidgetType, setDragWidgetType] = useState<WidgetType | null>(null);\n // State for drag & drop widget creation (stores pending position and type)\n const [pendingDropPosition, setPendingDropPosition] = useState<{\n x: number;\n y: number;\n width: number;\n height: number;\n } | null>(null);\n const [pendingDropType, setPendingDropType] = useState<WidgetType | null>(null);\n // Stores the full WidgetDefinition for installed/store widgets dropped from the palette,\n // so the config dialog can use the store metadata instead of the generic 'custom' definition.\n const [pendingDropDefinition, setPendingDropDefinition] = useState<WidgetDefinition | null>(null);\n\n // Store state\n const isEditMode = useDashboardStore((state) => state.viewMode === 'edit');\n\n // Fetch installed widgets only in edit mode — the palette and add-widget dialog\n // are not usable in read-only view, so skip the request for viewer-only sessions.\n const { workspaceId, apiGatewayUrl, authToken } = useBigConsole();\n const { installedWidgets, loading: installedWidgetsLoading } = useInstalledWidgets({\n workspaceId,\n apiGatewayUrl,\n authToken,\n enabled: isEditMode,\n });\n // Built-in widget catalog from the backend (getWidgetTypes); falls back to the\n // bundled registry while loading or if the supergraph hasn't adopted it yet.\n const { definitions: widgetCatalog } = useWidgetCatalog({\n workspaceId,\n apiGatewayUrl,\n authToken,\n enabled: isEditMode,\n });\n const zoomLevel = useDashboardStore((state) => state.zoomLevel);\n const setGlobalFilterValues = useDashboardStore((state) => state.setGlobalFilterValues);\n\n // Handle filter changes - update store for widget access\n const handleFilterChange = useCallback(\n (filterValues: Record<string, unknown>) => {\n // Update global filter values in dashboard store\n setGlobalFilterValues(filterValues);\n\n // Widgets can now access filter values via useGlobalFilterValues() hook\n // When backend supports filter parameters, widget queries will include these values\n },\n [setGlobalFilterValues]\n );\n\n // Widget operations hook (connects to backend) - skipFetch since parent page fetches\n const { createWidget: createWidgetApi } = useWidgetOperations(pageId, dashboardId, { skipFetch: true });\n\n const widgetsMap = useWidgetStore((state) => state.widgets);\n // Filter widgets to only show widgets for the current page\n const widgetsArray = useMemo(() => {\n const allWidgets = Array.from(widgetsMap.values());\n if (!pageId) return allWidgets;\n return allWidgets.filter((widget) => widget.pageId === pageId);\n }, [widgetsMap, pageId]);\n const selectedWidgetId = useWidgetStore((state) => state.selectedWidgetId);\n const selectWidget = useWidgetStore((state) => state.selectWidget);\n const addWidgetToStore = useWidgetStore((state) => state.addWidget);\n const updateWidgetPosition = useWidgetStore((state) => state.updateWidgetPosition);\n const configPanelOpen = useWidgetStore((state) => state.configPanelOpen);\n const closeConfigPanel = useWidgetStore((state) => state.closeConfigPanel);\n\n // Handle resize\n useEffect(() => {\n const updateWidth = () => {\n if (containerRef.current) {\n setContainerWidth(containerRef.current.offsetWidth);\n }\n };\n\n updateWidth();\n window.addEventListener('resize', updateWidth);\n return () => window.removeEventListener('resize', updateWidth);\n }, []);\n\n // Handle layout change\n const handleLayoutChange = useCallback(\n (layout: Layout[]) => {\n layout.forEach((item) => {\n updateWidgetPosition(item.i, {\n x: item.x,\n y: item.y,\n width: item.w,\n height: item.h,\n });\n });\n },\n [updateWidgetPosition]\n );\n\n // Handle widget select\n const handleWidgetSelect = useCallback(\n (widgetId: string) => {\n selectWidget(widgetId);\n },\n [selectWidget]\n );\n\n // Handle add widget - tries API first, falls back to local store\n const handleAddWidget = useCallback(\n async (widgetData: Partial<Widget>) => {\n // Get widget definition for default size; prefer the pending drop definition\n // which preserves installed/store widget metadata over the generic registry lookup.\n const definition = pendingDropDefinition || (widgetData.type ? getWidgetDefinition(widgetData.type) : null);\n const widgetSize = {\n width: definition?.defaultSize?.width ?? 4,\n height: definition?.defaultSize?.height ?? 4,\n };\n\n // Use pending drop position if available (from drag & drop), otherwise use smart placement\n let position: { x: number; y: number; width: number; height: number };\n if (pendingDropPosition) {\n position = pendingDropPosition;\n } else {\n // Use smart placement algorithm - horizontal first, then next row\n const { x, y } = findNextAvailablePosition(widgetsArray, widgetSize, { cols: 12 });\n position = {\n x,\n y,\n width: widgetSize.width,\n height: widgetSize.height,\n };\n }\n\n // Extract dataSinkId and parserId from widgetData (passed from WidgetConfigDialog)\n const extendedData = widgetData as Partial<Widget> & { dataSinkId?: string; parserId?: string };\n\n // Try to create via API first\n if (pageId) {\n const widget = await createWidgetApi({\n pageId,\n dashboardId,\n type: widgetData.type!,\n title: widgetData.title || 'Untitled Widget',\n description: widgetData.description,\n // v2.0: Renderer and template support\n renderer: widgetData.renderer,\n template: widgetData.template,\n config: widgetData.config || {},\n position,\n refreshInterval: widgetData.refreshInterval ?? undefined,\n // v2.0: Data source fields (DataSink → Parser → Widget)\n dataSinkId: extendedData.dataSinkId,\n parserId: extendedData.parserId,\n });\n\n if (widget) {\n selectWidget(widget.id);\n setIsAddDialogOpen(false);\n // Clear pending drop state\n setPendingDropPosition(null);\n setPendingDropType(null);\n setPendingDropDefinition(null);\n return;\n }\n }\n\n // Fallback to local store if API fails or no pageId\n const newWidget: Widget = {\n id: crypto.randomUUID(),\n dashboardId,\n pageId: pageId || 'local',\n type: widgetData.type!,\n title: widgetData.title || 'Untitled Widget',\n description: widgetData.description,\n // v2.0: Renderer and template support\n renderer: widgetData.renderer,\n template: widgetData.template,\n refreshInterval: widgetData.refreshInterval,\n config: widgetData.config || {},\n position,\n positionX: position.x,\n positionY: position.y,\n positionWidth: position.width,\n positionHeight: position.height,\n // v2.0: Data source fields (DataSink → Parser → Widget)\n dataSinkId: extendedData.dataSinkId,\n parserId: extendedData.parserId,\n translations: [],\n createdAt: new Date().toISOString(),\n updatedAt: new Date().toISOString(),\n };\n\n addWidgetToStore(newWidget);\n selectWidget(newWidget.id);\n setIsAddDialogOpen(false);\n // Clear pending drop state\n setPendingDropPosition(null);\n setPendingDropType(null);\n setPendingDropDefinition(null);\n },\n [\n dashboardId,\n pageId,\n createWidgetApi,\n addWidgetToStore,\n selectWidget,\n widgetsArray,\n pendingDropPosition,\n pendingDropDefinition,\n ]\n );\n\n // Handle drag start from palette\n const handlePaletteDragStart = useCallback((type: WidgetType) => {\n setIsDraggingWidget(true);\n setDragWidgetType(type);\n }, []);\n\n // Handle drag end from palette\n const handlePaletteDragEnd = useCallback(() => {\n setIsDraggingWidget(false);\n setDragWidgetType(null);\n }, []);\n\n // Handle drop on canvas - opens config dialog instead of creating widget directly\n const handleCanvasDrop = useCallback(\n (e: React.DragEvent) => {\n e.preventDefault();\n setIsDraggingWidget(false);\n setDragWidgetType(null);\n\n try {\n const data = e.dataTransfer.getData('application/json');\n if (!data) return;\n\n const widgetData = JSON.parse(data);\n const definition = getWidgetDefinition(widgetData.type);\n\n // If the dropped data includes defaultConfig (from WidgetPalette drag serialization),\n // reconstruct a full WidgetDefinition so installed/store widget identity is preserved.\n let droppedDefinition: WidgetDefinition | null = null;\n if (widgetData.defaultConfig && widgetData.category) {\n droppedDefinition = {\n ...definition,\n name: widgetData.name || definition.name,\n category: widgetData.category,\n defaultConfig: widgetData.defaultConfig,\n defaultSize: widgetData.defaultSize || definition.defaultSize,\n };\n }\n\n // Calculate drop position relative to canvas\n const canvasRect = containerRef.current?.getBoundingClientRect();\n if (!canvasRect) return;\n\n // Use utility to calculate grid position from drop coordinates\n const widgetSize = {\n width: definition.defaultSize.width,\n height: definition.defaultSize.height,\n };\n\n const { x: gridX, y: gridY } = calculatePositionFromDrop(\n e.clientX,\n e.clientY,\n canvasRect,\n containerWidth,\n widgetSize,\n { cols: 12 }\n );\n\n const position = {\n x: gridX,\n y: gridY,\n width: widgetSize.width,\n height: widgetSize.height,\n };\n\n // Store the drop position, type, and full definition, then open the config dialog\n setPendingDropPosition(position);\n setPendingDropType(widgetData.type as WidgetType);\n setPendingDropDefinition(droppedDefinition);\n setIsAddDialogOpen(true);\n } catch (error) {\n console.error('Failed to handle widget drop:', error);\n }\n },\n [containerWidth]\n );\n\n // Handle drag over to allow drop\n const handleCanvasDragOver = useCallback((e: React.DragEvent) => {\n e.preventDefault();\n e.dataTransfer.dropEffect = 'copy';\n }, []);\n\n // Handle widget select from palette (quick add without drag)\n const handlePaletteWidgetSelect = useCallback(\n (type: WidgetType, paletteDefinition?: WidgetDefinition) => {\n const definition = paletteDefinition || getWidgetDefinition(type);\n handleAddWidget({\n type,\n title: definition.name,\n config: definition.defaultConfig,\n });\n setIsPaletteOpen(false);\n },\n [handleAddWidget]\n );\n\n // Toggle palette\n const togglePalette = useCallback(() => {\n setIsPaletteOpen((prev) => !prev);\n }, []);\n\n // Board-pack export (PNG + PDF) — rasterizes the inner board element.\n const {\n exportPNG,\n exportPDF,\n isExporting: isExportingImage,\n } = useDashboardImageExport({ containerRef: boardRef, name: title || 'dashboard' });\n const handleExportImage = useCallback(() => {\n void exportPNG();\n }, [exportPNG]);\n const handleExportPdf = useCallback(() => {\n void exportPDF();\n }, [exportPDF]);\n\n return (\n <div className=\"flex flex-col h-full min-w-0 overflow-hidden\">\n {/* Toolbar */}\n <DashboardToolbar\n dashboardId={dashboardId}\n title={title}\n onAddWidget={() => setIsAddDialogOpen(true)}\n onTogglePalette={togglePalette}\n isPaletteOpen={isPaletteOpen}\n onPaste={onPaste}\n onSave={onSave}\n isSaving={isSaving}\n onExport={onExport}\n onExportImage={handleExportImage}\n onExportPdf={handleExportPdf}\n isExportingImage={isExportingImage}\n onShare={() => setIsShareDialogOpen(true)}\n />\n\n {/* Global Filters Bar */}\n {showFilters && <GlobalFiltersBar dashboardId={dashboardId} onFilterChange={handleFilterChange} collapsible />}\n\n {/* Page Tabs - always show in edit mode so user can add first page */}\n {showPageTabs && (isEditMode || pages.length > 0) && (\n <PageTabsManager\n pages={pages}\n activePageId={activePageId || pages[0]?.id}\n editable={isEditMode}\n onPageSelect={onPageChange || (() => {})}\n onPagesReorder={\n onPagesReorder\n ? (pageIds) => {\n // Convert string[] to DashboardPageTab[] for the callback\n const reorderedPages = pageIds.map((id) => pages.find((p) => p.id === id)!).filter(Boolean);\n onPagesReorder(reorderedPages);\n }\n : undefined\n }\n onPageCreate={onPageAdd}\n onPageRename={onPageRename}\n onPageDuplicate={onPageDuplicate}\n onPageDelete={onPageDelete}\n />\n )}\n\n {/* Main content area with palette and canvas */}\n <div className=\"flex flex-1 min-w-0 overflow-hidden\">\n {/* Widget Palette Sidebar */}\n {isEditMode && (\n <WidgetPalette\n isOpen={isPaletteOpen}\n onClose={() => setIsPaletteOpen(false)}\n onWidgetSelect={handlePaletteWidgetSelect}\n onDragStart={handlePaletteDragStart}\n onDragEnd={handlePaletteDragEnd}\n installedWidgets={installedWidgets}\n installedWidgetsLoading={installedWidgetsLoading}\n definitions={widgetCatalog}\n />\n )}\n\n {/* Canvas area */}\n <div\n ref={containerRef}\n className=\"flex-1 min-w-0 overflow-auto bg-bg-muted\"\n onDrop={handleCanvasDrop}\n onDragOver={handleCanvasDragOver}\n onClick={() => {\n if (isEditMode) {\n selectWidget(null);\n }\n }}\n style={{\n position: 'relative',\n border: isDraggingWidget ? '2px dashed var(--color-action-primary-bg)' : 'none',\n backgroundColor: isDraggingWidget ? 'var(--color-bg-hover)' : undefined,\n transition: 'border-color 0.2s, background-color 0.2s',\n }}\n >\n {/* Drop zone indicator */}\n {isDraggingWidget && (\n <div\n style={{\n position: 'absolute',\n top: '50%',\n left: '50%',\n transform: 'translate(-50%, -50%)',\n padding: 'var(--space-pagePadding)',\n backgroundColor: 'var(--color-bg-surface)',\n borderRadius: 'var(--radius-card)',\n boxShadow: 'var(--shadow-elevation-2)',\n textAlign: 'center',\n pointerEvents: 'none',\n zIndex: 100,\n }}\n >\n <p style={{ fontSize: 'var(--font-size-body-md)', color: 'var(--color-text-primary)', margin: 0 }}>\n Drop to add <strong>{dragWidgetType}</strong> widget\n </p>\n </div>\n )}\n\n <div\n ref={boardRef}\n className=\"min-h-full origin-top-left transition-transform\"\n style={{\n transform: `scale(${zoomLevel / 100})`,\n width: `${100 / (zoomLevel / 100)}%`,\n }}\n >\n {widgetsArray.length > 0 ? (\n <CrossFilterProvider\n enabled={enableCrossFilter}\n onCrossFilter={(_filters) => {\n // Cross-filter applied - widget queries can access filter values\n }}\n >\n <DashboardGrid\n widgets={widgetsArray}\n isEditMode={isEditMode}\n width={containerWidth}\n onLayoutChange={handleLayoutChange}\n onWidgetSelect={handleWidgetSelect}\n selectedWidgetId={selectedWidgetId}\n />\n </CrossFilterProvider>\n ) : (\n <div className=\"flex flex-col items-center justify-center min-h-[400px] p-8\">\n <div className=\"w-16 h-16 mb-4 flex items-center justify-center rounded-full bg-bg-surface\">\n <svg className=\"w-8 h-8 text-text-secondary\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n <path\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n strokeWidth={2}\n d=\"M4 5a1 1 0 011-1h14a1 1 0 011 1v2a1 1 0 01-1 1H5a1 1 0 01-1-1V5zM4 13a1 1 0 011-1h6a1 1 0 011 1v6a1 1 0 01-1 1H5a1 1 0 01-1-1v-6zM16 13a1 1 0 011-1h2a1 1 0 011 1v6a1 1 0 01-1 1h-2a1 1 0 01-1-1v-6z\"\n />\n </svg>\n </div>\n <h3 className=\"text-lg font-medium text-text-primary mb-2\">No widgets yet</h3>\n <p className=\"text-sm text-text-secondary mb-4 text-center max-w-[300px]\">\n Add widgets to build your dashboard. Click the &quot;Add Widget&quot; button to get started.\n </p>\n {isEditMode && (\n <button\n type=\"button\"\n onClick={() => setIsAddDialogOpen(true)}\n className=\"\n inline-flex items-center gap-2\n px-4 py-2\n text-sm font-medium\n text-action-primary-fg\n bg-action-primary-bg\n hover:bg-action-primary-bgHover\n rounded-md\n transition-colors\n \"\n >\n <svg className=\"w-4 h-4\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n <path strokeLinecap=\"round\" strokeLinejoin=\"round\" strokeWidth={2} d=\"M12 4v16m8-8H4\" />\n </svg>\n Add Widget\n </button>\n )}\n </div>\n )}\n </div>\n </div>\n </div>\n\n {/* Properties Panel */}\n <PropertiesPanel isOpen={configPanelOpen} onClose={closeConfigPanel} />\n\n {/* Add Widget Dialog */}\n <WidgetConfigDialog\n isOpen={isAddDialogOpen}\n onClose={() => {\n setIsAddDialogOpen(false);\n // Clear pending drop state when dialog is closed\n setPendingDropPosition(null);\n setPendingDropType(null);\n setPendingDropDefinition(null);\n }}\n onSave={handleAddWidget}\n preselectedType={pendingDropType || undefined}\n preselectedDefinition={pendingDropDefinition || undefined}\n dashboardId={dashboardId}\n installedWidgets={installedWidgets}\n />\n\n {/* Share Dialog */}\n <ShareDialog\n dashboardId={dashboardId}\n dashboardName={title || 'Dashboard'}\n isOpen={isShareDialogOpen}\n onClose={() => setIsShareDialogOpen(false)}\n />\n </div>\n );\n});\n\nexport default DashboardCanvas;\n"],"mappings":"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"}
@@ -4,129 +4,149 @@ import "../../store/index.js";
4
4
  import n from "./SavedViewsDropdown/SavedViewsDropdown.js";
5
5
  import "./SavedViewsDropdown/index.js";
6
6
  import { memo as r, useCallback as i } from "react";
7
- import { Clipboard as a, Download as o, Eye as s, Minus as c, PanelLeft as l, PanelLeftClose as u, Pencil as d, Plus as f, RotateCcw as p, Save as m, Share2 as h } from "lucide-react";
8
- import { Fragment as g, jsx as _, jsxs as v } from "react/jsx-runtime";
9
- import { Button as y, Spinner as b, Tooltip as x, TooltipContent as S, TooltipProvider as C, TooltipTrigger as w } from "@burdenoff/fe-libs/ui";
7
+ import { Clipboard as a, Download as o, Eye as s, FileText as c, Image as l, Minus as u, PanelLeft as d, PanelLeftClose as f, Pencil as p, Plus as m, RotateCcw as h, Save as g, Share2 as _ } from "lucide-react";
8
+ import { Fragment as v, jsx as y, jsxs as b } from "react/jsx-runtime";
9
+ import { Button as x, Spinner as S, Tooltip as C, TooltipContent as w, TooltipProvider as T, TooltipTrigger as E } from "@burdenoff/fe-libs/ui";
10
10
  //#region src/bigconsole/components/dashboard/DashboardToolbar.tsx
11
- var T = r(function({ dashboardId: r, title: T, onAddWidget: E, onTogglePalette: D, isPaletteOpen: O = !1, onPaste: k, onSave: A, isSaving: j = !1, onExport: M, onShare: N }) {
12
- let P = t((e) => e.viewMode === "edit"), F = t((e) => e.toggleViewMode), I = t((e) => e.zoomLevel), L = t((e) => e.setZoomLevel), R = !!e((e) => e.clipboardWidgetId), z = i(() => {
13
- R && k && k();
14
- }, [R, k]);
15
- return /* @__PURE__ */ _(C, { children: /* @__PURE__ */ v("div", {
11
+ var D = r(function({ dashboardId: r, title: D, onAddWidget: O, onTogglePalette: k, isPaletteOpen: A = !1, onPaste: j, onSave: M, isSaving: N = !1, onExport: P, onExportImage: F, onExportPdf: I, isExportingImage: L = !1, onShare: R }) {
12
+ let z = t((e) => e.viewMode === "edit"), B = t((e) => e.toggleViewMode), V = t((e) => e.zoomLevel), H = t((e) => e.setZoomLevel), U = !!e((e) => e.clipboardWidgetId), W = i(() => {
13
+ U && j && j();
14
+ }, [U, j]);
15
+ return /* @__PURE__ */ y(T, { children: /* @__PURE__ */ b("div", {
16
16
  className: "flex flex-wrap items-center justify-between gap-x-4 gap-y-2 px-4 py-2 bg-background border-b",
17
17
  children: [
18
- /* @__PURE__ */ v("div", {
18
+ /* @__PURE__ */ b("div", {
19
19
  className: "flex min-w-0 items-center gap-4",
20
- children: [T && /* @__PURE__ */ _("h1", {
20
+ children: [D && /* @__PURE__ */ y("h1", {
21
21
  className: "min-w-0 truncate text-lg font-semibold",
22
- title: T,
23
- children: T
24
- }), r && /* @__PURE__ */ _(n, { dashboardId: r })]
22
+ title: D,
23
+ children: D
24
+ }), r && /* @__PURE__ */ y(n, { dashboardId: r })]
25
25
  }),
26
- /* @__PURE__ */ v("div", {
26
+ /* @__PURE__ */ b("div", {
27
27
  className: "flex flex-shrink-0 items-center gap-1",
28
28
  children: [
29
- /* @__PURE__ */ v(x, { children: [/* @__PURE__ */ _(w, {
29
+ /* @__PURE__ */ b(C, { children: [/* @__PURE__ */ y(E, {
30
30
  asChild: !0,
31
- children: /* @__PURE__ */ _(y, {
31
+ children: /* @__PURE__ */ y(x, {
32
32
  variant: "ghost",
33
33
  size: "icon-sm",
34
- onClick: () => L(Math.max(50, I - 10)),
35
- disabled: I <= 50,
36
- children: /* @__PURE__ */ _(c, { className: "size-4" })
34
+ onClick: () => H(Math.max(50, V - 10)),
35
+ disabled: V <= 50,
36
+ children: /* @__PURE__ */ y(u, { className: "size-4" })
37
37
  })
38
- }), /* @__PURE__ */ _(S, { children: "Zoom out" })] }),
39
- /* @__PURE__ */ v("span", {
38
+ }), /* @__PURE__ */ y(w, { children: "Zoom out" })] }),
39
+ /* @__PURE__ */ b("span", {
40
40
  className: "text-sm text-muted-foreground w-12 text-center",
41
- children: [I, "%"]
41
+ children: [V, "%"]
42
42
  }),
43
- /* @__PURE__ */ v(x, { children: [/* @__PURE__ */ _(w, {
43
+ /* @__PURE__ */ b(C, { children: [/* @__PURE__ */ y(E, {
44
44
  asChild: !0,
45
- children: /* @__PURE__ */ _(y, {
45
+ children: /* @__PURE__ */ y(x, {
46
46
  variant: "ghost",
47
47
  size: "icon-sm",
48
- onClick: () => L(Math.min(200, I + 10)),
49
- disabled: I >= 200,
50
- children: /* @__PURE__ */ _(f, { className: "size-4" })
48
+ onClick: () => H(Math.min(200, V + 10)),
49
+ disabled: V >= 200,
50
+ children: /* @__PURE__ */ y(m, { className: "size-4" })
51
51
  })
52
- }), /* @__PURE__ */ _(S, { children: "Zoom in" })] }),
53
- /* @__PURE__ */ v(x, { children: [/* @__PURE__ */ _(w, {
52
+ }), /* @__PURE__ */ y(w, { children: "Zoom in" })] }),
53
+ /* @__PURE__ */ b(C, { children: [/* @__PURE__ */ y(E, {
54
54
  asChild: !0,
55
- children: /* @__PURE__ */ v(y, {
55
+ children: /* @__PURE__ */ b(x, {
56
56
  variant: "ghost",
57
57
  size: "sm",
58
- onClick: () => L(100),
59
- children: [/* @__PURE__ */ _(p, { className: "size-3.5 mr-1" }), "Reset"]
58
+ onClick: () => H(100),
59
+ children: [/* @__PURE__ */ y(h, { className: "size-3.5 mr-1" }), "Reset"]
60
60
  })
61
- }), /* @__PURE__ */ _(S, { children: "Reset zoom to 100%" })] })
61
+ }), /* @__PURE__ */ y(w, { children: "Reset zoom to 100%" })] })
62
62
  ]
63
63
  }),
64
- /* @__PURE__ */ v("div", {
64
+ /* @__PURE__ */ b("div", {
65
65
  className: "flex flex-wrap items-center justify-end gap-2",
66
66
  children: [
67
- P && /* @__PURE__ */ v(g, { children: [
68
- D && /* @__PURE__ */ v(x, { children: [/* @__PURE__ */ _(w, {
67
+ z && /* @__PURE__ */ b(v, { children: [
68
+ k && /* @__PURE__ */ b(C, { children: [/* @__PURE__ */ y(E, {
69
69
  asChild: !0,
70
- children: /* @__PURE__ */ v(y, {
71
- variant: O ? "secondary" : "outline",
70
+ children: /* @__PURE__ */ b(x, {
71
+ variant: A ? "secondary" : "outline",
72
72
  size: "sm",
73
- onClick: D,
74
- children: [_(O ? u : l, { className: "size-4" }), "Widgets"]
73
+ onClick: k,
74
+ children: [y(A ? f : d, { className: "size-4" }), "Widgets"]
75
75
  })
76
- }), /* @__PURE__ */ _(S, { children: O ? "Hide widget palette" : "Show widget palette for drag & drop" })] }),
77
- /* @__PURE__ */ v(x, { children: [/* @__PURE__ */ _(w, {
76
+ }), /* @__PURE__ */ y(w, { children: A ? "Hide widget palette" : "Show widget palette for drag & drop" })] }),
77
+ /* @__PURE__ */ b(C, { children: [/* @__PURE__ */ y(E, {
78
78
  asChild: !0,
79
- children: /* @__PURE__ */ v(y, {
79
+ children: /* @__PURE__ */ b(x, {
80
80
  variant: "ghost",
81
81
  size: "sm",
82
- onClick: z,
83
- disabled: !R,
84
- children: [/* @__PURE__ */ _(a, { className: "size-4" }), "Paste"]
82
+ onClick: W,
83
+ disabled: !U,
84
+ children: [/* @__PURE__ */ y(a, { className: "size-4" }), "Paste"]
85
85
  })
86
- }), /* @__PURE__ */ _(S, { children: "Paste widget from clipboard" })] }),
87
- /* @__PURE__ */ v(y, {
86
+ }), /* @__PURE__ */ y(w, { children: "Paste widget from clipboard" })] }),
87
+ /* @__PURE__ */ b(x, {
88
88
  size: "sm",
89
- onClick: E,
90
- children: [/* @__PURE__ */ _(f, { className: "size-4" }), "Add Widget"]
89
+ onClick: O,
90
+ children: [/* @__PURE__ */ y(m, { className: "size-4" }), "Add Widget"]
91
91
  })
92
92
  ] }),
93
- /* @__PURE__ */ _(y, {
94
- variant: P ? "secondary" : "outline",
93
+ /* @__PURE__ */ y(x, {
94
+ variant: z ? "secondary" : "outline",
95
95
  size: "sm",
96
- onClick: F,
97
- children: P ? /* @__PURE__ */ v(g, { children: [/* @__PURE__ */ _(s, { className: "size-4" }), "Preview"] }) : /* @__PURE__ */ v(g, { children: [/* @__PURE__ */ _(d, { className: "size-4" }), "Edit"] })
96
+ onClick: B,
97
+ children: z ? /* @__PURE__ */ b(v, { children: [/* @__PURE__ */ y(s, { className: "size-4" }), "Preview"] }) : /* @__PURE__ */ b(v, { children: [/* @__PURE__ */ y(p, { className: "size-4" }), "Edit"] })
98
98
  }),
99
- A && /* @__PURE__ */ _(y, {
99
+ M && /* @__PURE__ */ y(x, {
100
100
  variant: "outline",
101
101
  size: "sm",
102
- onClick: A,
103
- disabled: j,
104
- children: j ? /* @__PURE__ */ v(g, { children: [/* @__PURE__ */ _(b, { className: "size-4" }), "Saving..."] }) : /* @__PURE__ */ v(g, { children: [/* @__PURE__ */ _(m, { className: "size-4" }), "Save"] })
102
+ onClick: M,
103
+ disabled: N,
104
+ children: N ? /* @__PURE__ */ b(v, { children: [/* @__PURE__ */ y(S, { className: "size-4" }), "Saving..."] }) : /* @__PURE__ */ b(v, { children: [/* @__PURE__ */ y(g, { className: "size-4" }), "Save"] })
105
105
  }),
106
- N && /* @__PURE__ */ v(x, { children: [/* @__PURE__ */ _(w, {
106
+ R && /* @__PURE__ */ b(C, { children: [/* @__PURE__ */ y(E, {
107
107
  asChild: !0,
108
- children: /* @__PURE__ */ v(y, {
108
+ children: /* @__PURE__ */ b(x, {
109
109
  variant: "outline",
110
110
  size: "sm",
111
- onClick: N,
112
- children: [/* @__PURE__ */ _(h, { className: "size-4" }), "Share"]
111
+ onClick: R,
112
+ children: [/* @__PURE__ */ y(_, { className: "size-4" }), "Share"]
113
113
  })
114
- }), /* @__PURE__ */ _(S, { children: "Share dashboard with others" })] }),
115
- M && /* @__PURE__ */ v(x, { children: [/* @__PURE__ */ _(w, {
114
+ }), /* @__PURE__ */ y(w, { children: "Share dashboard with others" })] }),
115
+ P && /* @__PURE__ */ b(C, { children: [/* @__PURE__ */ y(E, {
116
116
  asChild: !0,
117
- children: /* @__PURE__ */ v(y, {
117
+ children: /* @__PURE__ */ b(x, {
118
118
  variant: "outline",
119
119
  size: "sm",
120
- onClick: M,
121
- children: [/* @__PURE__ */ _(o, { className: "size-4" }), "Export"]
120
+ onClick: P,
121
+ children: [/* @__PURE__ */ y(o, { className: "size-4" }), "Export"]
122
122
  })
123
- }), /* @__PURE__ */ _(S, { children: "Export dashboard to JSON" })] })
123
+ }), /* @__PURE__ */ y(w, { children: "Export dashboard to JSON" })] }),
124
+ F && /* @__PURE__ */ b(C, { children: [/* @__PURE__ */ y(E, {
125
+ asChild: !0,
126
+ children: /* @__PURE__ */ y(x, {
127
+ variant: "outline",
128
+ size: "icon-sm",
129
+ onClick: F,
130
+ disabled: L,
131
+ children: y(L ? S : l, { className: "size-4" })
132
+ })
133
+ }), /* @__PURE__ */ y(w, { children: "Export board as PNG image" })] }),
134
+ I && /* @__PURE__ */ b(C, { children: [/* @__PURE__ */ y(E, {
135
+ asChild: !0,
136
+ children: /* @__PURE__ */ y(x, {
137
+ variant: "outline",
138
+ size: "icon-sm",
139
+ onClick: I,
140
+ disabled: L,
141
+ children: /* @__PURE__ */ y(c, { className: "size-4" })
142
+ })
143
+ }), /* @__PURE__ */ y(w, { children: "Export board as PDF" })] })
124
144
  ]
125
145
  })
126
146
  ]
127
147
  }) });
128
148
  });
129
149
  //#endregion
130
- export { T as default };
150
+ export { D as default };
131
151
 
132
152
  //# sourceMappingURL=DashboardToolbar.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"DashboardToolbar.js","names":[],"sources":["../../../../src/bigconsole/components/dashboard/DashboardToolbar.tsx"],"sourcesContent":["/**\n * DashboardToolbar Component\n *\n * Toolbar for dashboard actions and controls.\n * Uses fe-libs shared components for consistent styling.\n * Phase 2: Added SavedViewsDropdown and Share button.\n */\n\nimport { type FC, memo, useCallback } from 'react';\nimport { Button } from '@burdenoff/fe-libs/ui';\nimport { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@burdenoff/fe-libs/ui';\nimport { Spinner } from '@burdenoff/fe-libs/ui';\nimport {\n Plus,\n Clipboard,\n Eye,\n Pencil,\n Save,\n Download,\n Minus,\n RotateCcw,\n Share2,\n LayoutGrid,\n PanelLeftClose,\n PanelLeft,\n} from 'lucide-react';\nimport { useDashboardStore, useWidgetStore } from '../../store';\nimport { SavedViewsDropdown } from './SavedViewsDropdown';\n\n// ============================================================================\n// Types\n// ============================================================================\n\nexport interface DashboardToolbarProps {\n /** Dashboard ID for saved views */\n dashboardId?: string;\n /** Dashboard title */\n title?: string;\n /** Callback to add widget */\n onAddWidget: () => void;\n /** Callback to toggle widget palette */\n onTogglePalette?: () => void;\n /** Whether palette is open */\n isPaletteOpen?: boolean;\n /** Callback to paste widget */\n onPaste?: () => void;\n /** Callback to save dashboard */\n onSave?: () => void;\n /** Whether save is in progress */\n isSaving?: boolean;\n /** Callback to export dashboard */\n onExport?: () => void;\n /** Callback to open share dialog */\n onShare?: () => void;\n}\n\n// ============================================================================\n// Component\n// ============================================================================\n\nexport const DashboardToolbar: FC<DashboardToolbarProps> = memo(function DashboardToolbar({\n dashboardId,\n title,\n onAddWidget,\n onTogglePalette,\n isPaletteOpen = false,\n onPaste,\n onSave,\n isSaving = false,\n onExport,\n onShare,\n}) {\n const isEditMode = useDashboardStore((state) => state.viewMode === 'edit');\n const toggleViewMode = useDashboardStore((state) => state.toggleViewMode);\n const zoomLevel = useDashboardStore((state) => state.zoomLevel);\n const setZoomLevel = useDashboardStore((state) => state.setZoomLevel);\n\n const clipboardWidgetId = useWidgetStore((state) => state.clipboardWidgetId);\n const hasClipboard = !!clipboardWidgetId;\n\n // Handle paste - use provided callback or fallback\n const handlePaste = useCallback(() => {\n if (hasClipboard && onPaste) {\n onPaste();\n }\n }, [hasClipboard, onPaste]);\n\n return (\n <TooltipProvider>\n <div className=\"flex flex-wrap items-center justify-between gap-x-4 gap-y-2 px-4 py-2 bg-background border-b\">\n {/* Left section */}\n <div className=\"flex min-w-0 items-center gap-4\">\n {title && (\n <h1 className=\"min-w-0 truncate text-lg font-semibold\" title={title}>\n {title}\n </h1>\n )}\n {/* Saved Views Dropdown */}\n {dashboardId && <SavedViewsDropdown dashboardId={dashboardId} />}\n </div>\n\n {/* Center section - Zoom controls */}\n <div className=\"flex flex-shrink-0 items-center gap-1\">\n <Tooltip>\n <TooltipTrigger asChild>\n <Button\n variant=\"ghost\"\n size=\"icon-sm\"\n onClick={() => setZoomLevel(Math.max(50, zoomLevel - 10))}\n disabled={zoomLevel <= 50}\n >\n <Minus className=\"size-4\" />\n </Button>\n </TooltipTrigger>\n <TooltipContent>Zoom out</TooltipContent>\n </Tooltip>\n\n <span className=\"text-sm text-muted-foreground w-12 text-center\">{zoomLevel}%</span>\n\n <Tooltip>\n <TooltipTrigger asChild>\n <Button\n variant=\"ghost\"\n size=\"icon-sm\"\n onClick={() => setZoomLevel(Math.min(200, zoomLevel + 10))}\n disabled={zoomLevel >= 200}\n >\n <Plus className=\"size-4\" />\n </Button>\n </TooltipTrigger>\n <TooltipContent>Zoom in</TooltipContent>\n </Tooltip>\n\n <Tooltip>\n <TooltipTrigger asChild>\n <Button variant=\"ghost\" size=\"sm\" onClick={() => setZoomLevel(100)}>\n <RotateCcw className=\"size-3.5 mr-1\" />\n Reset\n </Button>\n </TooltipTrigger>\n <TooltipContent>Reset zoom to 100%</TooltipContent>\n </Tooltip>\n </div>\n\n {/* Right section - Actions */}\n <div className=\"flex flex-wrap items-center justify-end gap-2\">\n {isEditMode && (\n <>\n {/* Widget Palette Toggle */}\n {onTogglePalette && (\n <Tooltip>\n <TooltipTrigger asChild>\n <Button variant={isPaletteOpen ? 'secondary' : 'outline'} size=\"sm\" onClick={onTogglePalette}>\n {isPaletteOpen ? <PanelLeftClose className=\"size-4\" /> : <PanelLeft className=\"size-4\" />}\n Widgets\n </Button>\n </TooltipTrigger>\n <TooltipContent>\n {isPaletteOpen ? 'Hide widget palette' : 'Show widget palette for drag & drop'}\n </TooltipContent>\n </Tooltip>\n )}\n\n {/* Paste */}\n <Tooltip>\n <TooltipTrigger asChild>\n <Button variant=\"ghost\" size=\"sm\" onClick={handlePaste} disabled={!hasClipboard}>\n <Clipboard className=\"size-4\" />\n Paste\n </Button>\n </TooltipTrigger>\n <TooltipContent>Paste widget from clipboard</TooltipContent>\n </Tooltip>\n\n {/* Add Widget (Dialog) */}\n <Button size=\"sm\" onClick={onAddWidget}>\n <Plus className=\"size-4\" />\n Add Widget\n </Button>\n </>\n )}\n\n {/* Edit/View Mode Toggle */}\n <Button variant={isEditMode ? 'secondary' : 'outline'} size=\"sm\" onClick={toggleViewMode}>\n {isEditMode ? (\n <>\n <Eye className=\"size-4\" />\n Preview\n </>\n ) : (\n <>\n <Pencil className=\"size-4\" />\n Edit\n </>\n )}\n </Button>\n\n {/* Save */}\n {onSave && (\n <Button variant=\"outline\" size=\"sm\" onClick={onSave} disabled={isSaving}>\n {isSaving ? (\n <>\n <Spinner className=\"size-4\" />\n Saving...\n </>\n ) : (\n <>\n <Save className=\"size-4\" />\n Save\n </>\n )}\n </Button>\n )}\n\n {/* Share */}\n {onShare && (\n <Tooltip>\n <TooltipTrigger asChild>\n <Button variant=\"outline\" size=\"sm\" onClick={onShare}>\n <Share2 className=\"size-4\" />\n Share\n </Button>\n </TooltipTrigger>\n <TooltipContent>Share dashboard with others</TooltipContent>\n </Tooltip>\n )}\n\n {/* Export */}\n {onExport && (\n <Tooltip>\n <TooltipTrigger asChild>\n <Button variant=\"outline\" size=\"sm\" onClick={onExport}>\n <Download className=\"size-4\" />\n Export\n </Button>\n </TooltipTrigger>\n <TooltipContent>Export dashboard to JSON</TooltipContent>\n </Tooltip>\n )}\n </div>\n </div>\n </TooltipProvider>\n );\n});\n\nexport default DashboardToolbar;\n"],"mappings":"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"}
1
+ {"version":3,"file":"DashboardToolbar.js","names":[],"sources":["../../../../src/bigconsole/components/dashboard/DashboardToolbar.tsx"],"sourcesContent":["/**\n * DashboardToolbar Component\n *\n * Toolbar for dashboard actions and controls.\n * Uses fe-libs shared components for consistent styling.\n * Phase 2: Added SavedViewsDropdown and Share button.\n */\n\nimport { type FC, memo, useCallback } from 'react';\nimport { Button } from '@burdenoff/fe-libs/ui';\nimport { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@burdenoff/fe-libs/ui';\nimport { Spinner } from '@burdenoff/fe-libs/ui';\nimport {\n Plus,\n Clipboard,\n Eye,\n Pencil,\n Save,\n Download,\n Minus,\n RotateCcw,\n Share2,\n PanelLeftClose,\n PanelLeft,\n Image as ImageIcon,\n FileText,\n} from 'lucide-react';\nimport { useDashboardStore, useWidgetStore } from '../../store';\nimport { SavedViewsDropdown } from './SavedViewsDropdown';\n\n// ============================================================================\n// Types\n// ============================================================================\n\nexport interface DashboardToolbarProps {\n /** Dashboard ID for saved views */\n dashboardId?: string;\n /** Dashboard title */\n title?: string;\n /** Callback to add widget */\n onAddWidget: () => void;\n /** Callback to toggle widget palette */\n onTogglePalette?: () => void;\n /** Whether palette is open */\n isPaletteOpen?: boolean;\n /** Callback to paste widget */\n onPaste?: () => void;\n /** Callback to save dashboard */\n onSave?: () => void;\n /** Whether save is in progress */\n isSaving?: boolean;\n /** Callback to export dashboard (JSON console definition) */\n onExport?: () => void;\n /** Callback to export the board as a PNG image */\n onExportImage?: () => void;\n /** Callback to export the board as a PDF */\n onExportPdf?: () => void;\n /** Whether an image/PDF export is in progress */\n isExportingImage?: boolean;\n /** Callback to open share dialog */\n onShare?: () => void;\n}\n\n// ============================================================================\n// Component\n// ============================================================================\n\nexport const DashboardToolbar: FC<DashboardToolbarProps> = memo(function DashboardToolbar({\n dashboardId,\n title,\n onAddWidget,\n onTogglePalette,\n isPaletteOpen = false,\n onPaste,\n onSave,\n isSaving = false,\n onExport,\n onExportImage,\n onExportPdf,\n isExportingImage = false,\n onShare,\n}) {\n const isEditMode = useDashboardStore((state) => state.viewMode === 'edit');\n const toggleViewMode = useDashboardStore((state) => state.toggleViewMode);\n const zoomLevel = useDashboardStore((state) => state.zoomLevel);\n const setZoomLevel = useDashboardStore((state) => state.setZoomLevel);\n\n const clipboardWidgetId = useWidgetStore((state) => state.clipboardWidgetId);\n const hasClipboard = !!clipboardWidgetId;\n\n // Handle paste - use provided callback or fallback\n const handlePaste = useCallback(() => {\n if (hasClipboard && onPaste) {\n onPaste();\n }\n }, [hasClipboard, onPaste]);\n\n return (\n <TooltipProvider>\n <div className=\"flex flex-wrap items-center justify-between gap-x-4 gap-y-2 px-4 py-2 bg-background border-b\">\n {/* Left section */}\n <div className=\"flex min-w-0 items-center gap-4\">\n {title && (\n <h1 className=\"min-w-0 truncate text-lg font-semibold\" title={title}>\n {title}\n </h1>\n )}\n {/* Saved Views Dropdown */}\n {dashboardId && <SavedViewsDropdown dashboardId={dashboardId} />}\n </div>\n\n {/* Center section - Zoom controls */}\n <div className=\"flex flex-shrink-0 items-center gap-1\">\n <Tooltip>\n <TooltipTrigger asChild>\n <Button\n variant=\"ghost\"\n size=\"icon-sm\"\n onClick={() => setZoomLevel(Math.max(50, zoomLevel - 10))}\n disabled={zoomLevel <= 50}\n >\n <Minus className=\"size-4\" />\n </Button>\n </TooltipTrigger>\n <TooltipContent>Zoom out</TooltipContent>\n </Tooltip>\n\n <span className=\"text-sm text-muted-foreground w-12 text-center\">{zoomLevel}%</span>\n\n <Tooltip>\n <TooltipTrigger asChild>\n <Button\n variant=\"ghost\"\n size=\"icon-sm\"\n onClick={() => setZoomLevel(Math.min(200, zoomLevel + 10))}\n disabled={zoomLevel >= 200}\n >\n <Plus className=\"size-4\" />\n </Button>\n </TooltipTrigger>\n <TooltipContent>Zoom in</TooltipContent>\n </Tooltip>\n\n <Tooltip>\n <TooltipTrigger asChild>\n <Button variant=\"ghost\" size=\"sm\" onClick={() => setZoomLevel(100)}>\n <RotateCcw className=\"size-3.5 mr-1\" />\n Reset\n </Button>\n </TooltipTrigger>\n <TooltipContent>Reset zoom to 100%</TooltipContent>\n </Tooltip>\n </div>\n\n {/* Right section - Actions */}\n <div className=\"flex flex-wrap items-center justify-end gap-2\">\n {isEditMode && (\n <>\n {/* Widget Palette Toggle */}\n {onTogglePalette && (\n <Tooltip>\n <TooltipTrigger asChild>\n <Button variant={isPaletteOpen ? 'secondary' : 'outline'} size=\"sm\" onClick={onTogglePalette}>\n {isPaletteOpen ? <PanelLeftClose className=\"size-4\" /> : <PanelLeft className=\"size-4\" />}\n Widgets\n </Button>\n </TooltipTrigger>\n <TooltipContent>\n {isPaletteOpen ? 'Hide widget palette' : 'Show widget palette for drag & drop'}\n </TooltipContent>\n </Tooltip>\n )}\n\n {/* Paste */}\n <Tooltip>\n <TooltipTrigger asChild>\n <Button variant=\"ghost\" size=\"sm\" onClick={handlePaste} disabled={!hasClipboard}>\n <Clipboard className=\"size-4\" />\n Paste\n </Button>\n </TooltipTrigger>\n <TooltipContent>Paste widget from clipboard</TooltipContent>\n </Tooltip>\n\n {/* Add Widget (Dialog) */}\n <Button size=\"sm\" onClick={onAddWidget}>\n <Plus className=\"size-4\" />\n Add Widget\n </Button>\n </>\n )}\n\n {/* Edit/View Mode Toggle */}\n <Button variant={isEditMode ? 'secondary' : 'outline'} size=\"sm\" onClick={toggleViewMode}>\n {isEditMode ? (\n <>\n <Eye className=\"size-4\" />\n Preview\n </>\n ) : (\n <>\n <Pencil className=\"size-4\" />\n Edit\n </>\n )}\n </Button>\n\n {/* Save */}\n {onSave && (\n <Button variant=\"outline\" size=\"sm\" onClick={onSave} disabled={isSaving}>\n {isSaving ? (\n <>\n <Spinner className=\"size-4\" />\n Saving...\n </>\n ) : (\n <>\n <Save className=\"size-4\" />\n Save\n </>\n )}\n </Button>\n )}\n\n {/* Share */}\n {onShare && (\n <Tooltip>\n <TooltipTrigger asChild>\n <Button variant=\"outline\" size=\"sm\" onClick={onShare}>\n <Share2 className=\"size-4\" />\n Share\n </Button>\n </TooltipTrigger>\n <TooltipContent>Share dashboard with others</TooltipContent>\n </Tooltip>\n )}\n\n {/* Export — JSON definition */}\n {onExport && (\n <Tooltip>\n <TooltipTrigger asChild>\n <Button variant=\"outline\" size=\"sm\" onClick={onExport}>\n <Download className=\"size-4\" />\n Export\n </Button>\n </TooltipTrigger>\n <TooltipContent>Export dashboard to JSON</TooltipContent>\n </Tooltip>\n )}\n\n {/* Export — board PNG */}\n {onExportImage && (\n <Tooltip>\n <TooltipTrigger asChild>\n <Button variant=\"outline\" size=\"icon-sm\" onClick={onExportImage} disabled={isExportingImage}>\n {isExportingImage ? <Spinner className=\"size-4\" /> : <ImageIcon className=\"size-4\" />}\n </Button>\n </TooltipTrigger>\n <TooltipContent>Export board as PNG image</TooltipContent>\n </Tooltip>\n )}\n\n {/* Export — board PDF */}\n {onExportPdf && (\n <Tooltip>\n <TooltipTrigger asChild>\n <Button variant=\"outline\" size=\"icon-sm\" onClick={onExportPdf} disabled={isExportingImage}>\n <FileText className=\"size-4\" />\n </Button>\n </TooltipTrigger>\n <TooltipContent>Export board as PDF</TooltipContent>\n </Tooltip>\n )}\n </div>\n </div>\n </TooltipProvider>\n );\n});\n\nexport default DashboardToolbar;\n"],"mappings":"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"}
@@ -1 +1,92 @@
1
- import "react";
1
+ import { useCallback as e, useState as t } from "react";
2
+ //#region src/bigconsole/hooks/useDashboardImageExport.ts
3
+ function n(e, t) {
4
+ let n = URL.createObjectURL(e), r = document.createElement("a");
5
+ r.href = n, r.download = t, r.click(), URL.revokeObjectURL(n);
6
+ }
7
+ function r(e) {
8
+ let t = getComputedStyle(e).backgroundColor;
9
+ return !t || t === "transparent" || /,\s*0\)\s*$/.test(t) ? getComputedStyle(document.documentElement).getPropertyValue("--color-bg-surface").trim() || "transparent" : t;
10
+ }
11
+ async function i(e) {
12
+ if (typeof window < "u" && "html2canvas" in window) return await window.html2canvas(e, {
13
+ backgroundColor: r(e),
14
+ scale: 2,
15
+ useCORS: !0
16
+ });
17
+ let t = document.createElement("canvas"), n = t.getContext("2d");
18
+ if (!n) throw Error("Could not get canvas context");
19
+ let i = e.getBoundingClientRect();
20
+ t.width = i.width * 2, t.height = i.height * 2, n.scale(2, 2), n.fillStyle = r(e), n.fillRect(0, 0, i.width, i.height);
21
+ let a = e.querySelectorAll("svg");
22
+ for (let e of a) {
23
+ let t = new XMLSerializer().serializeToString(e), r = new Blob([t], { type: "image/svg+xml;charset=utf-8" }), a = URL.createObjectURL(r);
24
+ try {
25
+ await new Promise((t) => {
26
+ let r = new Image();
27
+ r.onload = () => {
28
+ let a = e.getBoundingClientRect();
29
+ n.drawImage(r, a.left - i.left, a.top - i.top), t();
30
+ }, r.onerror = () => t(), r.src = a;
31
+ });
32
+ } finally {
33
+ URL.revokeObjectURL(a);
34
+ }
35
+ }
36
+ return t;
37
+ }
38
+ function a(r) {
39
+ let { containerRef: a, name: o = "dashboard" } = r, [s, c] = t(!1);
40
+ return {
41
+ exportPNG: e(async (e) => {
42
+ let t = a.current;
43
+ if (!(!t || s)) {
44
+ c(!0);
45
+ try {
46
+ let r = await i(t), a = await new Promise((e, t) => {
47
+ r.toBlob((n) => n ? e(n) : t(/* @__PURE__ */ Error("Failed to create image blob")), "image/png");
48
+ }), s = o.replace(/\s+/g, "_").toLowerCase();
49
+ n(a, e || `${s}_board.png`);
50
+ } finally {
51
+ c(!1);
52
+ }
53
+ }
54
+ }, [
55
+ a,
56
+ o,
57
+ s
58
+ ]),
59
+ exportPDF: e(async () => {
60
+ let e = a.current;
61
+ if (!(!e || s)) {
62
+ c(!0);
63
+ try {
64
+ let t = await i(e), n = t.toDataURL("image/png"), r = `${o} — board`, a = window.open("", "_blank");
65
+ if (!a) {
66
+ window.print();
67
+ return;
68
+ }
69
+ let s = t.width >= t.height ? "landscape" : "portrait", c = a.document;
70
+ c.title = r;
71
+ let l = c.createElement("style");
72
+ l.textContent = `@page{size:${s};margin:12mm}html,body{margin:0;padding:0}img{width:100%;height:auto;display:block}`, c.head.appendChild(l);
73
+ let u = c.createElement("img");
74
+ u.alt = r, u.addEventListener("load", () => {
75
+ a.focus(), a.print();
76
+ }), u.src = n, c.body.appendChild(u);
77
+ } finally {
78
+ c(!1);
79
+ }
80
+ }
81
+ }, [
82
+ a,
83
+ o,
84
+ s
85
+ ]),
86
+ isExporting: s
87
+ };
88
+ }
89
+ //#endregion
90
+ export { a as default };
91
+
92
+ //# sourceMappingURL=useDashboardImageExport.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"useDashboardImageExport.js","names":[],"sources":["../../../src/bigconsole/hooks/useDashboardImageExport.ts"],"sourcesContent":["/**\n * useDashboardImageExport\n *\n * Exports a whole dashboard board to a single PNG \"board-pack\" image by\n * rasterizing the live board container. Dependency-free: prefers a\n * page-provided `html2canvas` (loaded globally by the host shell when present),\n * otherwise falls back to compositing the board's SVG charts onto a canvas —\n * the same strategy `useWidgetExport` uses for single widgets.\n *\n * `exportPDF` rasterizes the board and prints *just that image* from a new\n * window, so the browser's \"Save as PDF\" produces a clean board-pack PDF with\n * no app chrome. This is intentionally dependency-free: this MFE declares its\n * runtime libs as peerDependencies (provided by the host shell), so bundling a\n * PDF engine here would require host-shell coordination. A library-based vector\n * PDF can be layered on later via the host shell if needed.\n */\n\nimport { useCallback, useState, type RefObject } from 'react';\n\nexport interface UseDashboardImageExportOptions {\n /** Ref to the board/grid container element to rasterize. */\n containerRef: RefObject<HTMLElement | null>;\n /** Human-readable dashboard name, used for the download filename. */\n name?: string;\n}\n\nexport interface UseDashboardImageExportResult {\n /** Export the whole board as a single PNG image. */\n exportPNG: (filename?: string) => Promise<void>;\n /** Export the board as a PDF via the browser's print-to-PDF (board image only). */\n exportPDF: () => Promise<void>;\n isExporting: boolean;\n}\n\nfunction downloadBlob(blob: Blob, filename: string): void {\n const url = URL.createObjectURL(blob);\n const a = document.createElement('a');\n a.href = url;\n a.download = filename;\n a.click();\n URL.revokeObjectURL(url);\n}\n\n/**\n * Resolve a concrete background fill for the export canvas. Prefers the\n * element's own computed background, then the resolved `--color-bg-surface`\n * theme token, then a transparent canvas — no raw color literals.\n */\nfunction resolveCanvasBackground(element: HTMLElement): string {\n const own = getComputedStyle(element).backgroundColor;\n // Treat keyword \"transparent\" or a fully-transparent rgba (alpha 0, i.e. a\n // value ending in \", 0)\") as \"no own background\" and fall back to the token.\n const isTransparent = !own || own === 'transparent' || /,\\s*0\\)\\s*$/.test(own);\n if (!isTransparent) return own;\n const token = getComputedStyle(document.documentElement).getPropertyValue('--color-bg-surface').trim();\n return token || 'transparent';\n}\n\n/** Rasterize an element to a canvas (html2canvas when available, SVG composite otherwise). */\nasync function elementToCanvas(element: HTMLElement): Promise<HTMLCanvasElement> {\n if (typeof window !== 'undefined' && 'html2canvas' in window) {\n // @ts-expect-error - html2canvas is loaded dynamically by the host shell\n return (await window.html2canvas(element, {\n backgroundColor: resolveCanvasBackground(element),\n scale: 2,\n useCORS: true,\n })) as HTMLCanvasElement;\n }\n\n const canvas = document.createElement('canvas');\n const ctx = canvas.getContext('2d');\n if (!ctx) throw new Error('Could not get canvas context');\n\n const rect = element.getBoundingClientRect();\n canvas.width = rect.width * 2;\n canvas.height = rect.height * 2;\n ctx.scale(2, 2);\n ctx.fillStyle = resolveCanvasBackground(element);\n ctx.fillRect(0, 0, rect.width, rect.height);\n\n // Composite each SVG chart at its on-screen position.\n const svgs = element.querySelectorAll('svg');\n for (const svg of svgs) {\n const svgData = new XMLSerializer().serializeToString(svg);\n const svgBlob = new Blob([svgData], { type: 'image/svg+xml;charset=utf-8' });\n const url = URL.createObjectURL(svgBlob);\n try {\n await new Promise<void>((resolve) => {\n const img = new Image();\n img.onload = () => {\n const svgRect = svg.getBoundingClientRect();\n ctx.drawImage(img, svgRect.left - rect.left, svgRect.top - rect.top);\n resolve();\n };\n img.onerror = () => resolve();\n img.src = url;\n });\n } finally {\n URL.revokeObjectURL(url);\n }\n }\n return canvas;\n}\n\nexport function useDashboardImageExport(options: UseDashboardImageExportOptions): UseDashboardImageExportResult {\n const { containerRef, name = 'dashboard' } = options;\n const [isExporting, setIsExporting] = useState(false);\n\n const exportPNG = useCallback(\n async (filename?: string) => {\n const el = containerRef.current;\n if (!el || isExporting) return;\n setIsExporting(true);\n try {\n const canvas = await elementToCanvas(el);\n const blob = await new Promise<Blob>((resolve, reject) => {\n canvas.toBlob((b) => (b ? resolve(b) : reject(new Error('Failed to create image blob'))), 'image/png');\n });\n const base = name.replace(/\\s+/g, '_').toLowerCase();\n downloadBlob(blob, filename || `${base}_board.png`);\n } finally {\n setIsExporting(false);\n }\n },\n [containerRef, name, isExporting]\n );\n\n const exportPDF = useCallback(async () => {\n const el = containerRef.current;\n if (!el || isExporting) return;\n setIsExporting(true);\n try {\n const canvas = await elementToCanvas(el);\n const imgData = canvas.toDataURL('image/png');\n const title = `${name} — board`;\n // Print just the board image from an isolated window, so \"Save as PDF\"\n // yields a clean board-pack with no app chrome. Falls back to printing the\n // current page if the popup is blocked. Built via DOM APIs (no\n // document.write) so the user-controlled dashboard `name` can never be\n // interpreted as markup.\n const win = window.open('', '_blank');\n if (!win) {\n window.print();\n return;\n }\n const orientation = canvas.width >= canvas.height ? 'landscape' : 'portrait';\n const doc = win.document;\n doc.title = title; // assigned as text, never parsed as HTML\n const style = doc.createElement('style');\n style.textContent = `@page{size:${orientation};margin:12mm}html,body{margin:0;padding:0}img{width:100%;height:auto;display:block}`;\n doc.head.appendChild(style);\n const img = doc.createElement('img');\n img.alt = title;\n img.addEventListener('load', () => {\n win.focus();\n win.print();\n });\n img.src = imgData; // trusted data: URL from our own canvas\n doc.body.appendChild(img);\n } finally {\n setIsExporting(false);\n }\n }, [containerRef, name, isExporting]);\n\n return { exportPNG, exportPDF, isExporting };\n}\n\nexport default useDashboardImageExport;\n"],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@burdenoff/microfe-bigconsole",
3
- "version": "2026.613.5",
3
+ "version": "2026.613.6",
4
4
  "description": "BigConsole - AI-powered analytics and dashboard platform",
5
5
  "type": "module",
6
6
  "files": [