@burdenoff/microfe-bigconsole 2026.803.1 → 2026.803.2

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.
@@ -2,7 +2,27 @@ import e from "../../../hooks/useDashboardEmbedPolicy.js";
2
2
  import { useEffect as t, useMemo as n, useState as r } from "react";
3
3
  import { Fragment as i, jsx as a, jsxs as o } from "react/jsx-runtime";
4
4
  //#region src/bigconsole/components/dashboard/ShareDialog/EmbedTab.tsx
5
- function s(e) {
5
+ var s = [
6
+ {
7
+ value: "light",
8
+ label: "Light"
9
+ },
10
+ {
11
+ value: "dark",
12
+ label: "Dark"
13
+ },
14
+ {
15
+ value: "auto",
16
+ label: "Auto (match viewer)"
17
+ }
18
+ ];
19
+ function c(e, t) {
20
+ return t === "light" ? e : e.replace(/(?<=\s)src="([^"]*)"/, (e, n) => {
21
+ let r = n.indexOf("#"), i = r === -1 ? n : n.slice(0, r), a = r === -1 ? "" : n.slice(r);
22
+ return `src="${i}${i.includes("?") ? "&" : "?"}theme=${t}${a}"`;
23
+ });
24
+ }
25
+ function l(e) {
6
26
  let t = e.trim();
7
27
  if (!t) return null;
8
28
  try {
@@ -12,65 +32,65 @@ function s(e) {
12
32
  return null;
13
33
  }
14
34
  }
15
- function c({ dashboardId: c, colors: l }) {
16
- let { policy: u, secret: d, loading: f, error: p, fetchPolicy: m, publishToWeb: h, setAllowedOrigins: g, rotateKey: _, unpublish: v } = e(), [y, b] = r(""), [x, S] = r([]), [C, w] = r(!1), [T, E] = r(!1), [D, O] = r(null);
35
+ function u({ dashboardId: u, colors: d }) {
36
+ let { policy: f, secret: p, loading: m, error: h, fetchPolicy: g, publishToWeb: _, setAllowedOrigins: v, rotateKey: y, unpublish: b } = e(), [x, S] = r(""), [C, w] = r([]), [T, E] = r(!1), [D, O] = r(!1), [k, A] = r("light"), [j, M] = r(null);
17
37
  t(() => {
18
- c && m(c);
19
- }, [c, m]);
20
- let k = u?.status === "ACTIVE", A = k ? u?.allowedParentOrigins ?? [] : x, j = n(() => d?.iframeSnippet ? d.iframeSnippet : null, [d]), M = () => {
21
- O(null);
22
- let e = s(y);
38
+ u && g(u);
39
+ }, [u, g]);
40
+ let N = f?.status === "ACTIVE", P = N ? f?.allowedParentOrigins ?? [] : C, F = n(() => p?.iframeSnippet ? c(p.iframeSnippet, k) : null, [p, k]), I = () => {
41
+ M(null);
42
+ let e = l(x);
23
43
  if (!e) {
24
- O("Enter a valid https origin, e.g. https://example.com");
44
+ M("Enter a valid https origin, e.g. https://example.com");
25
45
  return;
26
46
  }
27
- if (A.includes(e)) {
28
- b("");
47
+ if (P.includes(e)) {
48
+ S("");
29
49
  return;
30
50
  }
31
- k && u ? g(u.id, [...A, e]) : S((t) => [...t, e]), b("");
32
- }, N = (e) => {
33
- k && u ? g(u.id, A.filter((t) => t !== e)) : S((t) => t.filter((t) => t !== e));
34
- }, P = async () => {
35
- if (O(null), x.length === 0) {
36
- O("Add at least one allowed website domain before publishing.");
51
+ N && f ? v(f.id, [...P, e]) : w((t) => [...t, e]), S("");
52
+ }, L = (e) => {
53
+ N && f ? v(f.id, P.filter((t) => t !== e)) : w((t) => t.filter((t) => t !== e));
54
+ }, R = async () => {
55
+ if (M(null), C.length === 0) {
56
+ M("Add at least one allowed website domain before publishing.");
37
57
  return;
38
58
  }
39
- if (!C) {
40
- O("Please confirm this dashboard is safe to be public.");
59
+ if (!T) {
60
+ M("Please confirm this dashboard is safe to be public.");
41
61
  return;
42
62
  }
43
- await h(c, x, !0);
44
- }, F = async (e) => {
63
+ await _(u, C, !0);
64
+ }, z = async (e) => {
45
65
  try {
46
- await navigator.clipboard.writeText(e), E(!0), setTimeout(() => E(!1), 1500);
66
+ await navigator.clipboard.writeText(e), O(!0), setTimeout(() => O(!1), 1500);
47
67
  } catch {
48
- O("Could not copy — select and copy manually.");
68
+ M("Could not copy — select and copy manually.");
49
69
  }
50
- }, I = {
70
+ }, B = {
51
71
  padding: "8px 14px",
52
72
  borderRadius: "8px",
53
73
  border: "none",
54
- background: l.actionPrimaryBg,
74
+ background: d.actionPrimaryBg,
55
75
  color: "#fff",
56
76
  fontSize: "13px",
57
77
  fontWeight: 600,
58
78
  cursor: "pointer"
59
- }, L = {
79
+ }, V = {
60
80
  padding: "8px 14px",
61
81
  borderRadius: "8px",
62
- border: `1px solid ${l.borderDefault}`,
82
+ border: `1px solid ${d.borderDefault}`,
63
83
  background: "transparent",
64
- color: l.textPrimary,
84
+ color: d.textPrimary,
65
85
  fontSize: "13px",
66
86
  cursor: "pointer"
67
- }, R = {
87
+ }, H = {
68
88
  flex: 1,
69
89
  padding: "8px 10px",
70
90
  borderRadius: "8px",
71
- border: `1px solid ${l.borderDefault}`,
72
- background: l.bgSecondary,
73
- color: l.textPrimary,
91
+ border: `1px solid ${d.borderDefault}`,
92
+ background: d.bgSecondary,
93
+ color: d.textPrimary,
74
94
  fontSize: "13px"
75
95
  };
76
96
  return /* @__PURE__ */ o("div", {
@@ -83,23 +103,23 @@ function c({ dashboardId: c, colors: l }) {
83
103
  justifyContent: "space-between",
84
104
  padding: "12px 14px",
85
105
  borderRadius: "10px",
86
- background: l.bgSecondary,
106
+ background: d.bgSecondary,
87
107
  marginBottom: "16px"
88
108
  },
89
109
  children: [/* @__PURE__ */ o("div", { children: [/* @__PURE__ */ a("div", {
90
110
  style: {
91
111
  fontSize: "14px",
92
112
  fontWeight: 600,
93
- color: l.textPrimary
113
+ color: d.textPrimary
94
114
  },
95
115
  children: "Publish to the web"
96
116
  }), /* @__PURE__ */ a("div", {
97
117
  style: {
98
118
  fontSize: "12px",
99
- color: l.textSecondary,
119
+ color: d.textSecondary,
100
120
  marginTop: "2px"
101
121
  },
102
- children: k ? "This dashboard is live. Anyone with the link on an allowed website sees its live data." : "Embed this dashboard on your website. Viewers see live data — no login required."
122
+ children: N ? "This dashboard is live. Anyone with the link on an allowed website sees its live data." : "Embed this dashboard on your website. Viewers see live data — no login required."
103
123
  })] }), /* @__PURE__ */ a("span", {
104
124
  style: {
105
125
  fontSize: "11px",
@@ -107,9 +127,9 @@ function c({ dashboardId: c, colors: l }) {
107
127
  padding: "3px 8px",
108
128
  borderRadius: "999px",
109
129
  color: "#fff",
110
- background: k ? l.statusSuccessBg : l.textTertiary
130
+ background: N ? d.statusSuccessBg : d.textTertiary
111
131
  },
112
- children: k ? "PUBLISHED" : "PRIVATE"
132
+ children: N ? "PUBLISHED" : "PRIVATE"
113
133
  })]
114
134
  }),
115
135
  /* @__PURE__ */ o("div", {
@@ -119,7 +139,7 @@ function c({ dashboardId: c, colors: l }) {
119
139
  style: {
120
140
  fontSize: "12px",
121
141
  fontWeight: 600,
122
- color: l.textSecondary,
142
+ color: d.textSecondary,
123
143
  marginBottom: "6px"
124
144
  },
125
145
  children: "Allowed website domains"
@@ -131,24 +151,24 @@ function c({ dashboardId: c, colors: l }) {
131
151
  marginBottom: "8px"
132
152
  },
133
153
  children: [/* @__PURE__ */ a("input", {
134
- style: R,
154
+ style: H,
135
155
  placeholder: "https://yoursite.com",
136
- value: y,
137
- onChange: (e) => b(e.target.value),
156
+ value: x,
157
+ onChange: (e) => S(e.target.value),
138
158
  onKeyDown: (e) => {
139
- e.key === "Enter" && M();
159
+ e.key === "Enter" && I();
140
160
  }
141
161
  }), /* @__PURE__ */ a("button", {
142
- style: L,
143
- onClick: M,
162
+ style: V,
163
+ onClick: I,
144
164
  type: "button",
145
165
  children: "Add"
146
166
  })]
147
167
  }),
148
- A.length === 0 ? /* @__PURE__ */ a("div", {
168
+ P.length === 0 ? /* @__PURE__ */ a("div", {
149
169
  style: {
150
170
  fontSize: "12px",
151
- color: l.textTertiary
171
+ color: d.textTertiary
152
172
  },
153
173
  children: "No domains yet. Only listed https domains may frame this dashboard."
154
174
  }) : /* @__PURE__ */ a("div", {
@@ -157,28 +177,28 @@ function c({ dashboardId: c, colors: l }) {
157
177
  flexDirection: "column",
158
178
  gap: "6px"
159
179
  },
160
- children: A.map((e) => /* @__PURE__ */ o("div", {
180
+ children: P.map((e) => /* @__PURE__ */ o("div", {
161
181
  style: {
162
182
  display: "flex",
163
183
  alignItems: "center",
164
184
  justifyContent: "space-between",
165
185
  padding: "6px 10px",
166
186
  borderRadius: "8px",
167
- background: l.bgTertiary
187
+ background: d.bgTertiary
168
188
  },
169
189
  children: [/* @__PURE__ */ a("span", {
170
190
  style: {
171
191
  fontSize: "13px",
172
- color: l.textPrimary
192
+ color: d.textPrimary
173
193
  },
174
194
  children: e
175
195
  }), /* @__PURE__ */ a("button", {
176
196
  style: {
177
- ...L,
197
+ ...V,
178
198
  padding: "2px 8px",
179
- color: l.statusErrorText
199
+ color: d.statusErrorText
180
200
  },
181
- onClick: () => N(e),
201
+ onClick: () => L(e),
182
202
  type: "button",
183
203
  children: "Remove"
184
204
  })]
@@ -186,7 +206,7 @@ function c({ dashboardId: c, colors: l }) {
186
206
  })
187
207
  ]
188
208
  }),
189
- !k && /* @__PURE__ */ o("div", {
209
+ !N && /* @__PURE__ */ o("div", {
190
210
  style: { marginBottom: "8px" },
191
211
  children: [/* @__PURE__ */ o("label", {
192
212
  style: {
@@ -197,46 +217,85 @@ function c({ dashboardId: c, colors: l }) {
197
217
  },
198
218
  children: [/* @__PURE__ */ a("input", {
199
219
  type: "checkbox",
200
- checked: C,
201
- onChange: (e) => w(e.target.checked)
220
+ checked: T,
221
+ onChange: (e) => E(e.target.checked)
202
222
  }), /* @__PURE__ */ a("span", {
203
223
  style: {
204
224
  fontSize: "12px",
205
- color: l.textSecondary
225
+ color: d.textSecondary
206
226
  },
207
227
  children: "I understand this makes the dashboard's live data visible to anyone with the link, and I confirm it is safe to be public."
208
228
  })]
209
229
  }), /* @__PURE__ */ a("button", {
210
- style: I,
211
- onClick: P,
212
- disabled: f,
230
+ style: B,
231
+ onClick: R,
232
+ disabled: m,
213
233
  type: "button",
214
- children: f ? "Publishing…" : "Publish to web"
234
+ children: m ? "Publishing…" : "Publish to web"
215
235
  })]
216
236
  }),
217
- k && /* @__PURE__ */ o(i, { children: [j ? /* @__PURE__ */ o("div", {
237
+ N && /* @__PURE__ */ o(i, { children: [F ? /* @__PURE__ */ o("div", {
218
238
  style: { marginBottom: "16px" },
219
239
  children: [
220
- /* @__PURE__ */ a("div", {
240
+ /* @__PURE__ */ o("div", {
221
241
  style: {
222
- fontSize: "12px",
223
- fontWeight: 600,
224
- color: l.textSecondary,
242
+ display: "flex",
243
+ alignItems: "center",
244
+ justifyContent: "space-between",
225
245
  marginBottom: "6px"
226
246
  },
227
- children: "Embed snippet"
247
+ children: [/* @__PURE__ */ a("span", {
248
+ style: {
249
+ fontSize: "12px",
250
+ fontWeight: 600,
251
+ color: d.textSecondary
252
+ },
253
+ children: "Embed snippet"
254
+ }), /* @__PURE__ */ o("label", {
255
+ style: {
256
+ display: "flex",
257
+ alignItems: "center",
258
+ gap: "6px"
259
+ },
260
+ children: [/* @__PURE__ */ a("span", {
261
+ style: {
262
+ fontSize: "12px",
263
+ color: d.textTertiary
264
+ },
265
+ children: "Theme"
266
+ }), /* @__PURE__ */ a("select", {
267
+ value: k,
268
+ onChange: (e) => {
269
+ let t = e.target.value;
270
+ (t === "light" || t === "dark" || t === "auto") && A(t);
271
+ },
272
+ style: {
273
+ padding: "4px 8px",
274
+ borderRadius: "6px",
275
+ border: `1px solid ${d.borderDefault}`,
276
+ background: d.bgSecondary,
277
+ color: d.textPrimary,
278
+ fontSize: "12px",
279
+ cursor: "pointer"
280
+ },
281
+ children: s.map((e) => /* @__PURE__ */ a("option", {
282
+ value: e.value,
283
+ children: e.label
284
+ }, e.value))
285
+ })]
286
+ })]
228
287
  }),
229
288
  /* @__PURE__ */ a("textarea", {
230
289
  readOnly: !0,
231
- value: j,
290
+ value: F,
232
291
  style: {
233
292
  width: "100%",
234
293
  minHeight: "96px",
235
294
  padding: "10px",
236
295
  borderRadius: "8px",
237
- border: `1px solid ${l.borderDefault}`,
238
- background: l.bgSecondary,
239
- color: l.textPrimary,
296
+ border: `1px solid ${d.borderDefault}`,
297
+ background: d.bgSecondary,
298
+ color: d.textPrimary,
240
299
  fontFamily: "monospace",
241
300
  fontSize: "12px",
242
301
  resize: "vertical"
@@ -244,19 +303,19 @@ function c({ dashboardId: c, colors: l }) {
244
303
  }),
245
304
  /* @__PURE__ */ a("button", {
246
305
  style: {
247
- ...L,
306
+ ...V,
248
307
  marginTop: "8px"
249
308
  },
250
- onClick: () => F(j),
309
+ onClick: () => z(F),
251
310
  type: "button",
252
- children: T ? "Copied ✓" : "Copy snippet"
311
+ children: D ? "Copied ✓" : "Copy snippet"
253
312
  })
254
313
  ]
255
314
  }) : /* @__PURE__ */ o("div", {
256
315
  style: {
257
316
  fontSize: "12px",
258
- color: l.textSecondary,
259
- background: l.bgSecondary,
317
+ color: d.textSecondary,
318
+ background: d.bgSecondary,
260
319
  borderRadius: "8px",
261
320
  padding: "10px 12px",
262
321
  marginBottom: "16px"
@@ -272,37 +331,37 @@ function c({ dashboardId: c, colors: l }) {
272
331
  gap: "8px"
273
332
  },
274
333
  children: [/* @__PURE__ */ a("button", {
275
- style: L,
276
- onClick: () => u && _(u.id),
277
- disabled: f,
334
+ style: V,
335
+ onClick: () => f && y(f.id),
336
+ disabled: m,
278
337
  type: "button",
279
338
  children: "Rotate link"
280
339
  }), /* @__PURE__ */ a("button", {
281
340
  style: {
282
- ...L,
283
- color: l.statusErrorText,
284
- borderColor: l.statusErrorText
341
+ ...V,
342
+ color: d.statusErrorText,
343
+ borderColor: d.statusErrorText
285
344
  },
286
345
  onClick: () => {
287
- u && window.confirm("Unpublish this dashboard? The embed will stop working immediately for anyone using the link.") && v(u.id);
346
+ f && window.confirm("Unpublish this dashboard? The embed will stop working immediately for anyone using the link.") && b(f.id);
288
347
  },
289
- disabled: f,
348
+ disabled: m,
290
349
  type: "button",
291
350
  children: "Unpublish"
292
351
  })]
293
352
  })] }),
294
- (D || p) && /* @__PURE__ */ a("div", {
353
+ (j || h) && /* @__PURE__ */ a("div", {
295
354
  style: {
296
355
  marginTop: "12px",
297
356
  fontSize: "12px",
298
- color: l.statusErrorText
357
+ color: d.statusErrorText
299
358
  },
300
- children: D ?? p?.message
359
+ children: j ?? h?.message
301
360
  })
302
361
  ]
303
362
  });
304
363
  }
305
364
  //#endregion
306
- export { c as default };
365
+ export { u as default };
307
366
 
308
367
  //# sourceMappingURL=EmbedTab.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"EmbedTab.js","names":[],"sources":["../../../../../src/bigconsole/components/dashboard/ShareDialog/EmbedTab.tsx"],"sourcesContent":["/**\n * EmbedTab — \"Publish to web\" section of the dashboard Share dialog.\n *\n * Owner-facing UI to publish a dashboard as an anonymous-link embed serving LIVE\n * data, manage the allowed parent domains, copy the iframe snippet, rotate the\n * link key, and unpublish. Backed by {@link useDashboardEmbedPolicy}; all\n * mutations are gateway-enforced by `@rbac(manage_embed)` (owner-only).\n *\n * Self-contained (inline styles driven by the dialog's `colors`) so it slots into\n * the existing hand-styled ShareDialog without a broader refactor.\n */\n\nimport React, { useEffect, useMemo, useState } from 'react';\nimport { useDashboardEmbedPolicy } from '../../../hooks/useDashboardEmbedPolicy';\n\ninterface DialogColors {\n bgPrimary: string;\n bgSecondary: string;\n bgTertiary: string;\n textPrimary: string;\n textSecondary: string;\n textTertiary: string;\n borderDefault: string;\n actionPrimaryBg: string;\n actionPrimaryBgHover: string;\n statusSuccessBg: string;\n statusErrorText: string;\n}\n\nexport interface EmbedTabProps {\n dashboardId: string;\n colors: DialogColors;\n}\n\n/** Validate an exact https origin (matches the backend allowlist rules). */\nfunction normalizeOrigin(raw: string): string | null {\n const trimmed = raw.trim();\n if (!trimmed) return null;\n try {\n const url = new URL(trimmed);\n if (url.protocol !== 'https:') return null;\n if ((url.pathname !== '' && url.pathname !== '/') || url.search || url.hash) return null;\n return url.origin;\n } catch {\n return null;\n }\n}\n\nexport function EmbedTab({ dashboardId, colors }: EmbedTabProps) {\n const { policy, secret, loading, error, fetchPolicy, publishToWeb, setAllowedOrigins, rotateKey, unpublish } =\n useDashboardEmbedPolicy();\n\n const [domainInput, setDomainInput] = useState('');\n const [pendingDomains, setPendingDomains] = useState<string[]>([]);\n const [attested, setAttested] = useState(false);\n const [copied, setCopied] = useState(false);\n const [localError, setLocalError] = useState<string | null>(null);\n\n useEffect(() => {\n if (dashboardId) fetchPolicy(dashboardId);\n }, [dashboardId, fetchPolicy]);\n\n const isPublished = policy?.status === 'ACTIVE';\n const domains = isPublished ? (policy?.allowedParentOrigins ?? []) : pendingDomains;\n\n // The copyable iframe snippet: from the one-time secret if present (fresh\n // publish/rotate), else reconstructed from the locator for an already-published\n // policy (the fragment key is only available in the one-time secret).\n const iframeSnippet = useMemo(() => {\n if (secret?.iframeSnippet) return secret.iframeSnippet;\n return null;\n }, [secret]);\n\n const addDomain = () => {\n setLocalError(null);\n const origin = normalizeOrigin(domainInput);\n if (!origin) {\n setLocalError('Enter a valid https origin, e.g. https://example.com');\n return;\n }\n if (domains.includes(origin)) {\n setDomainInput('');\n return;\n }\n if (isPublished && policy) {\n void setAllowedOrigins(policy.id, [...domains, origin]);\n } else {\n setPendingDomains((prev) => [...prev, origin]);\n }\n setDomainInput('');\n };\n\n const removeDomain = (origin: string) => {\n if (isPublished && policy) {\n void setAllowedOrigins(\n policy.id,\n domains.filter((d) => d !== origin)\n );\n } else {\n setPendingDomains((prev) => prev.filter((d) => d !== origin));\n }\n };\n\n const handlePublish = async () => {\n setLocalError(null);\n if (pendingDomains.length === 0) {\n setLocalError('Add at least one allowed website domain before publishing.');\n return;\n }\n if (!attested) {\n setLocalError('Please confirm this dashboard is safe to be public.');\n return;\n }\n await publishToWeb(dashboardId, pendingDomains, true);\n };\n\n const handleCopy = async (text: string) => {\n try {\n await navigator.clipboard.writeText(text);\n setCopied(true);\n setTimeout(() => setCopied(false), 1500);\n } catch {\n setLocalError('Could not copy — select and copy manually.');\n }\n };\n\n const primaryBtn: React.CSSProperties = {\n padding: '8px 14px',\n borderRadius: '8px',\n border: 'none',\n background: colors.actionPrimaryBg,\n color: '#fff',\n fontSize: '13px',\n fontWeight: 600,\n cursor: 'pointer',\n };\n const ghostBtn: React.CSSProperties = {\n padding: '8px 14px',\n borderRadius: '8px',\n border: `1px solid ${colors.borderDefault}`,\n background: 'transparent',\n color: colors.textPrimary,\n fontSize: '13px',\n cursor: 'pointer',\n };\n const inputStyle: React.CSSProperties = {\n flex: 1,\n padding: '8px 10px',\n borderRadius: '8px',\n border: `1px solid ${colors.borderDefault}`,\n background: colors.bgSecondary,\n color: colors.textPrimary,\n fontSize: '13px',\n };\n\n return (\n <div style={{ padding: '20px 24px' }}>\n {/* Status banner */}\n <div\n style={{\n display: 'flex',\n alignItems: 'center',\n justifyContent: 'space-between',\n padding: '12px 14px',\n borderRadius: '10px',\n background: colors.bgSecondary,\n marginBottom: '16px',\n }}\n >\n <div>\n <div style={{ fontSize: '14px', fontWeight: 600, color: colors.textPrimary }}>Publish to the web</div>\n <div style={{ fontSize: '12px', color: colors.textSecondary, marginTop: '2px' }}>\n {isPublished\n ? 'This dashboard is live. Anyone with the link on an allowed website sees its live data.'\n : 'Embed this dashboard on your website. Viewers see live data — no login required.'}\n </div>\n </div>\n <span\n style={{\n fontSize: '11px',\n fontWeight: 700,\n padding: '3px 8px',\n borderRadius: '999px',\n color: '#fff',\n background: isPublished ? colors.statusSuccessBg : colors.textTertiary,\n }}\n >\n {isPublished ? 'PUBLISHED' : 'PRIVATE'}\n </span>\n </div>\n\n {/* Allowed domains */}\n <div style={{ marginBottom: '16px' }}>\n <div style={{ fontSize: '12px', fontWeight: 600, color: colors.textSecondary, marginBottom: '6px' }}>\n Allowed website domains\n </div>\n <div style={{ display: 'flex', gap: '8px', marginBottom: '8px' }}>\n <input\n style={inputStyle}\n placeholder=\"https://yoursite.com\"\n value={domainInput}\n onChange={(e) => setDomainInput(e.target.value)}\n onKeyDown={(e) => {\n if (e.key === 'Enter') addDomain();\n }}\n />\n <button style={ghostBtn} onClick={addDomain} type=\"button\">\n Add\n </button>\n </div>\n {domains.length === 0 ? (\n <div style={{ fontSize: '12px', color: colors.textTertiary }}>\n No domains yet. Only listed https domains may frame this dashboard.\n </div>\n ) : (\n <div style={{ display: 'flex', flexDirection: 'column', gap: '6px' }}>\n {domains.map((d) => (\n <div\n key={d}\n style={{\n display: 'flex',\n alignItems: 'center',\n justifyContent: 'space-between',\n padding: '6px 10px',\n borderRadius: '8px',\n background: colors.bgTertiary,\n }}\n >\n <span style={{ fontSize: '13px', color: colors.textPrimary }}>{d}</span>\n <button\n style={{ ...ghostBtn, padding: '2px 8px', color: colors.statusErrorText }}\n onClick={() => removeDomain(d)}\n type=\"button\"\n >\n Remove\n </button>\n </div>\n ))}\n </div>\n )}\n </div>\n\n {/* Publish CTA (unpublished) */}\n {!isPublished && (\n <div style={{ marginBottom: '8px' }}>\n <label style={{ display: 'flex', gap: '8px', alignItems: 'flex-start', marginBottom: '12px' }}>\n <input type=\"checkbox\" checked={attested} onChange={(e) => setAttested(e.target.checked)} />\n <span style={{ fontSize: '12px', color: colors.textSecondary }}>\n I understand this makes the dashboard&apos;s live data visible to anyone with the link, and I confirm it\n is safe to be public.\n </span>\n </label>\n <button style={primaryBtn} onClick={handlePublish} disabled={loading} type=\"button\">\n {loading ? 'Publishing…' : 'Publish to web'}\n </button>\n </div>\n )}\n\n {/* Embed snippet + management (published) */}\n {isPublished && (\n <>\n {iframeSnippet ? (\n <div style={{ marginBottom: '16px' }}>\n <div style={{ fontSize: '12px', fontWeight: 600, color: colors.textSecondary, marginBottom: '6px' }}>\n Embed snippet\n </div>\n <textarea\n readOnly\n value={iframeSnippet}\n style={{\n width: '100%',\n minHeight: '96px',\n padding: '10px',\n borderRadius: '8px',\n border: `1px solid ${colors.borderDefault}`,\n background: colors.bgSecondary,\n color: colors.textPrimary,\n fontFamily: 'monospace',\n fontSize: '12px',\n resize: 'vertical',\n }}\n />\n <button style={{ ...ghostBtn, marginTop: '8px' }} onClick={() => handleCopy(iframeSnippet)} type=\"button\">\n {copied ? 'Copied ✓' : 'Copy snippet'}\n </button>\n </div>\n ) : (\n <div\n style={{\n fontSize: '12px',\n color: colors.textSecondary,\n background: colors.bgSecondary,\n borderRadius: '8px',\n padding: '10px 12px',\n marginBottom: '16px',\n }}\n >\n The embed link is already active. For security the snippet (which contains the secret key) is only shown\n once at publish time — use <strong>Rotate link</strong> to generate a fresh snippet.\n </div>\n )}\n\n <div style={{ display: 'flex', gap: '8px' }}>\n <button style={ghostBtn} onClick={() => policy && rotateKey(policy.id)} disabled={loading} type=\"button\">\n Rotate link\n </button>\n <button\n style={{ ...ghostBtn, color: colors.statusErrorText, borderColor: colors.statusErrorText }}\n onClick={() => {\n if (\n policy &&\n window.confirm(\n 'Unpublish this dashboard? The embed will stop working immediately for anyone using the link.'\n )\n ) {\n void unpublish(policy.id);\n }\n }}\n disabled={loading}\n type=\"button\"\n >\n Unpublish\n </button>\n </div>\n </>\n )}\n\n {(localError || error) && (\n <div style={{ marginTop: '12px', fontSize: '12px', color: colors.statusErrorText }}>\n {localError ?? error?.message}\n </div>\n )}\n </div>\n );\n}\n\nexport default EmbedTab;\n"],"mappings":"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"}
1
+ {"version":3,"file":"EmbedTab.js","names":[],"sources":["../../../../../src/bigconsole/components/dashboard/ShareDialog/EmbedTab.tsx"],"sourcesContent":["/**\n * EmbedTab — \"Publish to web\" section of the dashboard Share dialog.\n *\n * Owner-facing UI to publish a dashboard as an anonymous-link embed serving LIVE\n * data, manage the allowed parent domains, copy the iframe snippet, rotate the\n * link key, and unpublish. Backed by {@link useDashboardEmbedPolicy}; all\n * mutations are gateway-enforced by `@rbac(manage_embed)` (owner-only).\n *\n * Self-contained (inline styles driven by the dialog's `colors`) so it slots into\n * the existing hand-styled ShareDialog without a broader refactor.\n */\n\nimport React, { useEffect, useMemo, useState } from 'react';\nimport { useDashboardEmbedPolicy } from '../../../hooks/useDashboardEmbedPolicy';\n\ninterface DialogColors {\n bgPrimary: string;\n bgSecondary: string;\n bgTertiary: string;\n textPrimary: string;\n textSecondary: string;\n textTertiary: string;\n borderDefault: string;\n actionPrimaryBg: string;\n actionPrimaryBgHover: string;\n statusSuccessBg: string;\n statusErrorText: string;\n}\n\nexport interface EmbedTabProps {\n dashboardId: string;\n colors: DialogColors;\n}\n\n/** Embed color-mode baked into the iframe URL (BOFF-5734). */\ntype EmbedSnippetTheme = 'light' | 'dark' | 'auto';\n\nconst THEME_OPTIONS: ReadonlyArray<{ value: EmbedSnippetTheme; label: string }> = [\n { value: 'light', label: 'Light' },\n { value: 'dark', label: 'Dark' },\n { value: 'auto', label: 'Auto (match viewer)' },\n];\n\n/**\n * Bake the chosen color mode into an iframe snippet by adding `?theme=` to its\n * `src` URL. `light` is the default and is left implicit so existing/plain\n * snippets stay unchanged. The secret `#key=` fragment is preserved untouched.\n *\n * Inserted via string splicing (before the fragment) rather than `URL`/\n * `URLSearchParams`, which re-serialize ALL params and could subtly re-encode\n * existing ones (e.g. `%20` → `+`). `theme` is a fixed enum, so no encoding is\n * needed; every other part of the URL is passed through byte-for-byte.\n */\nexport function withThemeInSnippet(snippet: string, theme: EmbedSnippetTheme): string {\n if (theme === 'light') return snippet;\n // Require whitespace before `src` so we match the real attribute, never a\n // substring like `data-src`/`srcdoc` (the snippet is backend-generated with\n // each attribute on its own indented line).\n return snippet.replace(/(?<=\\s)src=\"([^\"]*)\"/, (_match, url: string) => {\n const hashIdx = url.indexOf('#');\n const base = hashIdx === -1 ? url : url.slice(0, hashIdx);\n const fragment = hashIdx === -1 ? '' : url.slice(hashIdx);\n const sep = base.includes('?') ? '&' : '?';\n return `src=\"${base}${sep}theme=${theme}${fragment}\"`;\n });\n}\n\n/** Validate an exact https origin (matches the backend allowlist rules). */\nfunction normalizeOrigin(raw: string): string | null {\n const trimmed = raw.trim();\n if (!trimmed) return null;\n try {\n const url = new URL(trimmed);\n if (url.protocol !== 'https:') return null;\n if ((url.pathname !== '' && url.pathname !== '/') || url.search || url.hash) return null;\n return url.origin;\n } catch {\n return null;\n }\n}\n\nexport function EmbedTab({ dashboardId, colors }: EmbedTabProps) {\n const { policy, secret, loading, error, fetchPolicy, publishToWeb, setAllowedOrigins, rotateKey, unpublish } =\n useDashboardEmbedPolicy();\n\n const [domainInput, setDomainInput] = useState('');\n const [pendingDomains, setPendingDomains] = useState<string[]>([]);\n const [attested, setAttested] = useState(false);\n const [copied, setCopied] = useState(false);\n const [theme, setTheme] = useState<EmbedSnippetTheme>('light');\n const [localError, setLocalError] = useState<string | null>(null);\n\n useEffect(() => {\n if (dashboardId) fetchPolicy(dashboardId);\n }, [dashboardId, fetchPolicy]);\n\n const isPublished = policy?.status === 'ACTIVE';\n const domains = isPublished ? (policy?.allowedParentOrigins ?? []) : pendingDomains;\n\n // The copyable iframe snippet: from the one-time secret if present (fresh\n // publish/rotate), else reconstructed from the locator for an already-published\n // policy (the fragment key is only available in the one-time secret).\n const iframeSnippet = useMemo(() => {\n if (secret?.iframeSnippet) return withThemeInSnippet(secret.iframeSnippet, theme);\n return null;\n }, [secret, theme]);\n\n const addDomain = () => {\n setLocalError(null);\n const origin = normalizeOrigin(domainInput);\n if (!origin) {\n setLocalError('Enter a valid https origin, e.g. https://example.com');\n return;\n }\n if (domains.includes(origin)) {\n setDomainInput('');\n return;\n }\n if (isPublished && policy) {\n void setAllowedOrigins(policy.id, [...domains, origin]);\n } else {\n setPendingDomains((prev) => [...prev, origin]);\n }\n setDomainInput('');\n };\n\n const removeDomain = (origin: string) => {\n if (isPublished && policy) {\n void setAllowedOrigins(\n policy.id,\n domains.filter((d) => d !== origin)\n );\n } else {\n setPendingDomains((prev) => prev.filter((d) => d !== origin));\n }\n };\n\n const handlePublish = async () => {\n setLocalError(null);\n if (pendingDomains.length === 0) {\n setLocalError('Add at least one allowed website domain before publishing.');\n return;\n }\n if (!attested) {\n setLocalError('Please confirm this dashboard is safe to be public.');\n return;\n }\n await publishToWeb(dashboardId, pendingDomains, true);\n };\n\n const handleCopy = async (text: string) => {\n try {\n await navigator.clipboard.writeText(text);\n setCopied(true);\n setTimeout(() => setCopied(false), 1500);\n } catch {\n setLocalError('Could not copy — select and copy manually.');\n }\n };\n\n const primaryBtn: React.CSSProperties = {\n padding: '8px 14px',\n borderRadius: '8px',\n border: 'none',\n background: colors.actionPrimaryBg,\n color: '#fff',\n fontSize: '13px',\n fontWeight: 600,\n cursor: 'pointer',\n };\n const ghostBtn: React.CSSProperties = {\n padding: '8px 14px',\n borderRadius: '8px',\n border: `1px solid ${colors.borderDefault}`,\n background: 'transparent',\n color: colors.textPrimary,\n fontSize: '13px',\n cursor: 'pointer',\n };\n const inputStyle: React.CSSProperties = {\n flex: 1,\n padding: '8px 10px',\n borderRadius: '8px',\n border: `1px solid ${colors.borderDefault}`,\n background: colors.bgSecondary,\n color: colors.textPrimary,\n fontSize: '13px',\n };\n\n return (\n <div style={{ padding: '20px 24px' }}>\n {/* Status banner */}\n <div\n style={{\n display: 'flex',\n alignItems: 'center',\n justifyContent: 'space-between',\n padding: '12px 14px',\n borderRadius: '10px',\n background: colors.bgSecondary,\n marginBottom: '16px',\n }}\n >\n <div>\n <div style={{ fontSize: '14px', fontWeight: 600, color: colors.textPrimary }}>Publish to the web</div>\n <div style={{ fontSize: '12px', color: colors.textSecondary, marginTop: '2px' }}>\n {isPublished\n ? 'This dashboard is live. Anyone with the link on an allowed website sees its live data.'\n : 'Embed this dashboard on your website. Viewers see live data — no login required.'}\n </div>\n </div>\n <span\n style={{\n fontSize: '11px',\n fontWeight: 700,\n padding: '3px 8px',\n borderRadius: '999px',\n color: '#fff',\n background: isPublished ? colors.statusSuccessBg : colors.textTertiary,\n }}\n >\n {isPublished ? 'PUBLISHED' : 'PRIVATE'}\n </span>\n </div>\n\n {/* Allowed domains */}\n <div style={{ marginBottom: '16px' }}>\n <div style={{ fontSize: '12px', fontWeight: 600, color: colors.textSecondary, marginBottom: '6px' }}>\n Allowed website domains\n </div>\n <div style={{ display: 'flex', gap: '8px', marginBottom: '8px' }}>\n <input\n style={inputStyle}\n placeholder=\"https://yoursite.com\"\n value={domainInput}\n onChange={(e) => setDomainInput(e.target.value)}\n onKeyDown={(e) => {\n if (e.key === 'Enter') addDomain();\n }}\n />\n <button style={ghostBtn} onClick={addDomain} type=\"button\">\n Add\n </button>\n </div>\n {domains.length === 0 ? (\n <div style={{ fontSize: '12px', color: colors.textTertiary }}>\n No domains yet. Only listed https domains may frame this dashboard.\n </div>\n ) : (\n <div style={{ display: 'flex', flexDirection: 'column', gap: '6px' }}>\n {domains.map((d) => (\n <div\n key={d}\n style={{\n display: 'flex',\n alignItems: 'center',\n justifyContent: 'space-between',\n padding: '6px 10px',\n borderRadius: '8px',\n background: colors.bgTertiary,\n }}\n >\n <span style={{ fontSize: '13px', color: colors.textPrimary }}>{d}</span>\n <button\n style={{ ...ghostBtn, padding: '2px 8px', color: colors.statusErrorText }}\n onClick={() => removeDomain(d)}\n type=\"button\"\n >\n Remove\n </button>\n </div>\n ))}\n </div>\n )}\n </div>\n\n {/* Publish CTA (unpublished) */}\n {!isPublished && (\n <div style={{ marginBottom: '8px' }}>\n <label style={{ display: 'flex', gap: '8px', alignItems: 'flex-start', marginBottom: '12px' }}>\n <input type=\"checkbox\" checked={attested} onChange={(e) => setAttested(e.target.checked)} />\n <span style={{ fontSize: '12px', color: colors.textSecondary }}>\n I understand this makes the dashboard&apos;s live data visible to anyone with the link, and I confirm it\n is safe to be public.\n </span>\n </label>\n <button style={primaryBtn} onClick={handlePublish} disabled={loading} type=\"button\">\n {loading ? 'Publishing…' : 'Publish to web'}\n </button>\n </div>\n )}\n\n {/* Embed snippet + management (published) */}\n {isPublished && (\n <>\n {iframeSnippet ? (\n <div style={{ marginBottom: '16px' }}>\n <div\n style={{\n display: 'flex',\n alignItems: 'center',\n justifyContent: 'space-between',\n marginBottom: '6px',\n }}\n >\n <span style={{ fontSize: '12px', fontWeight: 600, color: colors.textSecondary }}>Embed snippet</span>\n <label style={{ display: 'flex', alignItems: 'center', gap: '6px' }}>\n <span style={{ fontSize: '12px', color: colors.textTertiary }}>Theme</span>\n <select\n value={theme}\n onChange={(e) => {\n // The options are fixed, but narrow instead of casting so an\n // unexpected value can never slip into state.\n const value = e.target.value;\n if (value === 'light' || value === 'dark' || value === 'auto') {\n setTheme(value);\n }\n }}\n style={{\n padding: '4px 8px',\n borderRadius: '6px',\n border: `1px solid ${colors.borderDefault}`,\n background: colors.bgSecondary,\n color: colors.textPrimary,\n fontSize: '12px',\n cursor: 'pointer',\n }}\n >\n {THEME_OPTIONS.map((opt) => (\n <option key={opt.value} value={opt.value}>\n {opt.label}\n </option>\n ))}\n </select>\n </label>\n </div>\n <textarea\n readOnly\n value={iframeSnippet}\n style={{\n width: '100%',\n minHeight: '96px',\n padding: '10px',\n borderRadius: '8px',\n border: `1px solid ${colors.borderDefault}`,\n background: colors.bgSecondary,\n color: colors.textPrimary,\n fontFamily: 'monospace',\n fontSize: '12px',\n resize: 'vertical',\n }}\n />\n <button style={{ ...ghostBtn, marginTop: '8px' }} onClick={() => handleCopy(iframeSnippet)} type=\"button\">\n {copied ? 'Copied ✓' : 'Copy snippet'}\n </button>\n </div>\n ) : (\n <div\n style={{\n fontSize: '12px',\n color: colors.textSecondary,\n background: colors.bgSecondary,\n borderRadius: '8px',\n padding: '10px 12px',\n marginBottom: '16px',\n }}\n >\n The embed link is already active. For security the snippet (which contains the secret key) is only shown\n once at publish time — use <strong>Rotate link</strong> to generate a fresh snippet.\n </div>\n )}\n\n <div style={{ display: 'flex', gap: '8px' }}>\n <button style={ghostBtn} onClick={() => policy && rotateKey(policy.id)} disabled={loading} type=\"button\">\n Rotate link\n </button>\n <button\n style={{ ...ghostBtn, color: colors.statusErrorText, borderColor: colors.statusErrorText }}\n onClick={() => {\n if (\n policy &&\n window.confirm(\n 'Unpublish this dashboard? The embed will stop working immediately for anyone using the link.'\n )\n ) {\n void unpublish(policy.id);\n }\n }}\n disabled={loading}\n type=\"button\"\n >\n Unpublish\n </button>\n </div>\n </>\n )}\n\n {(localError || error) && (\n <div style={{ marginTop: '12px', fontSize: '12px', color: colors.statusErrorText }}>\n {localError ?? error?.message}\n </div>\n )}\n </div>\n );\n}\n\nexport default EmbedTab;\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.803.1",
3
+ "version": "2026.803.2",
4
4
  "description": "BigConsole - AI-powered analytics and dashboard platform",
5
5
  "type": "module",
6
6
  "files": [