@lightdash/query-sdk 1.74.1 → 1.75.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/client.js +5 -1
- package/dist/colorScheme.d.ts +100 -0
- package/dist/colorScheme.js +180 -0
- package/dist/features.js +6 -0
- package/dist/generated/sdkVersion.d.ts +1 -1
- package/dist/generated/sdkVersion.js +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/package.json +2 -2
package/dist/client.js
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
* const lightdash = createClient() // auto-detects from environment
|
|
6
6
|
*/
|
|
7
7
|
import { createApiTransport } from './apiTransport';
|
|
8
|
+
import { applyColorSchemeSeed, mountColorScheme } from './colorScheme';
|
|
8
9
|
import { mountInspector } from './inspector';
|
|
9
10
|
import { mountLineage } from './lineage';
|
|
10
11
|
import { createPostMessageTransport } from './postMessageTransport';
|
|
@@ -80,10 +81,13 @@ export function createClient() {
|
|
|
80
81
|
const projectUuid = params.get('projectUuid') ?? '';
|
|
81
82
|
mountInspector(window.parent);
|
|
82
83
|
mountLineage(window.parent);
|
|
84
|
+
mountColorScheme(window.parent);
|
|
83
85
|
return new LightdashClient({ apiKey: '', baseUrl: '', projectUuid }, createPostMessageTransport({ targetWindow: window.parent, projectUuid }));
|
|
84
86
|
}
|
|
85
87
|
}
|
|
86
|
-
// 2. Env vars → API transport
|
|
88
|
+
// 2. Env vars → API transport. No host to follow, but honour a `?theme=`
|
|
89
|
+
// seed so an author running the app top-level can see both modes.
|
|
90
|
+
applyColorSchemeSeed();
|
|
87
91
|
const config = configFromEnv();
|
|
88
92
|
if (!config) {
|
|
89
93
|
throw new Error('Missing Lightdash client config. ' +
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Host color scheme — the iframe-side half of "data apps follow the host's
|
|
3
|
+
* light/dark mode". The Lightdash host owns the resolved scheme (its own theme
|
|
4
|
+
* toggle, or an embed's `?theme=`) and it reaches the app two ways, each doing
|
|
5
|
+
* a job the other can't:
|
|
6
|
+
*
|
|
7
|
+
* - a `theme=` seed in the iframe URL hash, read **synchronously** by
|
|
8
|
+
* `mountColorScheme()` from `createClient()`. That runs before React
|
|
9
|
+
* renders, so the app's first rendered frame is already in the right
|
|
10
|
+
* scheme. Only the seed can do this: a message reply lands a tick later,
|
|
11
|
+
* by which point React may already have committed the wrong mode.
|
|
12
|
+
* - `lightdash:sdk:theme` messages, which keep the app in step with every
|
|
13
|
+
* later host toggle without reloading the iframe.
|
|
14
|
+
*
|
|
15
|
+
* Delivery is a handshake, like `vizContext`: `mountColorScheme()` posts
|
|
16
|
+
* `lightdash:sdk:theme-request` once its listener is live, and re-posts it
|
|
17
|
+
* whenever the host announces `lightdash:sdk:ready` (the same belt-and-braces
|
|
18
|
+
* `manifest` uses). Whichever side mounts first, the app ends up on the host's
|
|
19
|
+
* scheme. Without it the app would depend on `createClient()` having run before
|
|
20
|
+
* the host's iframe `load` push, which the template happens to guarantee but
|
|
21
|
+
* app-owned `main.jsx` can silently break.
|
|
22
|
+
*
|
|
23
|
+
* Applying means toggling `.dark` on `<html>` (Tailwind's `darkMode: ['class']`
|
|
24
|
+
* hook, and the only scope Radix portals inherit) plus the CSS `color-scheme`
|
|
25
|
+
* property, so form controls and scrollbars follow too.
|
|
26
|
+
*
|
|
27
|
+
* Seed and message both cross a trust boundary — the hash is user-editable and
|
|
28
|
+
* the message is postMessage — so both are validated, and messages are only
|
|
29
|
+
* accepted from the window the client was created against.
|
|
30
|
+
*/
|
|
31
|
+
export type HostColorScheme = 'light' | 'dark';
|
|
32
|
+
/** Host → iframe, in reply to a request and on every host theme change. */
|
|
33
|
+
export type HostColorSchemeMessage = {
|
|
34
|
+
type: 'lightdash:sdk:theme';
|
|
35
|
+
colorScheme: HostColorScheme;
|
|
36
|
+
};
|
|
37
|
+
/** Iframe → host, once this module's listener is live. */
|
|
38
|
+
export type HostColorSchemeRequestMessage = {
|
|
39
|
+
type: 'lightdash:sdk:theme-request';
|
|
40
|
+
};
|
|
41
|
+
export declare const HOST_COLOR_SCHEME_MESSAGE = "lightdash:sdk:theme";
|
|
42
|
+
export declare const HOST_COLOR_SCHEME_REQUEST_MESSAGE = "lightdash:sdk:theme-request";
|
|
43
|
+
/**
|
|
44
|
+
* Read the scheme seed. The iframe hash is where the host forwards it; the
|
|
45
|
+
* search param is the top-level (local dev) fallback. Null when absent or not
|
|
46
|
+
* one of the two valid values.
|
|
47
|
+
*/
|
|
48
|
+
export declare function parseColorSchemeSeed(location: {
|
|
49
|
+
hash: string;
|
|
50
|
+
search: string;
|
|
51
|
+
}): HostColorScheme | null;
|
|
52
|
+
/** Stamp the scheme onto `<html>`. Idempotent. */
|
|
53
|
+
export declare function applyColorScheme(scheme: HostColorScheme): void;
|
|
54
|
+
export type ColorSchemeStore = {
|
|
55
|
+
getScheme: () => HostColorScheme;
|
|
56
|
+
/**
|
|
57
|
+
* Stamp `<html>` and publish. Always re-applies (the DOM may have been
|
|
58
|
+
* clobbered since), but only notifies when the value actually changed.
|
|
59
|
+
*/
|
|
60
|
+
setScheme: (scheme: HostColorScheme) => void;
|
|
61
|
+
subscribe: (listener: () => void) => () => void;
|
|
62
|
+
};
|
|
63
|
+
/**
|
|
64
|
+
* Minimal external store: synchronous reads for useSyncExternalStore. The seed
|
|
65
|
+
* is resolved by the caller and passed in, so the value never depends on when
|
|
66
|
+
* it was first read. Exported for tests — app code uses `useColorScheme`.
|
|
67
|
+
*/
|
|
68
|
+
export declare function createColorSchemeStore(options: {
|
|
69
|
+
seed: HostColorScheme;
|
|
70
|
+
apply?: (scheme: HostColorScheme) => void;
|
|
71
|
+
}): ColorSchemeStore;
|
|
72
|
+
/**
|
|
73
|
+
* Apply the URL seed and nothing else. Used by `createClient()` on the API
|
|
74
|
+
* transport (top-level local dev), where there is no host to talk to but
|
|
75
|
+
* `?theme=dark` should still render the app dark — otherwise an author can
|
|
76
|
+
* never see the dark half of the contract `skill.md` asks them to satisfy.
|
|
77
|
+
*/
|
|
78
|
+
export declare function applyColorSchemeSeed(): void;
|
|
79
|
+
/**
|
|
80
|
+
* Follow the host's scheme: apply the seed, listen for `lightdash:sdk:theme`
|
|
81
|
+
* from `targetWindow`, and ask the host to send the current value now. Called
|
|
82
|
+
* by `createClient()` when the postMessage transport is detected. One listener
|
|
83
|
+
* per bundle — re-mounting replaces the previous listener rather than stacking.
|
|
84
|
+
*/
|
|
85
|
+
export declare function mountColorScheme(targetWindow: Window): () => void;
|
|
86
|
+
/**
|
|
87
|
+
* The scheme the app is currently rendering in. CSS should follow the `.dark`
|
|
88
|
+
* class through the theme tokens; reach for this hook only where a value can't
|
|
89
|
+
* be expressed in CSS — a charting library's theme object, an image swap.
|
|
90
|
+
*
|
|
91
|
+
* const colorScheme = useColorScheme();
|
|
92
|
+
* <ResponsiveContainer theme={colorScheme === 'dark' ? darkTheme : lightTheme} />
|
|
93
|
+
*/
|
|
94
|
+
export declare function useColorScheme(): HostColorScheme;
|
|
95
|
+
/**
|
|
96
|
+
* Test-only seam: drops the listener and the shared store so the next mount
|
|
97
|
+
* re-reads the seed. The store itself is covered directly through
|
|
98
|
+
* `createColorSchemeStore`; this exists so the mount tests can vary the URL.
|
|
99
|
+
*/
|
|
100
|
+
export declare function resetColorSchemeState(): void;
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Host color scheme — the iframe-side half of "data apps follow the host's
|
|
3
|
+
* light/dark mode". The Lightdash host owns the resolved scheme (its own theme
|
|
4
|
+
* toggle, or an embed's `?theme=`) and it reaches the app two ways, each doing
|
|
5
|
+
* a job the other can't:
|
|
6
|
+
*
|
|
7
|
+
* - a `theme=` seed in the iframe URL hash, read **synchronously** by
|
|
8
|
+
* `mountColorScheme()` from `createClient()`. That runs before React
|
|
9
|
+
* renders, so the app's first rendered frame is already in the right
|
|
10
|
+
* scheme. Only the seed can do this: a message reply lands a tick later,
|
|
11
|
+
* by which point React may already have committed the wrong mode.
|
|
12
|
+
* - `lightdash:sdk:theme` messages, which keep the app in step with every
|
|
13
|
+
* later host toggle without reloading the iframe.
|
|
14
|
+
*
|
|
15
|
+
* Delivery is a handshake, like `vizContext`: `mountColorScheme()` posts
|
|
16
|
+
* `lightdash:sdk:theme-request` once its listener is live, and re-posts it
|
|
17
|
+
* whenever the host announces `lightdash:sdk:ready` (the same belt-and-braces
|
|
18
|
+
* `manifest` uses). Whichever side mounts first, the app ends up on the host's
|
|
19
|
+
* scheme. Without it the app would depend on `createClient()` having run before
|
|
20
|
+
* the host's iframe `load` push, which the template happens to guarantee but
|
|
21
|
+
* app-owned `main.jsx` can silently break.
|
|
22
|
+
*
|
|
23
|
+
* Applying means toggling `.dark` on `<html>` (Tailwind's `darkMode: ['class']`
|
|
24
|
+
* hook, and the only scope Radix portals inherit) plus the CSS `color-scheme`
|
|
25
|
+
* property, so form controls and scrollbars follow too.
|
|
26
|
+
*
|
|
27
|
+
* Seed and message both cross a trust boundary — the hash is user-editable and
|
|
28
|
+
* the message is postMessage — so both are validated, and messages are only
|
|
29
|
+
* accepted from the window the client was created against.
|
|
30
|
+
*/
|
|
31
|
+
import { useSyncExternalStore } from 'react';
|
|
32
|
+
export const HOST_COLOR_SCHEME_MESSAGE = 'lightdash:sdk:theme';
|
|
33
|
+
export const HOST_COLOR_SCHEME_REQUEST_MESSAGE = 'lightdash:sdk:theme-request';
|
|
34
|
+
const SDK_READY_MESSAGE = 'lightdash:sdk:ready';
|
|
35
|
+
// Mirrored by the host in packages/frontend/src/features/apps/utils/appIframeUrl.ts
|
|
36
|
+
const COLOR_SCHEME_PARAM = 'theme';
|
|
37
|
+
const DARK_CLASS = 'dark';
|
|
38
|
+
const isHostColorScheme = (value) => value === 'light' || value === 'dark';
|
|
39
|
+
/**
|
|
40
|
+
* Read the scheme seed. The iframe hash is where the host forwards it; the
|
|
41
|
+
* search param is the top-level (local dev) fallback. Null when absent or not
|
|
42
|
+
* one of the two valid values.
|
|
43
|
+
*/
|
|
44
|
+
export function parseColorSchemeSeed(location) {
|
|
45
|
+
const raw = new URLSearchParams(location.hash.replace(/^#/, '')).get(COLOR_SCHEME_PARAM) ?? new URLSearchParams(location.search).get(COLOR_SCHEME_PARAM);
|
|
46
|
+
return isHostColorScheme(raw) ? raw : null;
|
|
47
|
+
}
|
|
48
|
+
/** Stamp the scheme onto `<html>`. Idempotent. */
|
|
49
|
+
export function applyColorScheme(scheme) {
|
|
50
|
+
if (typeof document === 'undefined')
|
|
51
|
+
return;
|
|
52
|
+
const root = document.documentElement;
|
|
53
|
+
root.classList.toggle(DARK_CLASS, scheme === 'dark');
|
|
54
|
+
root.style.colorScheme = scheme;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Minimal external store: synchronous reads for useSyncExternalStore. The seed
|
|
58
|
+
* is resolved by the caller and passed in, so the value never depends on when
|
|
59
|
+
* it was first read. Exported for tests — app code uses `useColorScheme`.
|
|
60
|
+
*/
|
|
61
|
+
export function createColorSchemeStore(options) {
|
|
62
|
+
const apply = options.apply ?? applyColorScheme;
|
|
63
|
+
let scheme = options.seed;
|
|
64
|
+
const listeners = new Set();
|
|
65
|
+
return {
|
|
66
|
+
getScheme: () => scheme,
|
|
67
|
+
setScheme: (next) => {
|
|
68
|
+
const changed = next !== scheme;
|
|
69
|
+
scheme = next;
|
|
70
|
+
apply(next);
|
|
71
|
+
if (changed)
|
|
72
|
+
listeners.forEach((listener) => listener());
|
|
73
|
+
},
|
|
74
|
+
subscribe: (listener) => {
|
|
75
|
+
listeners.add(listener);
|
|
76
|
+
return () => {
|
|
77
|
+
listeners.delete(listener);
|
|
78
|
+
};
|
|
79
|
+
},
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Initial value: the URL seed, else whatever is already on `<html>` — an app
|
|
84
|
+
* that pins its own scheme in `:root`/`index.html` keeps reporting that one
|
|
85
|
+
* until the host says otherwise.
|
|
86
|
+
*/
|
|
87
|
+
function readSeed() {
|
|
88
|
+
if (typeof window === 'undefined')
|
|
89
|
+
return 'light';
|
|
90
|
+
const seed = parseColorSchemeSeed(window.location);
|
|
91
|
+
if (seed)
|
|
92
|
+
return seed;
|
|
93
|
+
return document.documentElement.classList.contains(DARK_CLASS)
|
|
94
|
+
? 'dark'
|
|
95
|
+
: 'light';
|
|
96
|
+
}
|
|
97
|
+
// Lazily created so tests (and SSR) never touch window at import time.
|
|
98
|
+
let sharedStore = null;
|
|
99
|
+
function getSharedStore() {
|
|
100
|
+
if (sharedStore === null) {
|
|
101
|
+
sharedStore = createColorSchemeStore({ seed: readSeed() });
|
|
102
|
+
}
|
|
103
|
+
return sharedStore;
|
|
104
|
+
}
|
|
105
|
+
let activeCleanup = null;
|
|
106
|
+
/**
|
|
107
|
+
* Apply the URL seed and nothing else. Used by `createClient()` on the API
|
|
108
|
+
* transport (top-level local dev), where there is no host to talk to but
|
|
109
|
+
* `?theme=dark` should still render the app dark — otherwise an author can
|
|
110
|
+
* never see the dark half of the contract `skill.md` asks them to satisfy.
|
|
111
|
+
*/
|
|
112
|
+
export function applyColorSchemeSeed() {
|
|
113
|
+
applyColorScheme(getSharedStore().getScheme());
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* Follow the host's scheme: apply the seed, listen for `lightdash:sdk:theme`
|
|
117
|
+
* from `targetWindow`, and ask the host to send the current value now. Called
|
|
118
|
+
* by `createClient()` when the postMessage transport is detected. One listener
|
|
119
|
+
* per bundle — re-mounting replaces the previous listener rather than stacking.
|
|
120
|
+
*/
|
|
121
|
+
export function mountColorScheme(targetWindow) {
|
|
122
|
+
if (typeof window === 'undefined')
|
|
123
|
+
return () => { };
|
|
124
|
+
const store = getSharedStore();
|
|
125
|
+
applyColorScheme(store.getScheme());
|
|
126
|
+
const request = {
|
|
127
|
+
type: HOST_COLOR_SCHEME_REQUEST_MESSAGE,
|
|
128
|
+
};
|
|
129
|
+
// Wildcard for the same reason as every other outbound bridge message: the
|
|
130
|
+
// sandboxed iframe has an opaque origin and can't derive the parent's.
|
|
131
|
+
const post = () => targetWindow.postMessage(request, '*');
|
|
132
|
+
const handler = (event) => {
|
|
133
|
+
if (event.source !== targetWindow)
|
|
134
|
+
return;
|
|
135
|
+
const data = event.data;
|
|
136
|
+
// Re-ask on `sdk:ready`: if the host mounted its bridge after our first
|
|
137
|
+
// request, that request went nowhere and this is the recovery.
|
|
138
|
+
if (data?.type === SDK_READY_MESSAGE) {
|
|
139
|
+
post();
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
if (data?.type !== HOST_COLOR_SCHEME_MESSAGE)
|
|
143
|
+
return;
|
|
144
|
+
const { colorScheme } = data;
|
|
145
|
+
if (!isHostColorScheme(colorScheme))
|
|
146
|
+
return;
|
|
147
|
+
store.setScheme(colorScheme);
|
|
148
|
+
};
|
|
149
|
+
activeCleanup?.();
|
|
150
|
+
window.addEventListener('message', handler);
|
|
151
|
+
const cleanup = () => {
|
|
152
|
+
window.removeEventListener('message', handler);
|
|
153
|
+
if (activeCleanup === cleanup)
|
|
154
|
+
activeCleanup = null;
|
|
155
|
+
};
|
|
156
|
+
activeCleanup = cleanup;
|
|
157
|
+
post();
|
|
158
|
+
return cleanup;
|
|
159
|
+
}
|
|
160
|
+
/**
|
|
161
|
+
* The scheme the app is currently rendering in. CSS should follow the `.dark`
|
|
162
|
+
* class through the theme tokens; reach for this hook only where a value can't
|
|
163
|
+
* be expressed in CSS — a charting library's theme object, an image swap.
|
|
164
|
+
*
|
|
165
|
+
* const colorScheme = useColorScheme();
|
|
166
|
+
* <ResponsiveContainer theme={colorScheme === 'dark' ? darkTheme : lightTheme} />
|
|
167
|
+
*/
|
|
168
|
+
export function useColorScheme() {
|
|
169
|
+
const store = getSharedStore();
|
|
170
|
+
return useSyncExternalStore(store.subscribe, store.getScheme, () => 'light');
|
|
171
|
+
}
|
|
172
|
+
/**
|
|
173
|
+
* Test-only seam: drops the listener and the shared store so the next mount
|
|
174
|
+
* re-reads the seed. The store itself is covered directly through
|
|
175
|
+
* `createColorSchemeStore`; this exists so the mount tests can vary the URL.
|
|
176
|
+
*/
|
|
177
|
+
export function resetColorSchemeState() {
|
|
178
|
+
activeCleanup?.();
|
|
179
|
+
sharedStore = null;
|
|
180
|
+
}
|
package/dist/features.js
CHANGED
|
@@ -65,6 +65,12 @@ export const SDK_FEATURES = [
|
|
|
65
65
|
description: "Let viewers adjust the visualization from the Lightdash config panel — toggles, dropdowns, numbers, text and colours — and take series colours from the chart's palette, without regenerating the app.",
|
|
66
66
|
wiring: 'Declare configOptions (and colorPalette, if the viz colours series) in the viz schema, then read options[name] and colorPalette from useVizContext().',
|
|
67
67
|
},
|
|
68
|
+
{
|
|
69
|
+
key: 'follow-host-theme',
|
|
70
|
+
label: 'Follow the host light/dark mode',
|
|
71
|
+
description: "Render in whatever light or dark mode the viewer's Lightdash (or embed) is set to, instead of a fixed theme, and restyle live when they switch.",
|
|
72
|
+
wiring: 'Keep complete light tokens in :root and dark tokens in .dark, remove any fixed dark class from the app shell, and use useColorScheme() only for colours that cannot be expressed in CSS.',
|
|
73
|
+
},
|
|
68
74
|
];
|
|
69
75
|
export const SDK_FEATURE_KEYS = SDK_FEATURES.map((f) => f.key);
|
|
70
76
|
export const SDK_MANIFEST_MESSAGE_TYPE = 'lightdash:sdk:manifest';
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const SDK_VERSION = "1.
|
|
1
|
+
export declare const SDK_VERSION = "1.75.1";
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
// Generated by scripts/generateSdkVersion.mjs (prebuild) — do not edit.
|
|
2
|
-
export const SDK_VERSION = '1.
|
|
2
|
+
export const SDK_VERSION = '1.75.1';
|
package/dist/index.d.ts
CHANGED
|
@@ -16,5 +16,7 @@ export { exportToSheets } from './exportToSheets';
|
|
|
16
16
|
export type { ExportToSheetsOptions, ExportToSheetsResult, } from './exportToSheets';
|
|
17
17
|
export { VizContextProvider, useVizContext, getFormatted, getRaw, } from './vizContext';
|
|
18
18
|
export type { VizContext, VizContextCell, VizContextOptionValue, VizContextRow, DataAppVizContextMessage, VizContextRequestMessage, } from './vizContext';
|
|
19
|
+
export { useColorScheme } from './colorScheme';
|
|
20
|
+
export type { HostColorScheme, HostColorSchemeMessage, HostColorSchemeRequestMessage, } from './colorScheme';
|
|
19
21
|
export { useUrlState } from './urlState';
|
|
20
22
|
export type { SdkUrlStateChangeMessage, UrlStateMap } from './urlState';
|
package/dist/index.js
CHANGED
|
@@ -20,5 +20,7 @@ export { useDelivery } from './delivery';
|
|
|
20
20
|
export { exportToSheets } from './exportToSheets';
|
|
21
21
|
// Data app viz render context (host-pushed rows + field mapping + config options)
|
|
22
22
|
export { VizContextProvider, useVizContext, getFormatted, getRaw, } from './vizContext';
|
|
23
|
+
// Host light/dark mode (seeded from the iframe URL, updated by the host)
|
|
24
|
+
export { useColorScheme } from './colorScheme';
|
|
23
25
|
// Shareable URL state (seeded from and written back to the host page URL)
|
|
24
26
|
export { useUrlState } from './urlState';
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lightdash/query-sdk",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.75.1",
|
|
4
4
|
"private": false,
|
|
5
5
|
"type": "module",
|
|
6
6
|
"description": "SDK for building custom data apps against the Lightdash semantic layer",
|
|
@@ -34,7 +34,7 @@
|
|
|
34
34
|
"jsdom": "26.1.0",
|
|
35
35
|
"typescript": "7.0.2",
|
|
36
36
|
"vitest": "4.1.6",
|
|
37
|
-
"@lightdash/common": "1.
|
|
37
|
+
"@lightdash/common": "1.75.1"
|
|
38
38
|
},
|
|
39
39
|
"scripts": {
|
|
40
40
|
"prebuild": "node ./scripts/generateSdkVersion.mjs",
|