diff --git a/packages/web-app/src/embedded.js b/packages/web-app/src/embedded.js index a8172aa..1adf146 100644 --- a/packages/web-app/src/embedded.js +++ b/packages/web-app/src/embedded.js @@ -1,177 +1,23 @@ -// embedded.js — Datalex-Cloud embed shim. Brought to parity with the DQL -// notebook embed (apps/dql-notebook/src/embedded.ts) so the cloud shell can -// drive DataLex the same way over postMessage. +// embedded.js — minimal embed marker for the DataLex web-app. // -// Activated when the URL contains ?embedded=1. Responsibilities: +// All cloud<->OSS integration (theme via [data-theme]/`dm_theme`, fit, +// x-oss-app, auth bearer, embed context, chrome-hiding, route sync, ready +// handshake) is driven from OUTSIDE the OSS app by the cloud's build-injected +// adapter (governed-analytics-cloud: scripts/embed-adapter.js). This module only +// exposes the embed flag + a reader for the cloud context the adapter publishes. +// Standalone DataLex is unaffected (no-op without ?embedded=1). // -// 1. Storage isolation — namespace localStorage by project id. -// 2. Theme bridge — apply `datalex.theme` tokens to Luna CSS variables. -// 3. Context injection — accept `datalex.cloud.context` { config } and expose -// it on window.__DATALEX_CLOUD_EMBED__ (tenant / project / role / -// capabilities / repo_context / warehouse_context). -// 4. Capability-driven chrome hiding — when capabilities.hide_activity_bar / -// hide_sidebar are set, the cloud rail is the only nav, so we hide -// DataLex's own topbar / activity rail / layer spine via injected CSS. -// 5. Auth pass-through — add `Authorization: Bearer ` to /api/* and -// /projects/* calls. Token arrives via `datalex.auth.token` or #token=. -// 6. Route reporting — post `datalex.route.changed` on hash change so the -// cloud can keep its outer URL in sync for deep links. -// -// Self-installs only when the embed flag is present; standalone DataLex is -// unaffected. +// NOTE: localStorage is intentionally NOT namespaced here. The cloud owns the +// embedded app's persisted UI state — including the theme, which the uiStore +// reads from the `dm_theme` key. Namespacing hid the adapter's pre-boot writes +// behind a project-scoped key the store never reads, so the theme never applied. const params = new URLSearchParams(window.location.search); const isEmbedded = params.get("embedded") === "1"; -const projectId = params.get("project") || "shared"; -/** Read the injected cloud embed config (set via postMessage or boot global). */ +/** Read the cloud embed config the adapter publishes on the window global. */ export function getCloudEmbedConfig() { return (typeof window !== "undefined" && window.__DATALEX_CLOUD_EMBED__) || null; } -// Token via hash so it never enters server logs / browser history. -function readAndStripToken() { - const hash = window.location.hash; - if (!hash) return null; - const m = hash.match(/(?:^|[#&])token=([^&]+)/); - if (!m) return null; - const token = decodeURIComponent(m[1]); - const next = hash.replace(/(?:^|[#&])token=[^&]+/, "").replace(/^[#&]/, "").trim(); - history.replaceState(null, "", `${window.location.pathname}${window.location.search}${next ? `#${next}` : ""}`); - return token; -} - -// Inject (once) the CSS that hides DataLex's own chrome when the cloud shell -// already provides the rail/topbar. Keyed off a data attribute so it -// only applies in capability-restricted embeds. -function installChromeHidingStyles() { - if (document.getElementById("datalex-embed-chrome-css")) return; - const style = document.createElement("style"); - style.id = "datalex-embed-chrome-css"; - style.textContent = ` - html[data-datalex-embed="minimal"] .topbar, - html[data-datalex-embed="minimal"] .project-tabs, - html[data-datalex-embed="minimal"] .activity-rail { display: none !important; } - html[data-datalex-embed="no-sidebar"] .activity-rail, - html[data-datalex-embed="no-sidebar"] .layer-spine { display: none !important; } - `; - document.head.appendChild(style); -} - -function applyCapabilities(config) { - const caps = (config && config.capabilities) || {}; - const root = document.documentElement; - installChromeHidingStyles(); - if (caps.hide_activity_bar && caps.hide_sidebar) { - root.dataset.datalexEmbed = "minimal"; - } else if (caps.hide_sidebar) { - root.dataset.datalexEmbed = "no-sidebar"; - } - root.dataset.datalexCloudKind = config && config.kind ? String(config.kind) : "datalex"; - root.dataset.datalexCloudSurface = config && config.surface ? String(config.surface) : ""; -} - -function applyTheme(tokens) { - const root = document.documentElement; - const map = { - brand: "--lux-color-accent", - ink900: "--lux-color-text", - bg: "--lux-color-bg", - surface: "--lux-color-surface", - border: "--lux-color-border", - }; - for (const [k, cssVar] of Object.entries(map)) { - if (tokens && tokens[k]) root.style.setProperty(cssVar, tokens[k]); - } -} - -if (isEmbedded) { - // 1. localStorage namespace. - const namespacedKey = (key) => `dlx:${projectId}:${key}`; - const realStorage = window.localStorage; - const storageProxy = { - getItem: (k) => realStorage.getItem(namespacedKey(k)), - setItem: (k, v) => realStorage.setItem(namespacedKey(k), v), - removeItem: (k) => realStorage.removeItem(namespacedKey(k)), - clear: () => { - const prefix = `dlx:${projectId}:`; - for (let i = realStorage.length - 1; i >= 0; i--) { - const k = realStorage.key(i); - if (k && k.startsWith(prefix)) realStorage.removeItem(k); - } - }, - key: (i) => { - const prefix = `dlx:${projectId}:`; - const matched = []; - for (let j = 0; j < realStorage.length; j++) { - const k = realStorage.key(j); - if (k && k.startsWith(prefix)) matched.push(k.slice(prefix.length)); - } - return matched[i] ?? null; - }, - get length() { - const prefix = `dlx:${projectId}:`; - let n = 0; - for (let i = 0; i < realStorage.length; i++) { - const k = realStorage.key(i); - if (k && k.startsWith(prefix)) n++; - } - return n; - }, - }; - Object.defineProperty(window, "localStorage", { value: storageProxy, configurable: true }); - - // 5. Auth pass-through. - let bearerToken = readAndStripToken(); - const realFetch = window.fetch.bind(window); - window.fetch = async (input, init) => { - const url = typeof input === "string" ? input : input instanceof URL ? input.toString() : input.url; - const isApi = - url.startsWith("/api/") || url.startsWith(`${window.location.origin}/api/`) || - url.startsWith("/projects/") || url.startsWith(`${window.location.origin}/projects/`); - if (!isApi) return realFetch(input, init); - const headers = new Headers((init && init.headers) || {}); - // Identify this app to the cloud gateway so it routes to the DataLex backend. - headers.set("x-oss-app", "datalex"); - if (bearerToken && !headers.has("authorization")) headers.set("authorization", `Bearer ${bearerToken}`); - return realFetch(input, { ...init, headers }); - }; - - // 2/3. Message bridge — theme, context, auth token. - window.addEventListener("message", (ev) => { - const data = ev.data || {}; - if (data.type === "datalex.theme") { - applyTheme(data.tokens || {}); - return; - } - if ((data.type === "datalex.cloud.context" || data.type === "dql.cloud.context") && data.config) { - window.__DATALEX_CLOUD_EMBED__ = data.config; - applyCapabilities(data.config); - window.dispatchEvent(new CustomEvent("datalex:cloud-context", { detail: data.config })); - return; - } - if (data.type === "datalex.auth.token" && typeof data.token === "string") { - bearerToken = data.token; - } - }); - - // Boot-time global (if the host injected it before scripts ran). - if (window.__DATALEX_CLOUD_EMBED__) applyCapabilities(window.__DATALEX_CLOUD_EMBED__); - - // 6. Route reporting — keep the parent's outer hash in sync for deep links. - let lastHash = window.location.hash; - window.addEventListener("hashchange", () => { - if (window.location.hash === lastHash) return; - lastHash = window.location.hash; - if (window.parent !== window) { - window.parent.postMessage({ type: "datalex.route.changed", path: lastHash, projectId }, "*"); - } - }); - - // Tell the parent we're ready — it responds with context + theme + token. - if (window.parent !== window) { - window.parent.postMessage({ type: "datalex.embedded.ready", projectId }, "*"); - } -} - export { isEmbedded }; diff --git a/packages/web-app/src/styles/datalex-design.css b/packages/web-app/src/styles/datalex-design.css index b1898ec..db46574 100644 --- a/packages/web-app/src/styles/datalex-design.css +++ b/packages/web-app/src/styles/datalex-design.css @@ -291,6 +291,72 @@ --scrim: rgba(40,35,25,0.35); } +/* ================================================================ + WHITE — crisp light (shared contract; see @duckcodeai/design-tokens). + Added so the cloud's "white" global theme maps 1:1 onto DataLex. + Core vars (bg/text/accent/border) use the canonical white values; + DataLex-specific extras are tuned to match. + ================================================================ */ +[data-theme="white"] { + color-scheme: light; + --bg-0: #f5f6f8; + --bg-1: #ffffff; + --bg-2: #ffffff; + --bg-3: #eef0f3; + --bg-4: #e1e4e9; + --bg-canvas: #ffffff; + --bg-grid: rgba(15, 23, 42, 0.04); + + --border-subtle: rgba(15,23,42,0.08); + --border-default: rgba(15,23,42,0.14); + --border-strong: rgba(15,23,42,0.22); + --border-focus: #2563eb; + + --text-primary: #0f172a; + --text-secondary: #475569; + --text-tertiary: #64748b; + --text-muted: #94a3b8; + + --accent: #2563eb; + --accent-hover: #1d4ed8; + --accent-dim: rgba(37,99,235,0.10); + --accent-fg: #ffffff; + --accent-on: #ffffff; + + --bg-hover: rgba(15, 23, 42, 0.045); + + --status-error: #dc2626; --status-error-bg: rgba(220,38,38,0.08); --status-error-border: rgba(220,38,38,0.30); + --status-warning: #ca8a04; --status-warning-bg: rgba(202,138,4,0.10); --status-warning-border: rgba(202,138,4,0.32); + --status-success: #16a34a; --status-success-bg: rgba(22,163,74,0.08); --status-success-border: rgba(22,163,74,0.30); + --status-info: var(--accent); --status-info-bg: var(--accent-dim); --status-info-border: rgba(37,99,235,0.30); + + --pk: #ca8a04; + --fk: #7c3aed; + --idx: #0891b2; + --nn: #64748b; + --nullable: #7c3aed; + + --cat-users: #7c3aed; --cat-users-soft: rgba(124,58,237,0.09); + --cat-billing: #16a34a; --cat-billing-soft: rgba(22,163,74,0.09); + --cat-product: #ea580c; --cat-product-soft: rgba(234,88,12,0.09); + --cat-system: #64748b; --cat-system-soft: rgba(100,116,139,0.10); + --cat-access: #db2777; --cat-access-soft: rgba(219,39,119,0.09); + --cat-audit: #0891b2; --cat-audit-soft: rgba(8,145,178,0.09); + + --rel-line: #cbd5e1; + --rel-line-strong: #94a3b8; + --rel-line-active: #2563eb; + --rel-line-dim: rgba(203, 213, 225, 0.45); + + --sql-keyword: #1d4ed8; --sql-type: #0891b2; --sql-string: #15803d; + --sql-number: #c2410c; --sql-function: #7c3aed; --sql-punctuation: #475569; + --sql-ident: var(--text-primary); --sql-comment: var(--text-tertiary); + + --shadow-card: 0 1px 2px rgba(15,23,42,0.04), 0 8px 24px rgba(15,23,42,0.06); + --shadow-pop: 0 20px 50px rgba(15,23,42,0.14); + --scrim: rgba(15,23,42,0.30); +} + /* ================================================================ ARCTIC — cool light (IBM Carbon / crisp blue-white enterprise) ================================================================ */