From a40498777c67163f90c90b14b0aaef8bde32d752 Mon Sep 17 00:00:00 2001 From: KKranthi6881 Date: Fri, 26 Jun 2026 12:27:18 -0500 Subject: [PATCH 1/5] fix(embed): switch DataLex theme MODE to match the cloud (paper/white) On datalex.theme with a mode, fire the Shell's datalex:theme-change hook so the embedded DataLex flips light/dark with the cloud, not just a few color tokens. Co-Authored-By: Claude Opus 4.8 --- packages/web-app/src/embedded.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/packages/web-app/src/embedded.js b/packages/web-app/src/embedded.js index a8172aa..b211313 100644 --- a/packages/web-app/src/embedded.js +++ b/packages/web-app/src/embedded.js @@ -142,6 +142,11 @@ if (isEmbedded) { const data = ev.data || {}; if (data.type === "datalex.theme") { applyTheme(data.tokens || {}); + // Switch DataLex's own theme MODE to match the cloud (paper/white). The + // Shell listens for this event + persists it; this is what flips light/dark. + if (data.mode) { + window.dispatchEvent(new CustomEvent("datalex:theme-change", { detail: { theme: data.mode } })); + } return; } if ((data.type === "datalex.cloud.context" || data.type === "dql.cloud.context") && data.config) { From 4d96c5d41fdeec159b2498c9dcd340bbadb9dbfa Mon Sep 17 00:00:00 2001 From: KKranthi6881 Date: Fri, 26 Jun 2026 13:32:28 -0500 Subject: [PATCH 2/5] fix(embed): persist cloud theme mode to THEME_STORAGE (beats mount race) Set localStorage['datalex.theme'] so the Shell's initial state adopts the cloud theme even when the message arrives before the live listener mounts. Co-Authored-By: Claude Opus 4.8 --- packages/web-app/src/embedded.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/packages/web-app/src/embedded.js b/packages/web-app/src/embedded.js index b211313..26280cf 100644 --- a/packages/web-app/src/embedded.js +++ b/packages/web-app/src/embedded.js @@ -142,9 +142,11 @@ if (isEmbedded) { const data = ev.data || {}; if (data.type === "datalex.theme") { applyTheme(data.tokens || {}); - // Switch DataLex's own theme MODE to match the cloud (paper/white). The - // Shell listens for this event + persists it; this is what flips light/dark. + // Switch DataLex's own theme MODE to match the cloud (paper/white). + // setItem(THEME_STORAGE) so the Shell's initial state reads it if the + // message beats mount; the event covers live toggles after mount. if (data.mode) { + try { localStorage.setItem("datalex.theme", data.mode); } catch { /* ignore */ } window.dispatchEvent(new CustomEvent("datalex:theme-change", { detail: { theme: data.mode } })); } return; From 5688883fae3bdd0f626da97295d2dc5d5ebf9de3 Mon Sep 17 00:00:00 2001 From: KKranthi6881 Date: Fri, 26 Jun 2026 14:03:49 -0500 Subject: [PATCH 3/5] refactor(embed): reduce embed shim to localStorage namespacing only Theme+mode, x-oss-app + bearer fetch wrap, capability-driven chrome-hiding, cloud-context injection, route reporting and the ready handshake are now driven from OUTSIDE the OSS app by the cloud's build-injected embed adapter (governed-analytics-cloud: scripts/embed-adapter.js) + its override CSS. This shim keeps only per-project localStorage isolation. Standalone DataLex is unaffected (no-op without ?embedded=1). Builds clean (vite). Co-Authored-By: Claude Opus 4.8 --- packages/web-app/src/embedded.js | 150 +++---------------------------- 1 file changed, 14 insertions(+), 136 deletions(-) diff --git a/packages/web-app/src/embedded.js b/packages/web-app/src/embedded.js index 26280cf..aae8432 100644 --- a/packages/web-app/src/embedded.js +++ b/packages/web-app/src/embedded.js @@ -1,92 +1,29 @@ -// 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 shim for the DataLex web-app. // -// Activated when the URL contains ?embedded=1. Responsibilities: +// Activated when the URL contains ?embedded=1. Its ONLY job is localStorage +// isolation: namespace keys by the project id from `?project=` so two +// tenants/projects embedding into the same cloud origin can't clobber each +// other's panel layout / theme prefs / last-opened state. // -// 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. +// Everything else that used to live here — theme bridge + mode switch, auth +// `x-oss-app` + bearer fetch wrap, capability-driven chrome hiding, +// cloud-context injection, route reporting, the ready handshake — is now driven +// from OUTSIDE the OSS app by the cloud's build-injected adapter +// (governed-analytics-cloud: scripts/embed-adapter.js) plus its cloud-owned +// override CSS. Keeping that integration cloud-side means cloud tweaks no longer +// touch this file and OSS releases flow into the cloud unchanged. 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 exposes 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. + // localStorage namespace — isolate per project within a shared cloud origin. const namespacedKey = (key) => `dlx:${projectId}:${key}`; const realStorage = window.localStorage; const storageProxy = { @@ -120,65 +57,6 @@ if (isEmbedded) { }, }; 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 || {}); - // Switch DataLex's own theme MODE to match the cloud (paper/white). - // setItem(THEME_STORAGE) so the Shell's initial state reads it if the - // message beats mount; the event covers live toggles after mount. - if (data.mode) { - try { localStorage.setItem("datalex.theme", data.mode); } catch { /* ignore */ } - window.dispatchEvent(new CustomEvent("datalex:theme-change", { detail: { theme: data.mode } })); - } - 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 }; From 0b8ad95e105a0420c9b9d1c0d774e576647e648a Mon Sep 17 00:00:00 2001 From: KKranthi6881 Date: Fri, 26 Jun 2026 15:13:00 -0500 Subject: [PATCH 4/5] fix(embed): stop namespacing localStorage in the embed shim Namespacing hid the cloud adapter's pre-boot dm_theme write behind a project-scoped key the uiStore never reads, so the embedded DataLex theme never applied (stuck dark). The cloud owns embedded UI state; the shim is now just the embed flag + cloud-context reader. Builds clean (vite). Co-Authored-By: Claude Opus 4.8 --- packages/web-app/src/embedded.js | 63 ++++++-------------------------- 1 file changed, 12 insertions(+), 51 deletions(-) diff --git a/packages/web-app/src/embedded.js b/packages/web-app/src/embedded.js index aae8432..1adf146 100644 --- a/packages/web-app/src/embedded.js +++ b/packages/web-app/src/embedded.js @@ -1,62 +1,23 @@ -// embedded.js — minimal embed shim for the DataLex web-app. +// embedded.js — minimal embed marker for the DataLex web-app. // -// Activated when the URL contains ?embedded=1. Its ONLY job is localStorage -// isolation: namespace keys by the project id from `?project=` so two -// tenants/projects embedding into the same cloud origin can't clobber each -// other's panel layout / theme prefs / last-opened state. +// 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). // -// Everything else that used to live here — theme bridge + mode switch, auth -// `x-oss-app` + bearer fetch wrap, capability-driven chrome hiding, -// cloud-context injection, route reporting, the ready handshake — is now driven -// from OUTSIDE the OSS app by the cloud's build-injected adapter -// (governed-analytics-cloud: scripts/embed-adapter.js) plus its cloud-owned -// override CSS. Keeping that integration cloud-side means cloud tweaks no longer -// touch this file and OSS releases flow into the cloud unchanged. +// 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 cloud embed config the adapter exposes on the window 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; } -if (isEmbedded) { - // localStorage namespace — isolate per project within a shared cloud origin. - 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 }); -} - export { isEmbedded }; From a0b4bc55a2e677349b8e90fd2720475532effc19 Mon Sep 17 00:00:00 2001 From: KKranthi6881 Date: Fri, 26 Jun 2026 16:09:05 -0500 Subject: [PATCH 5/5] feat(theme): add white theme to match the shared design-token contract Adds a [data-theme="white"] block (crisp light) mirroring the paper block, using the canonical white values from the shared contract (@duckcodeai/design-tokens). This lets the cloud global "white" theme map 1:1 onto DataLex instead of falling back to arctic. Core vars (bg/text/accent/border) use the canonical white; DataLex-specific extras (pk/fk/idx, cat-*, sql-*) are tuned to match. Builds clean (vite). Co-Authored-By: Claude Opus 4.8 --- .../web-app/src/styles/datalex-design.css | 66 +++++++++++++++++++ 1 file changed, 66 insertions(+) diff --git a/packages/web-app/src/styles/datalex-design.css b/packages/web-app/src/styles/datalex-design.css index 0da5647..883f65b 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) ================================================================ */