@burdenoff/microfe-bigconsole 2026.925.1 → 2026.928.1
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.
- package/dist/bigconsole/components/dashboard/ShareDialog/EmbedTab.js +142 -145
- package/dist/bigconsole/components/dashboard/ShareDialog/EmbedTab.js.map +1 -1
- package/dist/bigconsole/components/embed/EmbedWidgetRenderer.js +43 -13
- package/dist/bigconsole/components/embed/EmbedWidgetRenderer.js.map +1 -1
- package/dist/bigconsole/components/embed/ReadOnlyDashboardRenderer.js +5 -5
- package/dist/bigconsole/components/embed/ReadOnlyDashboardRenderer.js.map +1 -1
- package/dist/bigconsole/components/embed/renderer-manifest.js +14 -47
- package/dist/bigconsole/components/embed/renderer-manifest.js.map +1 -1
- package/dist/bigconsole/components/embed/types.js.map +1 -1
- package/dist/bigconsole/components/embed/widgetShim.js +28 -0
- package/dist/bigconsole/components/embed/widgetShim.js.map +1 -0
- package/dist/bigconsole/components/embed/widgets/CalendarEmbed.js +38 -0
- package/dist/bigconsole/components/embed/widgets/CalendarEmbed.js.map +1 -0
- package/dist/bigconsole/components/embed/widgets/ChartEmbed.js +225 -0
- package/dist/bigconsole/components/embed/widgets/ChartEmbed.js.map +1 -0
- package/dist/bigconsole/components/embed/widgets/EmbedFrame.js +18 -0
- package/dist/bigconsole/components/embed/widgets/EmbedFrame.js.map +1 -0
- package/dist/bigconsole/components/embed/widgets/FormEmbed.js +147 -0
- package/dist/bigconsole/components/embed/widgets/FormEmbed.js.map +1 -0
- package/dist/bigconsole/components/embed/widgets/GaugeEmbed.js +44 -0
- package/dist/bigconsole/components/embed/widgets/GaugeEmbed.js.map +1 -0
- package/dist/bigconsole/components/embed/widgets/HeatmapEmbed.js +35 -0
- package/dist/bigconsole/components/embed/widgets/HeatmapEmbed.js.map +1 -0
- package/dist/bigconsole/components/embed/widgets/KanbanEmbed.js +50 -0
- package/dist/bigconsole/components/embed/widgets/KanbanEmbed.js.map +1 -0
- package/dist/bigconsole/components/embed/widgets/PivotEmbed.js +146 -0
- package/dist/bigconsole/components/embed/widgets/PivotEmbed.js.map +1 -0
- package/dist/bigconsole/components/embed/widgets/ProgressEmbed.js +30 -0
- package/dist/bigconsole/components/embed/widgets/ProgressEmbed.js.map +1 -0
- package/dist/bigconsole/components/embed/widgets/RetentionEmbed.js +41 -0
- package/dist/bigconsole/components/embed/widgets/RetentionEmbed.js.map +1 -0
- package/dist/bigconsole/components/embed/widgets/TimelineEmbed.js +40 -0
- package/dist/bigconsole/components/embed/widgets/TimelineEmbed.js.map +1 -0
- package/dist/bigconsole/components/embed/widgets/shared.js +13 -1
- package/dist/bigconsole/components/embed/widgets/shared.js.map +1 -1
- package/dist/bigconsole/components/widgets/calendar/CalendarView.js +152 -0
- package/dist/bigconsole/components/widgets/calendar/CalendarView.js.map +1 -0
- package/dist/bigconsole/components/widgets/calendar/CalendarWidget.js +10 -147
- package/dist/bigconsole/components/widgets/calendar/CalendarWidget.js.map +1 -1
- package/dist/bigconsole/components/widgets/chart/ChartWidget.js +19 -19
- package/dist/bigconsole/components/widgets/chart/index.js +8 -8
- package/dist/bigconsole/components/widgets/index.js +5 -5
- package/dist/bigconsole/components/widgets/kanban/KanbanView.js +192 -0
- package/dist/bigconsole/components/widgets/kanban/KanbanView.js.map +1 -0
- package/dist/bigconsole/components/widgets/kanban/KanbanWidget.js +10 -187
- package/dist/bigconsole/components/widgets/kanban/KanbanWidget.js.map +1 -1
- package/dist/bigconsole/components/widgets/metric-card/MetricCardWidget.js +20 -20
- package/dist/bigconsole/components/widgets/timeline/TimelineWidget.js +23 -22
- package/dist/bigconsole/components/widgets/timeline/TimelineWidget.js.map +1 -1
- package/dist/bigconsole/components/widgets/utils/translate.js +9 -0
- package/dist/bigconsole/components/widgets/utils/translate.js.map +1 -0
- package/dist/bigconsole/hooks/useDashboardEmbedPolicy.js +2 -4
- package/dist/bigconsole/hooks/useDashboardEmbedPolicy.js.map +1 -1
- package/dist/generated/wspace-operations.js +0 -1
- package/dist/generated/wspace-operations.js.map +1 -1
- package/package.json +1 -1
|
@@ -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, useRef, useState } from 'react';\nimport { usePermissions } from '@burdenoff/fe-libs/shared/providers/shell';\nimport { useI18n } from '@burdenoff/fe-libs/shared/providers/shell/I18nProvider';\nimport { useDashboardEmbedPolicy, themeChoiceFromPolicy } from '../../../hooks/useDashboardEmbedPolicy';\nimport { BIGCONSOLE_PERMISSIONS } from '../../../../constants/permissions';\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_OPTION_VALUES: ReadonlyArray<EmbedSnippetTheme> = ['light', 'dark', 'auto'];\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 { t } = useI18n();\n const {\n policy,\n secret,\n loading,\n error,\n fetchPolicy,\n publishToWeb,\n applyPresentation,\n setAllowedOrigins,\n rotateKey,\n unpublish,\n } = useDashboardEmbedPolicy();\n\n // All embed policy mutations (publish, rotate, revoke, set-origins, apply\n // presentation) are gateway-enforced by `@rbac(manage_embed)`. Gate the UI on\n // the same permission so the controls degrade gracefully instead of erroring.\n const { hasPermission } = usePermissions();\n const canManageEmbed = hasPermission(BIGCONSOLE_PERMISSIONS.DASHBOARD_MANAGE_EMBED);\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 // Presentation options (BOFF-5735). Defaults mirror the backend column defaults\n // so a fresh publish captures a sensible look without extra clicks.\n const [theme, setTheme] = useState<EmbedSnippetTheme>('light');\n const [showTitle, setShowTitle] = useState(false);\n const [showBranding, setShowBranding] = useState(true);\n const [presentationSaved, setPresentationSaved] = useState(false);\n const [localError, setLocalError] = useState<string | null>(null);\n // Only seed the presentation controls once per policy identity — NOT on every\n // `policy` object change. Otherwise an unrelated mutation (e.g. adding a domain\n // via setAllowedOrigins) would return a fresh policy object and clobber the\n // owner's in-progress, not-yet-applied theme/title/branding edits.\n const syncedPolicyIdRef = useRef<string | null>(null);\n const savedTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);\n\n useEffect(() => {\n if (dashboardId) fetchPolicy(dashboardId);\n }, [dashboardId, fetchPolicy]);\n\n // Reflect the persisted presentation the first time each policy loads.\n useEffect(() => {\n if (!policy || syncedPolicyIdRef.current === policy.id) return;\n syncedPolicyIdRef.current = policy.id;\n setTheme(themeChoiceFromPolicy(policy.presentationTheme));\n setShowTitle(policy.showDashboardTitle);\n setShowBranding(policy.showBrandingFooter);\n }, [policy]);\n\n // Clear any pending \"Updated ✓\" timer on unmount.\n useEffect(\n () => () => {\n if (savedTimerRef.current) clearTimeout(savedTimerRef.current);\n },\n []\n );\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(\n t('bigconsole.dashboards.embed.error.invalidOrigin', 'Enter a valid https origin, e.g. https://example.com')\n );\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(\n t('bigconsole.dashboards.embed.error.noDomains', 'Add at least one allowed website domain before publishing.')\n );\n return;\n }\n if (!attested) {\n setLocalError(\n t('bigconsole.dashboards.embed.error.notAttested', 'Please confirm this dashboard is safe to be public.')\n );\n return;\n }\n await publishToWeb(dashboardId, pendingDomains, true, { theme, showTitle, showBranding });\n };\n\n const handleApplyPresentation = async () => {\n if (!policy) return;\n setLocalError(null);\n const updated = await applyPresentation(policy.id, { theme, showTitle, showBranding });\n if (updated) {\n setPresentationSaved(true);\n // Clear any in-flight timer so rapid re-clicks don't hide the confirmation early.\n if (savedTimerRef.current) clearTimeout(savedTimerRef.current);\n savedTimerRef.current = setTimeout(() => setPresentationSaved(false), 1500);\n }\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(t('bigconsole.dashboards.embed.error.copyFailed', '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 const selectStyle: React.CSSProperties = {\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 const toggleRow: React.CSSProperties = {\n display: 'flex',\n gap: '8px',\n alignItems: 'flex-start',\n fontSize: '12px',\n color: colors.textSecondary,\n };\n\n const themeOptions = useMemo(\n () =>\n THEME_OPTION_VALUES.map((value) => ({\n value,\n label:\n value === 'light'\n ? t('bigconsole.dashboards.embed.theme.option.light', 'Light')\n : value === 'dark'\n ? t('bigconsole.dashboards.embed.theme.option.dark', 'Dark')\n : t('bigconsole.dashboards.embed.theme.option.auto', 'Auto (match viewer)'),\n })),\n [t]\n );\n\n // Presentation controls (BOFF-5735) — shared by the pre-publish and published\n // states. Local state drives both the persisted policy and the snippet `?theme=`.\n const presentationControls = (\n <div style={{ display: 'flex', flexDirection: 'column', gap: '10px' }}>\n <label style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: '8px' }}>\n <span style={{ fontSize: '12px', color: colors.textSecondary }}>\n {t('bigconsole.dashboards.embed.theme.label', 'Theme')}\n </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={selectStyle}\n >\n {themeOptions.map((opt) => (\n <option key={opt.value} value={opt.value}>\n {opt.label}\n </option>\n ))}\n </select>\n </label>\n <label style={toggleRow}>\n <input type=\"checkbox\" checked={showTitle} onChange={(e) => setShowTitle(e.target.checked)} />\n <span>{t('bigconsole.dashboards.embed.presentation.showTitle', 'Show the dashboard title as a header')}</span>\n </label>\n <label style={toggleRow}>\n <input type=\"checkbox\" checked={showBranding} onChange={(e) => setShowBranding(e.target.checked)} />\n <span>\n {t('bigconsole.dashboards.embed.presentation.showBranding', 'Show a subtle “Powered by BigConsole” footer')}\n </span>\n </label>\n </div>\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 }}>\n {t('bigconsole.dashboards.embed.heading', 'Publish to the web')}\n </div>\n <div style={{ fontSize: '12px', color: colors.textSecondary, marginTop: '2px' }}>\n {isPublished\n ? t(\n 'bigconsole.dashboards.embed.status.liveDescription',\n 'This dashboard is live. Anyone with the link on an allowed website sees its live data.'\n )\n : t(\n 'bigconsole.dashboards.embed.status.privateDescription',\n 'Embed this dashboard on your website. Viewers see live data — no login required.'\n )}\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\n ? t('bigconsole.dashboards.embed.badge.published', 'PUBLISHED')\n : t('bigconsole.dashboards.embed.badge.private', '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 {t('bigconsole.dashboards.embed.domains.label', 'Allowed website domains')}\n </div>\n <div style={{ display: 'flex', gap: '8px', marginBottom: '8px' }}>\n <input\n style={inputStyle}\n placeholder={t('bigconsole.dashboards.embed.domains.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\" disabled={!canManageEmbed}>\n {t('bigconsole.dashboards.embed.domains.add', 'Add')}\n </button>\n </div>\n {domains.length === 0 ? (\n <div style={{ fontSize: '12px', color: colors.textTertiary }}>\n {t(\n 'bigconsole.dashboards.embed.domains.empty',\n 'No domains yet. Only listed https domains may frame this dashboard.'\n )}\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 {canManageEmbed && (\n <button\n style={{ ...ghostBtn, padding: '2px 8px', color: colors.statusErrorText }}\n onClick={() => removeDomain(d)}\n type=\"button\"\n >\n {t('bigconsole.dashboards.embed.domains.remove', 'Remove')}\n </button>\n )}\n </div>\n ))}\n </div>\n )}\n </div>\n\n {/* Presentation (unpublished — captured into the publish) */}\n {!isPublished && (\n <div style={{ marginBottom: '16px' }}>\n <div style={{ fontSize: '12px', fontWeight: 600, color: colors.textSecondary, marginBottom: '8px' }}>\n {t('bigconsole.dashboards.embed.presentation.heading', 'Presentation')}\n </div>\n {presentationControls}\n </div>\n )}\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 {t(\n 'bigconsole.dashboards.embed.attestation',\n 'I understand this makes the dashboard’s live data visible to anyone with the link, and I confirm it is safe to be public.'\n )}\n </span>\n </label>\n <button style={primaryBtn} onClick={handlePublish} disabled={loading || !canManageEmbed} type=\"button\">\n {loading\n ? t('bigconsole.dashboards.embed.publishing', 'Publishing…')\n : t('bigconsole.dashboards.embed.publish', '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={{ marginBottom: '6px' }}>\n <span style={{ fontSize: '12px', fontWeight: 600, color: colors.textSecondary }}>\n {t('bigconsole.dashboards.embed.snippet.label', 'Embed snippet')}\n </span>\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\n ? t('bigconsole.dashboards.embed.snippet.copied', 'Copied ✓')\n : t('bigconsole.dashboards.embed.snippet.copy', '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 {t(\n 'bigconsole.dashboards.embed.snippet.alreadyActive',\n 'The embed link is already active. For security the snippet (which contains the secret key) is only shown once at publish time — use {{rotateLabel}} to generate a fresh snippet.',\n { rotateLabel: t('bigconsole.dashboards.embed.rotate', 'Rotate link') }\n )}\n </div>\n )}\n\n {/* Presentation (published — persisted + re-published to take effect) */}\n <div style={{ marginBottom: '16px' }}>\n <div style={{ fontSize: '12px', fontWeight: 600, color: colors.textSecondary, marginBottom: '8px' }}>\n {t('bigconsole.dashboards.embed.presentation.heading', 'Presentation')}\n </div>\n {presentationControls}\n <div style={{ display: 'flex', alignItems: 'center', gap: '10px', marginTop: '10px' }}>\n <button\n style={ghostBtn}\n onClick={handleApplyPresentation}\n disabled={loading || !canManageEmbed}\n type=\"button\"\n >\n {loading\n ? t('bigconsole.dashboards.embed.applying', 'Applying…')\n : t('bigconsole.dashboards.embed.apply', 'Apply changes')}\n </button>\n {presentationSaved && (\n <span style={{ fontSize: '12px', color: colors.textTertiary }}>\n {t('bigconsole.dashboards.embed.updated', 'Updated ✓')}\n </span>\n )}\n </div>\n <div style={{ fontSize: '11px', color: colors.textTertiary, marginTop: '6px' }}>\n {t(\n 'bigconsole.dashboards.embed.presentation.applyHelp',\n 'Changes apply to live embeds within a few seconds — no re-publish needed, and the embed link stays the same.'\n )}\n </div>\n </div>\n\n {/* Rotate/Unpublish are destructive management actions — hide entirely\n when the caller can't manage the embed. */}\n {canManageEmbed && (\n <div style={{ display: 'flex', gap: '8px' }}>\n <button style={ghostBtn} onClick={() => policy && rotateKey(policy.id)} disabled={loading} type=\"button\">\n {t('bigconsole.dashboards.embed.rotate', '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 t(\n 'bigconsole.dashboards.embed.unpublishConfirm',\n 'Unpublish this dashboard? The embed will stop working immediately for anyone using the link.'\n )\n )\n ) {\n void unpublish(policy.id);\n }\n }}\n disabled={loading}\n type=\"button\"\n >\n {t('bigconsole.dashboards.embed.unpublish', 'Unpublish')}\n </button>\n </div>\n )}\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, useRef, useState } from 'react';\nimport { usePermissions } from '@burdenoff/fe-libs/shared/providers/shell';\nimport { useI18n } from '@burdenoff/fe-libs/shared/providers/shell/I18nProvider';\nimport { useDashboardEmbedPolicy, themeChoiceFromPolicy } from '../../../hooks/useDashboardEmbedPolicy';\nimport { BIGCONSOLE_PERMISSIONS } from '../../../../constants/permissions';\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_OPTION_VALUES: ReadonlyArray<EmbedSnippetTheme> = ['light', 'dark', 'auto'];\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 { t } = useI18n();\n const {\n policy,\n secret,\n loading,\n error,\n fetchPolicy,\n publishToWeb,\n applyPresentation,\n setAllowedOrigins,\n rotateKey,\n unpublish,\n } = useDashboardEmbedPolicy();\n\n // All embed policy mutations (publish, rotate, revoke, set-origins, apply\n // presentation) are gateway-enforced by `@rbac(manage_embed)`. Gate the UI on\n // the same permission so the controls degrade gracefully instead of erroring.\n const { hasPermission } = usePermissions();\n const canManageEmbed = hasPermission(BIGCONSOLE_PERMISSIONS.DASHBOARD_MANAGE_EMBED);\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 // Presentation options (BOFF-5735). Defaults mirror the backend column defaults\n // so a fresh publish captures a sensible look without extra clicks.\n const [theme, setTheme] = useState<EmbedSnippetTheme>('light');\n const [showTitle, setShowTitle] = useState(false);\n const [presentationSaved, setPresentationSaved] = useState(false);\n const [localError, setLocalError] = useState<string | null>(null);\n // Only seed the presentation controls once per policy identity — NOT on every\n // `policy` object change. Otherwise an unrelated mutation (e.g. adding a domain\n // via setAllowedOrigins) would return a fresh policy object and clobber the\n // owner's in-progress, not-yet-applied theme/title edits.\n const syncedPolicyIdRef = useRef<string | null>(null);\n const savedTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);\n\n useEffect(() => {\n if (dashboardId) fetchPolicy(dashboardId);\n }, [dashboardId, fetchPolicy]);\n\n // Reflect the persisted presentation the first time each policy loads.\n useEffect(() => {\n if (!policy || syncedPolicyIdRef.current === policy.id) return;\n syncedPolicyIdRef.current = policy.id;\n setTheme(themeChoiceFromPolicy(policy.presentationTheme));\n setShowTitle(policy.showDashboardTitle);\n }, [policy]);\n\n // Clear any pending \"Updated ✓\" timer on unmount.\n useEffect(\n () => () => {\n if (savedTimerRef.current) clearTimeout(savedTimerRef.current);\n },\n []\n );\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(\n t('bigconsole.dashboards.embed.error.invalidOrigin', 'Enter a valid https origin, e.g. https://example.com')\n );\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(\n t('bigconsole.dashboards.embed.error.noDomains', 'Add at least one allowed website domain before publishing.')\n );\n return;\n }\n if (!attested) {\n setLocalError(\n t('bigconsole.dashboards.embed.error.notAttested', 'Please confirm this dashboard is safe to be public.')\n );\n return;\n }\n await publishToWeb(dashboardId, pendingDomains, true, { theme, showTitle });\n };\n\n const handleApplyPresentation = async () => {\n if (!policy) return;\n setLocalError(null);\n const updated = await applyPresentation(policy.id, { theme, showTitle });\n if (updated) {\n setPresentationSaved(true);\n // Clear any in-flight timer so rapid re-clicks don't hide the confirmation early.\n if (savedTimerRef.current) clearTimeout(savedTimerRef.current);\n savedTimerRef.current = setTimeout(() => setPresentationSaved(false), 1500);\n }\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(t('bigconsole.dashboards.embed.error.copyFailed', '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 const selectStyle: React.CSSProperties = {\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 const toggleRow: React.CSSProperties = {\n display: 'flex',\n gap: '8px',\n alignItems: 'flex-start',\n fontSize: '12px',\n color: colors.textSecondary,\n };\n\n const themeOptions = useMemo(\n () =>\n THEME_OPTION_VALUES.map((value) => ({\n value,\n label:\n value === 'light'\n ? t('bigconsole.dashboards.embed.theme.option.light', 'Light')\n : value === 'dark'\n ? t('bigconsole.dashboards.embed.theme.option.dark', 'Dark')\n : t('bigconsole.dashboards.embed.theme.option.auto', 'Auto (match viewer)'),\n })),\n [t]\n );\n\n // Presentation controls (BOFF-5735) — shared by the pre-publish and published\n // states. Local state drives both the persisted policy and the snippet `?theme=`.\n // Branding is not a control: every embed carries the \"Powered by BigConsole\"\n // footer (no white-labelling), so the owner only sees a static note about it.\n const presentationControls = (\n <div style={{ display: 'flex', flexDirection: 'column', gap: '10px' }}>\n <label style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: '8px' }}>\n <span style={{ fontSize: '12px', color: colors.textSecondary }}>\n {t('bigconsole.dashboards.embed.theme.label', 'Theme')}\n </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={selectStyle}\n >\n {themeOptions.map((opt) => (\n <option key={opt.value} value={opt.value}>\n {opt.label}\n </option>\n ))}\n </select>\n </label>\n <label style={toggleRow}>\n <input type=\"checkbox\" checked={showTitle} onChange={(e) => setShowTitle(e.target.checked)} />\n <span>{t('bigconsole.dashboards.embed.presentation.showTitle', 'Show the dashboard title as a header')}</span>\n </label>\n <div style={{ fontSize: '11px', color: colors.textTertiary }}>\n {t(\n 'bigconsole.dashboards.embed.presentation.brandingNote',\n 'Embeds always carry a “Powered by BigConsole” footer.'\n )}\n </div>\n </div>\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 }}>\n {t('bigconsole.dashboards.embed.heading', 'Publish to the web')}\n </div>\n <div style={{ fontSize: '12px', color: colors.textSecondary, marginTop: '2px' }}>\n {isPublished\n ? t(\n 'bigconsole.dashboards.embed.status.liveDescription',\n 'This dashboard is live. Anyone with the link on an allowed website sees its live data.'\n )\n : t(\n 'bigconsole.dashboards.embed.status.privateDescription',\n 'Embed this dashboard on your website. Viewers see live data — no login required.'\n )}\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\n ? t('bigconsole.dashboards.embed.badge.published', 'PUBLISHED')\n : t('bigconsole.dashboards.embed.badge.private', '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 {t('bigconsole.dashboards.embed.domains.label', 'Allowed website domains')}\n </div>\n <div style={{ display: 'flex', gap: '8px', marginBottom: '8px' }}>\n <input\n style={inputStyle}\n placeholder={t('bigconsole.dashboards.embed.domains.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\" disabled={!canManageEmbed}>\n {t('bigconsole.dashboards.embed.domains.add', 'Add')}\n </button>\n </div>\n {domains.length === 0 ? (\n <div style={{ fontSize: '12px', color: colors.textTertiary }}>\n {t(\n 'bigconsole.dashboards.embed.domains.empty',\n 'No domains yet. Only listed https domains may frame this dashboard.'\n )}\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 {canManageEmbed && (\n <button\n style={{ ...ghostBtn, padding: '2px 8px', color: colors.statusErrorText }}\n onClick={() => removeDomain(d)}\n type=\"button\"\n >\n {t('bigconsole.dashboards.embed.domains.remove', 'Remove')}\n </button>\n )}\n </div>\n ))}\n </div>\n )}\n </div>\n\n {/* Presentation (unpublished — captured into the publish) */}\n {!isPublished && (\n <div style={{ marginBottom: '16px' }}>\n <div style={{ fontSize: '12px', fontWeight: 600, color: colors.textSecondary, marginBottom: '8px' }}>\n {t('bigconsole.dashboards.embed.presentation.heading', 'Presentation')}\n </div>\n {presentationControls}\n </div>\n )}\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 {t(\n 'bigconsole.dashboards.embed.attestation',\n 'I understand this makes the dashboard’s live data visible to anyone with the link, and I confirm it is safe to be public.'\n )}\n </span>\n </label>\n <button style={primaryBtn} onClick={handlePublish} disabled={loading || !canManageEmbed} type=\"button\">\n {loading\n ? t('bigconsole.dashboards.embed.publishing', 'Publishing…')\n : t('bigconsole.dashboards.embed.publish', '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={{ marginBottom: '6px' }}>\n <span style={{ fontSize: '12px', fontWeight: 600, color: colors.textSecondary }}>\n {t('bigconsole.dashboards.embed.snippet.label', 'Embed snippet')}\n </span>\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\n ? t('bigconsole.dashboards.embed.snippet.copied', 'Copied ✓')\n : t('bigconsole.dashboards.embed.snippet.copy', '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 {t(\n 'bigconsole.dashboards.embed.snippet.alreadyActive',\n 'The embed link is already active. For security the snippet (which contains the secret key) is only shown once at publish time — use {{rotateLabel}} to generate a fresh snippet.',\n { rotateLabel: t('bigconsole.dashboards.embed.rotate', 'Rotate link') }\n )}\n </div>\n )}\n\n {/* Presentation (published — persisted + re-published to take effect) */}\n <div style={{ marginBottom: '16px' }}>\n <div style={{ fontSize: '12px', fontWeight: 600, color: colors.textSecondary, marginBottom: '8px' }}>\n {t('bigconsole.dashboards.embed.presentation.heading', 'Presentation')}\n </div>\n {presentationControls}\n <div style={{ display: 'flex', alignItems: 'center', gap: '10px', marginTop: '10px' }}>\n <button\n style={ghostBtn}\n onClick={handleApplyPresentation}\n disabled={loading || !canManageEmbed}\n type=\"button\"\n >\n {loading\n ? t('bigconsole.dashboards.embed.applying', 'Applying…')\n : t('bigconsole.dashboards.embed.apply', 'Apply changes')}\n </button>\n {presentationSaved && (\n <span style={{ fontSize: '12px', color: colors.textTertiary }}>\n {t('bigconsole.dashboards.embed.updated', 'Updated ✓')}\n </span>\n )}\n </div>\n <div style={{ fontSize: '11px', color: colors.textTertiary, marginTop: '6px' }}>\n {t(\n 'bigconsole.dashboards.embed.presentation.applyHelp',\n 'Changes apply to live embeds within a few seconds — no re-publish needed, and the embed link stays the same.'\n )}\n </div>\n </div>\n\n {/* Rotate/Unpublish are destructive management actions — hide entirely\n when the caller can't manage the embed. */}\n {canManageEmbed && (\n <div style={{ display: 'flex', gap: '8px' }}>\n <button style={ghostBtn} onClick={() => policy && rotateKey(policy.id)} disabled={loading} type=\"button\">\n {t('bigconsole.dashboards.embed.rotate', '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 t(\n 'bigconsole.dashboards.embed.unpublishConfirm',\n 'Unpublish this dashboard? The embed will stop working immediately for anyone using the link.'\n )\n )\n ) {\n void unpublish(policy.id);\n }\n }}\n disabled={loading}\n type=\"button\"\n >\n {t('bigconsole.dashboards.embed.unpublish', 'Unpublish')}\n </button>\n </div>\n )}\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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"}
|
|
@@ -5,32 +5,62 @@ import { KpiComparisonEmbed as r } from "./widgets/KpiComparisonEmbed.js";
|
|
|
5
5
|
import { TableEmbed as i } from "./widgets/TableEmbed.js";
|
|
6
6
|
import { ListEmbed as a } from "./widgets/ListEmbed.js";
|
|
7
7
|
import { TextEmbed as o } from "./widgets/TextEmbed.js";
|
|
8
|
-
import {
|
|
8
|
+
import { GaugeEmbed as s } from "./widgets/GaugeEmbed.js";
|
|
9
|
+
import { ProgressEmbed as c } from "./widgets/ProgressEmbed.js";
|
|
10
|
+
import { HeatmapEmbed as l } from "./widgets/HeatmapEmbed.js";
|
|
11
|
+
import { RetentionEmbed as u } from "./widgets/RetentionEmbed.js";
|
|
12
|
+
import { TimelineEmbed as d } from "./widgets/TimelineEmbed.js";
|
|
13
|
+
import { CalendarEmbed as f } from "./widgets/CalendarEmbed.js";
|
|
14
|
+
import { KanbanEmbed as p } from "./widgets/KanbanEmbed.js";
|
|
15
|
+
import { PivotEmbed as m } from "./widgets/PivotEmbed.js";
|
|
16
|
+
import { FormEmbed as h } from "./widgets/FormEmbed.js";
|
|
17
|
+
import { Suspense as g, lazy as _ } from "react";
|
|
18
|
+
import { jsx as v } from "react/jsx-runtime";
|
|
9
19
|
//#region src/bigconsole/components/embed/EmbedWidgetRenderer.tsx
|
|
10
|
-
var
|
|
20
|
+
var y = _(() => import("./widgets/ChartEmbed.js")), b = {
|
|
11
21
|
metric_card: n,
|
|
12
22
|
kpi_card_comparison: r,
|
|
13
23
|
table: i,
|
|
14
24
|
list: a,
|
|
15
|
-
text: o
|
|
25
|
+
text: o,
|
|
26
|
+
chart: y,
|
|
27
|
+
funnel_chart: y,
|
|
28
|
+
pivot_table: m,
|
|
29
|
+
gauge: s,
|
|
30
|
+
progress: c,
|
|
31
|
+
form: h,
|
|
32
|
+
heatmap: l,
|
|
33
|
+
calendar: f,
|
|
34
|
+
kanban: p,
|
|
35
|
+
timeline: d,
|
|
36
|
+
retention: u
|
|
16
37
|
};
|
|
17
|
-
function
|
|
18
|
-
return Object.keys(
|
|
38
|
+
function x() {
|
|
39
|
+
return Object.keys(b);
|
|
19
40
|
}
|
|
20
|
-
function
|
|
21
|
-
return /* @__PURE__ */
|
|
41
|
+
function S({ title: e }) {
|
|
42
|
+
return /* @__PURE__ */ v("div", {
|
|
22
43
|
className: "flex h-full w-full items-center justify-center bg-bg-surface p-4 text-center text-sm text-text-tertiary",
|
|
23
44
|
children: e ? `“${e}” is not available in this embed.` : "This widget is not available in this embed."
|
|
24
45
|
});
|
|
25
46
|
}
|
|
26
|
-
function
|
|
27
|
-
|
|
28
|
-
|
|
47
|
+
function C() {
|
|
48
|
+
return /* @__PURE__ */ v("div", {
|
|
49
|
+
className: "h-full w-full animate-pulse bg-bg-sunken/60",
|
|
50
|
+
"aria-busy": "true"
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
function w(n) {
|
|
54
|
+
let { widget: r } = n, i = e[r.type]?.support === "implemented" ? b[r.type] : void 0;
|
|
55
|
+
return i ? /* @__PURE__ */ v(t, {
|
|
29
56
|
label: "widget",
|
|
30
|
-
children: /* @__PURE__ */
|
|
31
|
-
|
|
57
|
+
children: /* @__PURE__ */ v(g, {
|
|
58
|
+
fallback: /* @__PURE__ */ v(C, {}),
|
|
59
|
+
children: /* @__PURE__ */ v(i, { ...n })
|
|
60
|
+
})
|
|
61
|
+
}) : /* @__PURE__ */ v(S, { title: r.title });
|
|
32
62
|
}
|
|
33
63
|
//#endregion
|
|
34
|
-
export {
|
|
64
|
+
export { w as EmbedWidgetRenderer, x as getImplementedRendererTypes };
|
|
35
65
|
|
|
36
66
|
//# sourceMappingURL=EmbedWidgetRenderer.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"EmbedWidgetRenderer.js","names":[],"sources":["../../../../src/bigconsole/components/embed/EmbedWidgetRenderer.tsx"],"sourcesContent":["/**\n * Strict embed widget dispatcher (BOFF-2986).\n *\n * Maps a sanitized widget DTO to its embed renderer via an EXACT registry.\n * There is NO silent fallback (unlike the authenticated WidgetRendererFactory):\n * a type that is not `implemented` in the coverage manifest renders a neutral\n * \"not available\" placeholder, never a guessed or partial widget. Each widget is\n * wrapped in its own error boundary so one failure can't take down the page.\n */\n\nimport type
|
|
1
|
+
{"version":3,"file":"EmbedWidgetRenderer.js","names":[],"sources":["../../../../src/bigconsole/components/embed/EmbedWidgetRenderer.tsx"],"sourcesContent":["/**\n * Strict embed widget dispatcher (BOFF-2986).\n *\n * Maps a sanitized widget DTO to its embed renderer via an EXACT registry.\n * There is NO silent fallback (unlike the authenticated WidgetRendererFactory):\n * a type that is not `implemented` in the coverage manifest renders a neutral\n * \"not available\" placeholder, never a guessed or partial widget. Each widget is\n * wrapped in its own error boundary so one failure can't take down the page.\n */\n\nimport { lazy, Suspense, type ComponentType } from 'react';\nimport type { WidgetType } from '../../types';\nimport type { EmbedWidgetRenderProps } from './types';\nimport { EMBED_WIDGET_COVERAGE } from './renderer-manifest';\nimport { EmbedErrorBoundary } from './EmbedErrorBoundary';\nimport { MetricCardEmbed } from './widgets/MetricCardEmbed';\nimport { KpiComparisonEmbed } from './widgets/KpiComparisonEmbed';\nimport { TableEmbed } from './widgets/TableEmbed';\nimport { ListEmbed } from './widgets/ListEmbed';\nimport { TextEmbed } from './widgets/TextEmbed';\nimport { GaugeEmbed } from './widgets/GaugeEmbed';\nimport { ProgressEmbed } from './widgets/ProgressEmbed';\nimport { HeatmapEmbed } from './widgets/HeatmapEmbed';\nimport { RetentionEmbed } from './widgets/RetentionEmbed';\nimport { TimelineEmbed } from './widgets/TimelineEmbed';\nimport { CalendarEmbed } from './widgets/CalendarEmbed';\nimport { KanbanEmbed } from './widgets/KanbanEmbed';\nimport { PivotEmbed } from './widgets/PivotEmbed';\nimport { FormEmbed } from './widgets/FormEmbed';\n\n// recharts is by far the largest dependency of the embed bundle — load it only\n// when a published dashboard actually contains a chart (BOFF-7926). The host\n// build must emit chunks relative to the entry (`base: './'`) so the chunk is\n// fetched from the asset origin the embed CSP allows.\nconst ChartEmbed = lazy(() => import('./widgets/ChartEmbed'));\n\n/**\n * Registry of implemented renderers. Every key here MUST be marked\n * `implemented` in EMBED_WIDGET_COVERAGE, and vice-versa — the completeness test\n * asserts the two stay in lockstep, so a renderer can never be silently missing\n * for a type the manifest claims to support.\n */\nconst IMPLEMENTED_RENDERERS: Partial<Record<WidgetType, ComponentType<EmbedWidgetRenderProps>>> = {\n metric_card: MetricCardEmbed,\n kpi_card_comparison: KpiComparisonEmbed,\n table: TableEmbed,\n list: ListEmbed,\n text: TextEmbed,\n chart: ChartEmbed,\n funnel_chart: ChartEmbed,\n pivot_table: PivotEmbed,\n gauge: GaugeEmbed,\n progress: ProgressEmbed,\n form: FormEmbed,\n heatmap: HeatmapEmbed,\n calendar: CalendarEmbed,\n kanban: KanbanEmbed,\n timeline: TimelineEmbed,\n retention: RetentionEmbed,\n};\n\nexport function getImplementedRendererTypes(): WidgetType[] {\n return Object.keys(IMPLEMENTED_RENDERERS) as WidgetType[];\n}\n\nfunction UnavailableWidget({ title }: { title?: string }) {\n return (\n <div className=\"flex h-full w-full items-center justify-center bg-bg-surface p-4 text-center text-sm text-text-tertiary\">\n {title ? `“${title}” is not available in this embed.` : 'This widget is not available in this embed.'}\n </div>\n );\n}\n\nfunction WidgetLoading() {\n return <div className=\"h-full w-full animate-pulse bg-bg-sunken/60\" aria-busy=\"true\" />;\n}\n\nexport function EmbedWidgetRenderer(props: EmbedWidgetRenderProps) {\n const { widget } = props;\n const coverage = EMBED_WIDGET_COVERAGE[widget.type];\n const Renderer = coverage?.support === 'implemented' ? IMPLEMENTED_RENDERERS[widget.type] : undefined;\n\n if (!Renderer) {\n return <UnavailableWidget title={widget.title} />;\n }\n\n return (\n <EmbedErrorBoundary label=\"widget\">\n <Suspense fallback={<WidgetLoading />}>\n <Renderer {...props} />\n </Suspense>\n </EmbedErrorBoundary>\n );\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;AAkCA,IAAM,IAAa,QAAW,OAAO,2BAAwB,EAQvD,IAA4F;CAChG,aAAa;CACb,qBAAqB;CACrB,OAAO;CACP,MAAM;CACN,MAAM;CACN,OAAO;CACP,cAAc;CACd,aAAa;CACb,OAAO;CACP,UAAU;CACV,MAAM;CACN,SAAS;CACT,UAAU;CACV,QAAQ;CACR,UAAU;CACV,WAAW;CACZ;AAED,SAAgB,IAA4C;AAC1D,QAAO,OAAO,KAAK,EAAsB;;AAG3C,SAAS,EAAkB,EAAE,YAA6B;AACxD,QACE,kBAAC,OAAD;EAAK,WAAU;YACZ,IAAQ,IAAI,EAAM,qCAAqC;EACpD,CAAA;;AAIV,SAAS,IAAgB;AACvB,QAAO,kBAAC,OAAD;EAAK,WAAU;EAA8C,aAAU;EAAS,CAAA;;AAGzF,SAAgB,EAAoB,GAA+B;CACjE,IAAM,EAAE,cAAW,GAEb,IADW,EAAsB,EAAO,OACnB,YAAY,gBAAgB,EAAsB,EAAO,QAAQ,KAAA;AAM5F,QAJK,IAKH,kBAAC,GAAD;EAAoB,OAAM;YACxB,kBAAC,GAAD;GAAU,UAAU,kBAAC,GAAD,EAAiB,CAAA;aACnC,kBAAC,GAAD,EAAU,GAAI,GAAS,CAAA;GACd,CAAA;EACQ,CAAA,GARd,kBAAC,GAAD,EAAmB,OAAO,EAAO,OAAS,CAAA"}
|
|
@@ -43,8 +43,8 @@ function s() {
|
|
|
43
43
|
});
|
|
44
44
|
}
|
|
45
45
|
function c({ model: t }) {
|
|
46
|
-
let [a, c] = n(0), l = t.pages, u = t.presentation, d = u?.showTitle ? (u.title ?? "").trim() : "", f =
|
|
47
|
-
return
|
|
46
|
+
let [a, c] = n(0), l = t.pages, u = t.presentation, d = u?.showTitle ? (u.title ?? "").trim() : "", f = l[Math.min(a, l.length - 1)];
|
|
47
|
+
return f ? /* @__PURE__ */ r(e, {
|
|
48
48
|
label: "dashboard",
|
|
49
49
|
children: /* @__PURE__ */ i("div", {
|
|
50
50
|
className: "flex h-full w-full flex-col bg-bg-canvas",
|
|
@@ -70,11 +70,11 @@ function c({ model: t }) {
|
|
|
70
70
|
/* @__PURE__ */ r("div", {
|
|
71
71
|
className: "min-h-0 flex-1 overflow-auto",
|
|
72
72
|
children: /* @__PURE__ */ r(o, {
|
|
73
|
-
page:
|
|
73
|
+
page: f,
|
|
74
74
|
capabilities: t.capabilities
|
|
75
75
|
})
|
|
76
76
|
}),
|
|
77
|
-
|
|
77
|
+
/* @__PURE__ */ r(s, {})
|
|
78
78
|
]
|
|
79
79
|
})
|
|
80
80
|
}) : /* @__PURE__ */ i("div", {
|
|
@@ -91,7 +91,7 @@ function c({ model: t }) {
|
|
|
91
91
|
className: "flex flex-1 items-center justify-center text-sm text-text-secondary",
|
|
92
92
|
children: "This dashboard has no pages."
|
|
93
93
|
}),
|
|
94
|
-
|
|
94
|
+
/* @__PURE__ */ r(s, {})
|
|
95
95
|
]
|
|
96
96
|
});
|
|
97
97
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ReadOnlyDashboardRenderer.js","names":[],"sources":["../../../../src/bigconsole/components/embed/ReadOnlyDashboardRenderer.tsx"],"sourcesContent":["/**\n * Read-only embedded dashboard renderer (BOFF-2986).\n *\n * Renders the sanitized render model's pages + widgets on a simple responsive\n * grid. It is STRICTLY view-only: no builder toolbar, edit toggle, sharing,\n * save, palette, inspector or comments — those authenticated affordances do not\n * exist in this component tree at all.\n */\n\nimport { useState } from 'react';\nimport type { EmbedCapabilities, EmbedPageModel, EmbedRenderModel } from './types';\nimport { EmbedWidgetRenderer } from './EmbedWidgetRenderer';\nimport { EmbedErrorBoundary } from './EmbedErrorBoundary';\n\nconst GRID_COLUMNS = 12;\n\ninterface Props {\n readonly model: EmbedRenderModel;\n}\n\nfunction PageGrid({ page, capabilities }: { page: EmbedPageModel; capabilities: EmbedCapabilities }) {\n if (page.widgets.length === 0) {\n return (\n <div className=\"flex h-40 items-center justify-center text-sm text-text-secondary\">This page has no widgets.</div>\n );\n }\n return (\n <div\n className=\"grid w-full gap-4 p-4\"\n style={{ gridTemplateColumns: `repeat(${GRID_COLUMNS}, minmax(0, 1fr))`, gridAutoRows: 'minmax(80px, auto)' }}\n >\n {page.widgets.map((widget) => (\n <div\n key={widget.id}\n className=\"overflow-hidden rounded-lg border border-border-default bg-bg-surface shadow-sm\"\n style={{\n // Clamp both ends: negative/oversized coordinates from malformed\n // model data must never produce an invalid CSS grid line.\n gridColumn: `${Math.min(Math.max(widget.layout.x, 0), GRID_COLUMNS - 1) + 1} / span ${Math.min(\n Math.max(widget.layout.w, 1),\n GRID_COLUMNS\n )}`,\n gridRow: `${Math.max(widget.layout.y, 0) + 1} / span ${Math.max(widget.layout.h, 1)}`,\n }}\n >\n <EmbedWidgetRenderer widget={widget} capabilities={capabilities} />\n </div>\n ))}\n </div>\n );\n}\n\n/**
|
|
1
|
+
{"version":3,"file":"ReadOnlyDashboardRenderer.js","names":[],"sources":["../../../../src/bigconsole/components/embed/ReadOnlyDashboardRenderer.tsx"],"sourcesContent":["/**\n * Read-only embedded dashboard renderer (BOFF-2986).\n *\n * Renders the sanitized render model's pages + widgets on a simple responsive\n * grid. It is STRICTLY view-only: no builder toolbar, edit toggle, sharing,\n * save, palette, inspector or comments — those authenticated affordances do not\n * exist in this component tree at all.\n *\n * Every embed carries the \"Powered by BigConsole\" footer. Burdenoff does no\n * white-labelling, so there is deliberately no model field, policy option or\n * URL parameter that hides it.\n */\n\nimport { useState } from 'react';\nimport type { EmbedCapabilities, EmbedPageModel, EmbedRenderModel } from './types';\nimport { EmbedWidgetRenderer } from './EmbedWidgetRenderer';\nimport { EmbedErrorBoundary } from './EmbedErrorBoundary';\n\nconst GRID_COLUMNS = 12;\n\ninterface Props {\n readonly model: EmbedRenderModel;\n}\n\nfunction PageGrid({ page, capabilities }: { page: EmbedPageModel; capabilities: EmbedCapabilities }) {\n if (page.widgets.length === 0) {\n return (\n <div className=\"flex h-40 items-center justify-center text-sm text-text-secondary\">This page has no widgets.</div>\n );\n }\n return (\n <div\n className=\"grid w-full gap-4 p-4\"\n style={{ gridTemplateColumns: `repeat(${GRID_COLUMNS}, minmax(0, 1fr))`, gridAutoRows: 'minmax(80px, auto)' }}\n >\n {page.widgets.map((widget) => (\n <div\n key={widget.id}\n className=\"overflow-hidden rounded-lg border border-border-default bg-bg-surface shadow-sm\"\n style={{\n // Clamp both ends: negative/oversized coordinates from malformed\n // model data must never produce an invalid CSS grid line.\n gridColumn: `${Math.min(Math.max(widget.layout.x, 0), GRID_COLUMNS - 1) + 1} / span ${Math.min(\n Math.max(widget.layout.w, 1),\n GRID_COLUMNS\n )}`,\n gridRow: `${Math.max(widget.layout.y, 0) + 1} / span ${Math.max(widget.layout.h, 1)}`,\n }}\n >\n <EmbedWidgetRenderer widget={widget} capabilities={capabilities} />\n </div>\n ))}\n </div>\n );\n}\n\n/** \"Powered by BigConsole\" attribution footer, rendered on every embed (never optional). */\nfunction BrandingFooter() {\n return (\n <div className=\"flex shrink-0 items-center justify-end border-t border-border-default bg-bg-surface px-4 py-2\">\n <a\n href=\"https://bigconsole.com\"\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n className=\"text-xs text-text-tertiary transition-colors hover:text-text-secondary\"\n >\n Powered by <span className=\"font-semibold text-text-secondary\">BigConsole</span>\n </a>\n </div>\n );\n}\n\nexport function ReadOnlyDashboardRenderer({ model }: Props) {\n const [activePageIndex, setActivePageIndex] = useState(0);\n const pages = model.pages;\n const presentation = model.presentation;\n // Only show a header when the owner enabled it AND there's a non-empty title.\n const headerTitle = presentation?.showTitle ? (presentation.title ?? '').trim() : '';\n const activePage = pages[Math.min(activePageIndex, pages.length - 1)];\n\n if (!activePage) {\n return (\n <div className=\"flex h-full flex-col bg-bg-canvas\">\n {headerTitle && (\n <header className=\"shrink-0 border-b border-border-default bg-bg-surface px-4 py-3\">\n <h1 className=\"truncate text-base font-semibold text-text-primary\">{headerTitle}</h1>\n </header>\n )}\n <div className=\"flex flex-1 items-center justify-center text-sm text-text-secondary\">\n This dashboard has no pages.\n </div>\n <BrandingFooter />\n </div>\n );\n }\n\n return (\n <EmbedErrorBoundary label=\"dashboard\">\n <div className=\"flex h-full w-full flex-col bg-bg-canvas\">\n {headerTitle && (\n <header className=\"shrink-0 border-b border-border-default bg-bg-surface px-4 py-3\">\n <h1 className=\"truncate text-base font-semibold text-text-primary\">{headerTitle}</h1>\n </header>\n )}\n {pages.length > 1 && (\n <div role=\"tablist\" className=\"flex gap-1 border-b border-border-default bg-bg-surface px-2\">\n {pages.map((page, i) => (\n <button\n key={page.id}\n role=\"tab\"\n aria-selected={i === activePageIndex}\n onClick={() => setActivePageIndex(i)}\n className={\n i === activePageIndex\n ? '-mb-px border-b-2 border-action-primary-bg px-3 py-2 text-sm font-medium text-text-primary'\n : 'border-b-2 border-transparent px-3 py-2 text-sm text-text-secondary hover:text-text-primary'\n }\n >\n {page.title ?? `Page ${i + 1}`}\n </button>\n ))}\n </div>\n )}\n <div className=\"min-h-0 flex-1 overflow-auto\">\n <PageGrid page={activePage} capabilities={model.capabilities} />\n </div>\n <BrandingFooter />\n </div>\n </EmbedErrorBoundary>\n );\n}\n"],"mappings":";;;;;AAkBA,IAAM,IAAe;AAMrB,SAAS,EAAS,EAAE,SAAM,mBAA2E;AAMnG,QALI,EAAK,QAAQ,WAAW,IAExB,kBAAC,OAAD;EAAK,WAAU;YAAoE;EAA+B,CAAA,GAIpH,kBAAC,OAAD;EACE,WAAU;EACV,OAAO;GAAE,qBAAqB,UAAU,EAAa;GAAoB,cAAc;GAAsB;YAE5G,EAAK,QAAQ,KAAK,MACjB,kBAAC,OAAD;GAEE,WAAU;GACV,OAAO;IAGL,YAAY,GAAG,KAAK,IAAI,KAAK,IAAI,EAAO,OAAO,GAAG,EAAE,EAAE,IAAe,EAAE,GAAG,EAAE,UAAU,KAAK,IACzF,KAAK,IAAI,EAAO,OAAO,GAAG,EAAE,EAC5B,EACD;IACD,SAAS,GAAG,KAAK,IAAI,EAAO,OAAO,GAAG,EAAE,GAAG,EAAE,UAAU,KAAK,IAAI,EAAO,OAAO,GAAG,EAAE;IACpF;aAED,kBAAC,GAAD;IAA6B;IAAsB;IAAgB,CAAA;GAC/D,EAbC,EAAO,GAaR,CACN;EACE,CAAA;;AAKV,SAAS,IAAiB;AACxB,QACE,kBAAC,OAAD;EAAK,WAAU;YACb,kBAAC,KAAD;GACE,MAAK;GACL,QAAO;GACP,KAAI;GACJ,WAAU;aAJZ,CAKC,eACY,kBAAC,QAAD;IAAM,WAAU;cAAoC;IAAiB,CAAA,CAC9E;;EACA,CAAA;;AAIV,SAAgB,EAA0B,EAAE,YAAgB;CAC1D,IAAM,CAAC,GAAiB,KAAsB,EAAS,EAAE,EACnD,IAAQ,EAAM,OACd,IAAe,EAAM,cAErB,IAAc,GAAc,aAAa,EAAa,SAAS,IAAI,MAAM,GAAG,IAC5E,IAAa,EAAM,KAAK,IAAI,GAAiB,EAAM,SAAS,EAAE;AAkBpE,QAhBK,IAiBH,kBAAC,GAAD;EAAoB,OAAM;YACxB,kBAAC,OAAD;GAAK,WAAU;aAAf;IACG,KACC,kBAAC,UAAD;KAAQ,WAAU;eAChB,kBAAC,MAAD;MAAI,WAAU;gBAAsD;MAAiB,CAAA;KAC9E,CAAA;IAEV,EAAM,SAAS,KACd,kBAAC,OAAD;KAAK,MAAK;KAAU,WAAU;eAC3B,EAAM,KAAK,GAAM,MAChB,kBAAC,UAAD;MAEE,MAAK;MACL,iBAAe,MAAM;MACrB,eAAe,EAAmB,EAAE;MACpC,WACE,MAAM,IACF,+FACA;gBAGL,EAAK,SAAS,QAAQ,IAAI;MACpB,EAXF,EAAK,GAWH,CACT;KACE,CAAA;IAER,kBAAC,OAAD;KAAK,WAAU;eACb,kBAAC,GAAD;MAAU,MAAM;MAAY,cAAc,EAAM;MAAgB,CAAA;KAC5D,CAAA;IACN,kBAAC,GAAD,EAAkB,CAAA;IACd;;EACa,CAAA,GA9CnB,kBAAC,OAAD;EAAK,WAAU;YAAf;GACG,KACC,kBAAC,UAAD;IAAQ,WAAU;cAChB,kBAAC,MAAD;KAAI,WAAU;eAAsD;KAAiB,CAAA;IAC9E,CAAA;GAEX,kBAAC,OAAD;IAAK,WAAU;cAAsE;IAE/E,CAAA;GACN,kBAAC,GAAD,EAAkB,CAAA;GACd"}
|
|
@@ -6,61 +6,28 @@ var e = {
|
|
|
6
6
|
table: { support: "implemented" },
|
|
7
7
|
list: { support: "implemented" },
|
|
8
8
|
text: { support: "implemented" },
|
|
9
|
-
chart: {
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
},
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
},
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
},
|
|
21
|
-
gauge: {
|
|
22
|
-
support: "blocked",
|
|
23
|
-
reason: "gauge renderer pending"
|
|
24
|
-
},
|
|
25
|
-
progress: {
|
|
26
|
-
support: "blocked",
|
|
27
|
-
reason: "progress renderer pending"
|
|
28
|
-
},
|
|
29
|
-
form: {
|
|
30
|
-
support: "blocked",
|
|
31
|
-
reason: "requires side-effect action broker (post read-only MVP)"
|
|
32
|
-
},
|
|
9
|
+
chart: { support: "implemented" },
|
|
10
|
+
funnel_chart: { support: "implemented" },
|
|
11
|
+
pivot_table: { support: "implemented" },
|
|
12
|
+
gauge: { support: "implemented" },
|
|
13
|
+
progress: { support: "implemented" },
|
|
14
|
+
form: { support: "implemented" },
|
|
15
|
+
heatmap: { support: "implemented" },
|
|
16
|
+
calendar: { support: "implemented" },
|
|
17
|
+
kanban: { support: "implemented" },
|
|
18
|
+
timeline: { support: "implemented" },
|
|
19
|
+
retention: { support: "implemented" },
|
|
33
20
|
iframe: {
|
|
34
21
|
support: "blocked",
|
|
35
|
-
reason: "requires frozen exact-origin
|
|
22
|
+
reason: "requires publish-time frozen exact-origin + frame-src CSP (BOFF-7926 slice 9)"
|
|
36
23
|
},
|
|
37
24
|
map: {
|
|
38
25
|
support: "blocked",
|
|
39
|
-
reason: "requires
|
|
40
|
-
},
|
|
41
|
-
heatmap: {
|
|
42
|
-
support: "blocked",
|
|
43
|
-
reason: "heatmap renderer pending"
|
|
44
|
-
},
|
|
45
|
-
calendar: {
|
|
46
|
-
support: "blocked",
|
|
47
|
-
reason: "calendar renderer pending"
|
|
48
|
-
},
|
|
49
|
-
kanban: {
|
|
50
|
-
support: "blocked",
|
|
51
|
-
reason: "kanban renderer pending"
|
|
52
|
-
},
|
|
53
|
-
timeline: {
|
|
54
|
-
support: "blocked",
|
|
55
|
-
reason: "timeline renderer pending"
|
|
56
|
-
},
|
|
57
|
-
retention: {
|
|
58
|
-
support: "blocked",
|
|
59
|
-
reason: "retention/cohort renderer pending"
|
|
26
|
+
reason: "requires publish-time tile provider + img-src CSP (BOFF-7926 slice 9)"
|
|
60
27
|
},
|
|
61
28
|
custom: {
|
|
62
29
|
support: "blocked",
|
|
63
|
-
reason: "requires nested opaque-origin sandbox
|
|
30
|
+
reason: "requires nested opaque-origin sandbox document (BOFF-7926 slice 10)"
|
|
64
31
|
}
|
|
65
32
|
};
|
|
66
33
|
function t(t) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"renderer-manifest.js","names":[],"sources":["../../../../src/bigconsole/components/embed/renderer-manifest.ts"],"sourcesContent":["/**\n * Embed widget coverage manifest (BOFF-2986).\n *\n * A COMPLETE, explicit decision for every persisted widget type. There is no\n * silent fallback: a type is either `implemented` (has a sanitizer + embed\n * renderer + test) or `blocked` (publication containing it is rejected, and the\n * runtime renders a safe placeholder — never a wrong or partial widget).\n *\n * The completeness test (`renderer-manifest.test.ts`) enumerates the\n * `WidgetType` union and fails the build if any type is missing from this map,\n * so a newly-added widget type cannot ship without an explicit embed decision.\n */\n\nimport type { WidgetType } from '../../types';\nimport { EMBED_RENDER_MODEL_VERSION } from './types';\n\nexport type EmbedWidgetSupport = 'implemented' | 'blocked';\n\nexport interface EmbedWidgetCoverage {\n readonly support: EmbedWidgetSupport;\n /** Why a type is blocked (surfaced to publication validation + operators). */\n readonly reason?: string;\n}\n\n/**\n * Every `WidgetType` MUST appear here. Implemented set is the read-only MVP\n * native-data core; the rest are blocked with a concrete reason until their\n * sanitizer + renderer + tests land (each will flip to `implemented` in a\n * follow-up within this same renderer PR).\n */\nexport const EMBED_WIDGET_COVERAGE: Record<WidgetType, EmbedWidgetCoverage> = {\n // --- implemented (native data, no external fetch, no sandbox) ---\n metric_card: { support: 'implemented' },\n kpi_card_comparison: { support: 'implemented' },\n table: { support: 'implemented' },\n list: { support: 'implemented' },\n text: { support: 'implemented' },\n\n // --- blocked (pending sanitizer/renderer/tests in this PR) ---\n chart: { support: '
|
|
1
|
+
{"version":3,"file":"renderer-manifest.js","names":[],"sources":["../../../../src/bigconsole/components/embed/renderer-manifest.ts"],"sourcesContent":["/**\n * Embed widget coverage manifest (BOFF-2986).\n *\n * A COMPLETE, explicit decision for every persisted widget type. There is no\n * silent fallback: a type is either `implemented` (has a sanitizer + embed\n * renderer + test) or `blocked` (publication containing it is rejected, and the\n * runtime renders a safe placeholder — never a wrong or partial widget).\n *\n * The completeness test (`renderer-manifest.test.ts`) enumerates the\n * `WidgetType` union and fails the build if any type is missing from this map,\n * so a newly-added widget type cannot ship without an explicit embed decision.\n */\n\nimport type { WidgetType } from '../../types';\nimport { EMBED_RENDER_MODEL_VERSION } from './types';\n\nexport type EmbedWidgetSupport = 'implemented' | 'blocked';\n\nexport interface EmbedWidgetCoverage {\n readonly support: EmbedWidgetSupport;\n /** Why a type is blocked (surfaced to publication validation + operators). */\n readonly reason?: string;\n}\n\n/**\n * Every `WidgetType` MUST appear here. Implemented set is the read-only MVP\n * native-data core; the rest are blocked with a concrete reason until their\n * sanitizer + renderer + tests land (each will flip to `implemented` in a\n * follow-up within this same renderer PR).\n */\nexport const EMBED_WIDGET_COVERAGE: Record<WidgetType, EmbedWidgetCoverage> = {\n // --- implemented (native data, no external fetch, no sandbox) ---\n metric_card: { support: 'implemented' },\n kpi_card_comparison: { support: 'implemented' },\n table: { support: 'implemented' },\n list: { support: 'implemented' },\n text: { support: 'implemented' },\n\n // --- blocked (pending sanitizer/renderer/tests in this PR) ---\n chart: { support: 'implemented' },\n funnel_chart: { support: 'implemented' },\n pivot_table: { support: 'implemented' },\n gauge: { support: 'implemented' },\n progress: { support: 'implemented' },\n form: { support: 'implemented' },\n heatmap: { support: 'implemented' },\n calendar: { support: 'implemented' },\n kanban: { support: 'implemented' },\n timeline: { support: 'implemented' },\n retention: { support: 'implemented' },\n\n iframe: {\n support: 'blocked',\n reason: 'requires publish-time frozen exact-origin + frame-src CSP (BOFF-7926 slice 9)',\n },\n map: { support: 'blocked', reason: 'requires publish-time tile provider + img-src CSP (BOFF-7926 slice 9)' },\n custom: { support: 'blocked', reason: 'requires nested opaque-origin sandbox document (BOFF-7926 slice 10)' },\n};\n\nexport function isWidgetTypeEmbeddable(type: WidgetType): boolean {\n return EMBED_WIDGET_COVERAGE[type]?.support === 'implemented';\n}\n\n/** Immutable manifest emitted alongside the asset bundle for contract auditing. */\nexport const EMBED_RENDERER_MANIFEST = {\n renderModelVersion: EMBED_RENDER_MODEL_VERSION,\n implemented: Object.entries(EMBED_WIDGET_COVERAGE)\n .filter(([, v]) => v.support === 'implemented')\n .map(([k]) => k)\n .sort(),\n blocked: Object.entries(EMBED_WIDGET_COVERAGE)\n .filter(([, v]) => v.support === 'blocked')\n .map(([k]) => k)\n .sort(),\n} as const;\n"],"mappings":";;AA8BA,IAAa,IAAiE;CAE5E,aAAa,EAAE,SAAS,eAAe;CACvC,qBAAqB,EAAE,SAAS,eAAe;CAC/C,OAAO,EAAE,SAAS,eAAe;CACjC,MAAM,EAAE,SAAS,eAAe;CAChC,MAAM,EAAE,SAAS,eAAe;CAGhC,OAAO,EAAE,SAAS,eAAe;CACjC,cAAc,EAAE,SAAS,eAAe;CACxC,aAAa,EAAE,SAAS,eAAe;CACvC,OAAO,EAAE,SAAS,eAAe;CACjC,UAAU,EAAE,SAAS,eAAe;CACpC,MAAM,EAAE,SAAS,eAAe;CAChC,SAAS,EAAE,SAAS,eAAe;CACnC,UAAU,EAAE,SAAS,eAAe;CACpC,QAAQ,EAAE,SAAS,eAAe;CAClC,UAAU,EAAE,SAAS,eAAe;CACpC,WAAW,EAAE,SAAS,eAAe;CAErC,QAAQ;EACN,SAAS;EACT,QAAQ;EACT;CACD,KAAK;EAAE,SAAS;EAAW,QAAQ;EAAyE;CAC5G,QAAQ;EAAE,SAAS;EAAW,QAAQ;EAAuE;CAC9G;AAED,SAAgB,EAAuB,GAA2B;AAChE,QAAO,EAAsB,IAAO,YAAY;;AAIlD,IAAa,IAA0B;CACrC,oBAAA;CACA,aAAa,OAAO,QAAQ,EAAsB,CAC/C,QAAQ,GAAG,OAAO,EAAE,YAAY,cAAc,CAC9C,KAAK,CAAC,OAAO,EAAE,CACf,MAAM;CACT,SAAS,OAAO,QAAQ,EAAsB,CAC3C,QAAQ,GAAG,OAAO,EAAE,YAAY,UAAU,CAC1C,KAAK,CAAC,OAAO,EAAE,CACf,MAAM;CACV"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.js","names":[],"sources":["../../../../src/bigconsole/components/embed/types.ts"],"sourcesContent":["/**\n * Stable, versioned embed render-model contract (BOFF-2986).\n *\n * This is the ONLY shape the read-only embed renderer consumes. It is a\n * sanitized DTO produced by the BigConsole publication builder — it deliberately\n * carries NO authenticated stores, Apollo clients, data-source configs,\n * credentials or internal ids. The renderer never fetches data itself; all\n * values are pre-projected into the model or fetched later through the\n * publication-scoped interaction controller using opaque aliases.\n *\n * Backend (publication builder) and frontend (renderer) MUST agree on\n * `EMBED_RENDER_MODEL_VERSION`. A mismatch fails closed at load time rather than\n * rendering a stale/incompatible view.\n */\n\nimport type { WidgetType } from '../../types';\n\n/** Bump on any breaking change to the DTO shapes below. */\nexport const EMBED_RENDER_MODEL_VERSION = 1 as const;\n\n/** Pure layout box (grid units); no store, no event emission. */\nexport interface EmbedWidgetLayout {\n readonly x: number;\n readonly y: number;\n readonly w: number;\n readonly h: number;\n}\n\n/**\n * A single widget as projected for embedding. `config` and `data` are already\n * sanitized and schema-narrowed per widget type by the publication builder; the\n * renderer validates them again against its per-type schema before rendering.\n */\nexport interface EmbedWidgetModel {\n /** Opaque, publication-scoped widget id (not the internal DB id). */\n readonly id: string;\n readonly type: WidgetType;\n readonly title?: string;\n readonly layout: EmbedWidgetLayout;\n /** Sanitized, type-narrowed presentation config. */\n readonly config: Readonly<Record<string, unknown>>;\n /** Sanitized, pre-projected data payload (or null for interaction-fetched). */\n readonly data: Readonly<Record<string, unknown>> | null;\n}\n\nexport interface EmbedPageModel {\n readonly id: string;\n readonly title?: string;\n readonly widgets: readonly EmbedWidgetModel[];\n}\n\n/**\n * Publication-approved runtime capabilities. Everything defaults OFF; the\n * renderer must never enable an interaction the publication did not grant.\n */\nexport interface EmbedCapabilities {\n readonly filters: boolean;\n readonly drilldown: boolean;\n readonly downloads: boolean;\n /** Side-effect actions (forms/webhooks/workflows) — out of scope for read-only MVP. */\n readonly actions: boolean;\n}\n\n/** Publish-time color mode chosen by the owner (BOFF-5735). */\nexport type EmbedThemeMode = 'light' | 'dark' | 'auto';\n\n/**\n * Owner-chosen, publish-time presentation options (BOFF-5735), attached to the\n * render model (resolved live from the policy). Optional: pre-5735 render models\n * carry no `presentation` block, so the renderer must default gracefully (light,\n * no header, no
|
|
1
|
+
{"version":3,"file":"types.js","names":[],"sources":["../../../../src/bigconsole/components/embed/types.ts"],"sourcesContent":["/**\n * Stable, versioned embed render-model contract (BOFF-2986).\n *\n * This is the ONLY shape the read-only embed renderer consumes. It is a\n * sanitized DTO produced by the BigConsole publication builder — it deliberately\n * carries NO authenticated stores, Apollo clients, data-source configs,\n * credentials or internal ids. The renderer never fetches data itself; all\n * values are pre-projected into the model or fetched later through the\n * publication-scoped interaction controller using opaque aliases.\n *\n * Backend (publication builder) and frontend (renderer) MUST agree on\n * `EMBED_RENDER_MODEL_VERSION`. A mismatch fails closed at load time rather than\n * rendering a stale/incompatible view.\n */\n\nimport type { WidgetType } from '../../types';\n\n/** Bump on any breaking change to the DTO shapes below. */\nexport const EMBED_RENDER_MODEL_VERSION = 1 as const;\n\n/** Pure layout box (grid units); no store, no event emission. */\nexport interface EmbedWidgetLayout {\n readonly x: number;\n readonly y: number;\n readonly w: number;\n readonly h: number;\n}\n\n/**\n * A single widget as projected for embedding. `config` and `data` are already\n * sanitized and schema-narrowed per widget type by the publication builder; the\n * renderer validates them again against its per-type schema before rendering.\n */\nexport interface EmbedWidgetModel {\n /** Opaque, publication-scoped widget id (not the internal DB id). */\n readonly id: string;\n readonly type: WidgetType;\n readonly title?: string;\n readonly layout: EmbedWidgetLayout;\n /** Sanitized, type-narrowed presentation config. */\n readonly config: Readonly<Record<string, unknown>>;\n /** Sanitized, pre-projected data payload (or null for interaction-fetched). */\n readonly data: Readonly<Record<string, unknown>> | null;\n}\n\nexport interface EmbedPageModel {\n readonly id: string;\n readonly title?: string;\n readonly widgets: readonly EmbedWidgetModel[];\n}\n\n/**\n * Publication-approved runtime capabilities. Everything defaults OFF; the\n * renderer must never enable an interaction the publication did not grant.\n */\nexport interface EmbedCapabilities {\n readonly filters: boolean;\n readonly drilldown: boolean;\n readonly downloads: boolean;\n /** Side-effect actions (forms/webhooks/workflows) — out of scope for read-only MVP. */\n readonly actions: boolean;\n}\n\n/** Publish-time color mode chosen by the owner (BOFF-5735). */\nexport type EmbedThemeMode = 'light' | 'dark' | 'auto';\n\n/**\n * Owner-chosen, publish-time presentation options (BOFF-5735), attached to the\n * render model (resolved live from the policy). Optional: pre-5735 render models\n * carry no `presentation` block, so the renderer must default gracefully (light,\n * no header) when absent.\n *\n * Branding is NOT a presentation option: the \"Powered by BigConsole\" footer is\n * rendered on every embed, with or without this block. Burdenoff does no\n * white-labelling, so never add a field here that hides or replaces it.\n */\nexport interface EmbedPresentation {\n readonly theme: EmbedThemeMode;\n readonly showTitle: boolean;\n /** Dashboard name for the optional header, or null when hidden/empty. */\n readonly title: string | null;\n}\n\nexport interface EmbedRenderModel {\n readonly renderModelVersion: number;\n readonly clientContractVersion: number;\n readonly title?: string;\n readonly pages: readonly EmbedPageModel[];\n readonly capabilities: EmbedCapabilities;\n readonly presentation?: EmbedPresentation;\n /** ISO timestamp the immutable publication was frozen. */\n readonly definitionPublishedAt: string;\n}\n\n/** Result of validating an inbound render model before rendering. */\nexport type EmbedModelValidation =\n { readonly ok: true; readonly model: EmbedRenderModel } | { readonly ok: false; readonly reason: string };\n\n/**\n * Shape every embed widget renderer component receives. It gets the sanitized\n * model plus a controller for publication-scoped interactions — never raw hooks.\n */\nexport interface EmbedWidgetRenderProps {\n readonly widget: EmbedWidgetModel;\n readonly capabilities: EmbedCapabilities;\n}\n"],"mappings":";AAkBA,IAAa,IAA6B"}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
//#region src/bigconsole/components/embed/widgetShim.ts
|
|
2
|
+
var e = "1970-01-01T00:00:00.000Z";
|
|
3
|
+
function t(t) {
|
|
4
|
+
let { x: n, y: r, w: i, h: a } = t.layout;
|
|
5
|
+
return {
|
|
6
|
+
id: t.id,
|
|
7
|
+
pageId: "",
|
|
8
|
+
type: t.type,
|
|
9
|
+
title: t.title ?? "",
|
|
10
|
+
config: { ...t.config },
|
|
11
|
+
position: {
|
|
12
|
+
x: n,
|
|
13
|
+
y: r,
|
|
14
|
+
width: i,
|
|
15
|
+
height: a
|
|
16
|
+
},
|
|
17
|
+
positionX: n,
|
|
18
|
+
positionY: r,
|
|
19
|
+
positionWidth: i,
|
|
20
|
+
positionHeight: a,
|
|
21
|
+
createdAt: e,
|
|
22
|
+
updatedAt: e
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
//#endregion
|
|
26
|
+
export { t as toWidgetShim };
|
|
27
|
+
|
|
28
|
+
//# sourceMappingURL=widgetShim.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"widgetShim.js","names":[],"sources":["../../../../src/bigconsole/components/embed/widgetShim.ts"],"sourcesContent":["/**\n * Adapter from a sanitized embed widget DTO to the in-app `Widget` shape\n * (BOFF-7926).\n *\n * Reused in-app widget components take a full `Widget`. The embed only has the\n * sanitized model, so this shim fills the structural fields with inert values:\n * no data-source ids, no parser, no drilldown(s), no actions. Components\n * rendered through it must also never receive `onClick`/`onDrilldown`/`onSubmit`\n * — every reused component gates interactivity on those props or on\n * `drilldown(s)`, both of which stay absent here.\n */\n\nimport type { Widget } from '../../types';\nimport type { EmbedWidgetModel } from './types';\n\nconst EPOCH = '1970-01-01T00:00:00.000Z';\n\nexport function toWidgetShim(model: EmbedWidgetModel): Widget {\n const { x, y, w, h } = model.layout;\n return {\n id: model.id,\n pageId: '',\n type: model.type,\n title: model.title ?? '',\n config: { ...model.config },\n position: { x, y, width: w, height: h },\n positionX: x,\n positionY: y,\n positionWidth: w,\n positionHeight: h,\n createdAt: EPOCH,\n updatedAt: EPOCH,\n };\n}\n"],"mappings":";AAeA,IAAM,IAAQ;AAEd,SAAgB,EAAa,GAAiC;CAC5D,IAAM,EAAE,MAAG,MAAG,MAAG,SAAM,EAAM;AAC7B,QAAO;EACL,IAAI,EAAM;EACV,QAAQ;EACR,MAAM,EAAM;EACZ,OAAO,EAAM,SAAS;EACtB,QAAQ,EAAE,GAAG,EAAM,QAAQ;EAC3B,UAAU;GAAE;GAAG;GAAG,OAAO;GAAG,QAAQ;GAAG;EACvC,WAAW;EACX,WAAW;EACX,eAAe;EACf,gBAAgB;EAChB,WAAW;EACX,WAAW;EACZ"}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { asFiniteNumber as e, asOptionalString as t, asRecord as n, asRecordArray as r, displayValue as i, safeColor as a } from "./shared.js";
|
|
2
|
+
import { toWidgetShim as o } from "../widgetShim.js";
|
|
3
|
+
import { EmbedFrame as s } from "./EmbedFrame.js";
|
|
4
|
+
import c from "../../widgets/calendar/CalendarView.js";
|
|
5
|
+
import { fallbackTranslate as l } from "../../widgets/utils/translate.js";
|
|
6
|
+
import { useMemo as u } from "react";
|
|
7
|
+
import { jsx as d } from "react/jsx-runtime";
|
|
8
|
+
//#region src/bigconsole/components/embed/widgets/CalendarEmbed.tsx
|
|
9
|
+
var f = 1e3;
|
|
10
|
+
function p({ widget: p }) {
|
|
11
|
+
let m = u(() => o(p), [p]), h = u(() => {
|
|
12
|
+
let o = n(p.data), s = r(o.events, f).map((e, n) => ({
|
|
13
|
+
id: `e${n}`,
|
|
14
|
+
title: i(e.title),
|
|
15
|
+
start: i(e.start),
|
|
16
|
+
end: i(e.end) || i(e.start),
|
|
17
|
+
color: a(e.color),
|
|
18
|
+
allDay: e.allDay === !0,
|
|
19
|
+
category: t(e.category, 80)
|
|
20
|
+
})).filter((e) => e.start !== ""), c = e(o.eventCount);
|
|
21
|
+
return {
|
|
22
|
+
events: s,
|
|
23
|
+
eventCount: c !== null && c >= 0 ? Math.trunc(c) : void 0
|
|
24
|
+
};
|
|
25
|
+
}, [p.data]);
|
|
26
|
+
return /* @__PURE__ */ d(s, {
|
|
27
|
+
title: p.title,
|
|
28
|
+
children: /* @__PURE__ */ d(c, {
|
|
29
|
+
widget: m,
|
|
30
|
+
data: h,
|
|
31
|
+
t: l
|
|
32
|
+
})
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
//#endregion
|
|
36
|
+
export { p as CalendarEmbed };
|
|
37
|
+
|
|
38
|
+
//# sourceMappingURL=CalendarEmbed.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"CalendarEmbed.js","names":[],"sources":["../../../../../src/bigconsole/components/embed/widgets/CalendarEmbed.tsx"],"sourcesContent":["/**\n * Read-only calendar embed renderer (BOFF-7926).\n *\n * Reuses the provider-free CalendarView with a static translator. The service\n * projects `{ events: [{id, title, start, end, color?, allDay?, category?}], eventCount }`\n * (events capped; `eventCount` is the true total).\n */\nimport { useMemo } from 'react';\nimport { CalendarView, type CalendarData } from '../../widgets/calendar/CalendarView';\nimport { fallbackTranslate } from '../../widgets/utils/translate';\nimport type { EmbedWidgetRenderProps } from '../types';\nimport { toWidgetShim } from '../widgetShim';\nimport { EmbedFrame } from './EmbedFrame';\nimport { asFiniteNumber, asOptionalString, asRecord, asRecordArray, displayValue, safeColor } from './shared';\n\nconst MAX_EVENTS = 1000;\n\nexport function CalendarEmbed({ widget }: EmbedWidgetRenderProps) {\n const shim = useMemo(() => toWidgetShim(widget), [widget]);\n const data = useMemo((): CalendarData => {\n const d = asRecord(widget.data);\n const events = asRecordArray(d.events, MAX_EVENTS)\n .map((e, i) => ({\n id: `e${i}`,\n title: displayValue(e.title),\n start: displayValue(e.start),\n end: displayValue(e.end) || displayValue(e.start),\n color: safeColor(e.color),\n allDay: e.allDay === true,\n category: asOptionalString(e.category, 80),\n }))\n .filter((e) => e.start !== '');\n const count = asFiniteNumber(d.eventCount);\n return { events, eventCount: count !== null && count >= 0 ? Math.trunc(count) : undefined };\n }, [widget.data]);\n return (\n <EmbedFrame title={widget.title}>\n <CalendarView widget={shim} data={data} t={fallbackTranslate} />\n </EmbedFrame>\n );\n}\n"],"mappings":";;;;;;;;AAeA,IAAM,IAAa;AAEnB,SAAgB,EAAc,EAAE,aAAkC;CAChE,IAAM,IAAO,QAAc,EAAa,EAAO,EAAE,CAAC,EAAO,CAAC,EACpD,IAAO,QAA4B;EACvC,IAAM,IAAI,EAAS,EAAO,KAAK,EACzB,IAAS,EAAc,EAAE,QAAQ,EAAW,CAC/C,KAAK,GAAG,OAAO;GACd,IAAI,IAAI;GACR,OAAO,EAAa,EAAE,MAAM;GAC5B,OAAO,EAAa,EAAE,MAAM;GAC5B,KAAK,EAAa,EAAE,IAAI,IAAI,EAAa,EAAE,MAAM;GACjD,OAAO,EAAU,EAAE,MAAM;GACzB,QAAQ,EAAE,WAAW;GACrB,UAAU,EAAiB,EAAE,UAAU,GAAG;GAC3C,EAAE,CACF,QAAQ,MAAM,EAAE,UAAU,GAAG,EAC1B,IAAQ,EAAe,EAAE,WAAW;AAC1C,SAAO;GAAE;GAAQ,YAAY,MAAU,QAAQ,KAAS,IAAI,KAAK,MAAM,EAAM,GAAG,KAAA;GAAW;IAC1F,CAAC,EAAO,KAAK,CAAC;AACjB,QACE,kBAAC,GAAD;EAAY,OAAO,EAAO;YACxB,kBAAC,GAAD;GAAc,QAAQ;GAAY;GAAM,GAAG;GAAqB,CAAA;EACrD,CAAA"}
|