@pantoken/scope 0.3.11

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Danny Wahl
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/dist/index.mjs ADDED
@@ -0,0 +1,2 @@
1
+ import { _ as THEME_ATTR, a as createScope, b as schemeClass, c as resolveScheme, d as BOUNDARY_CLASS, f as COLOR_ATTR, g as SCOPE_ATTRS, h as SCHEME_ATTR, i as connectScope, l as resolveScope, m as INSTANCE_ATTR, n as ensureProperties, o as getScope, p as DEFAULT_INSTANCE, r as hasProperties, s as resolveInstance, t as observeScope, u as BOUNDARY_ATTR, v as colorClass, x as themeClass, y as isScheme } from "./src-Daa7KXHA.mjs";
2
+ export { BOUNDARY_ATTR, BOUNDARY_CLASS, COLOR_ATTR, DEFAULT_INSTANCE, INSTANCE_ATTR, SCHEME_ATTR, SCOPE_ATTRS, THEME_ATTR, colorClass, connectScope, createScope, ensureProperties, getScope, hasProperties, isScheme, observeScope, resolveInstance, resolveScheme, resolveScope, schemeClass, themeClass };
@@ -0,0 +1,179 @@
1
+ import { BOUNDARY_ATTR, BOUNDARY_CLASS, COLOR_ATTR, INSTANCE_ATTR, SCHEME_ATTR, SCOPE_ATTRS, Scheme, Scheme as Scheme$1, THEME_ATTR, colorClass, isScheme, schemeClass, themeClass } from "@pantoken/utils/scope";
2
+ //#region src/contract.d.ts
3
+ /** The resolvable state of a scope. Every field is optional — an unset field inherits. */
4
+ interface ScopeConfig {
5
+ /** Theme key, e.g. `"rebrand"`. Matched by `[data-pantoken-theme="…"]`. */
6
+ theme?: string;
7
+ /** Pinned color scheme. Omit to follow the OS preference. */
8
+ scheme?: Scheme$1;
9
+ /** Custom brand color key. */
10
+ color?: string;
11
+ /** Identifier of the pantoken instance that owns this scope. */
12
+ instanceId?: string;
13
+ }
14
+ /** A {@link ScopeConfig} resolved against the DOM, plus where each part came from. */
15
+ interface ResolvedScope extends ScopeConfig {
16
+ /** The nearest element that declared any scope attribute, or `null` when none did. */
17
+ element: Element | null;
18
+ /** True when resolution stopped at a `data-pantoken-boundary` element. */
19
+ bounded: boolean;
20
+ }
21
+ /** The default instance id, used when a scope does not name one. */
22
+ export declare const DEFAULT_INSTANCE = "pantoken";
23
+ //#endregion
24
+ //#region src/resolve.d.ts
25
+ /**
26
+ * Resolve the scope in effect for `start` by walking up its ancestors.
27
+ *
28
+ * @param start - The element to resolve for. Its own attributes participate.
29
+ * @returns The {@link ResolvedScope}; fields no ancestor declared are left `undefined`.
30
+ *
31
+ * @example
32
+ * ```ts
33
+ * import { resolveScope } from "@pantoken/scope";
34
+ *
35
+ * // <div data-pantoken-theme="canvas"><span id="x"></span></div>
36
+ * resolveScope(document.querySelector("#x")!).theme; // "canvas"
37
+ * ```
38
+ */
39
+ export declare function resolveScope(start: Element | null): ResolvedScope;
40
+ /**
41
+ * The instance that owns `start`, falling back to {@link DEFAULT_INSTANCE}.
42
+ *
43
+ * Use this to decide whether an event or message is yours before acting on it.
44
+ */
45
+ export declare function resolveInstance(start: Element | null): string;
46
+ /**
47
+ * The effective color scheme for `start`: an explicit pin if any ancestor set one, otherwise the
48
+ * `color-scheme` the element actually computes to, otherwise the OS preference.
49
+ *
50
+ * Never reads `document.documentElement` — that global read is exactly what desynchronises two
51
+ * instances on one page.
52
+ */
53
+ export declare function resolveScheme(start: Element | null): Scheme;
54
+ //#endregion
55
+ //#region src/scope.d.ts
56
+ /** The message envelope exchanged between a scope and its attached frames. */
57
+ interface ScopeMessage extends ScopeConfig {
58
+ type: "pantoken-scope" | "pantoken-scope-request";
59
+ instanceId: string;
60
+ }
61
+ /** Options for {@link createScope}. */
62
+ interface CreateScopeOptions extends ScopeConfig {
63
+ /**
64
+ * Identifier distinguishing this pantoken instance from others on the page. Scopes, storage keys,
65
+ * and frame messages are all keyed by it.
66
+ */
67
+ instanceId?: string;
68
+ /**
69
+ * Stop ancestor resolution at this element, so nothing outside can theme what is inside. Use for
70
+ * a preview pane that must not inherit the surrounding app's theme.
71
+ */
72
+ boundary?: boolean;
73
+ /** Persist and restore state. Pass `false` to keep the scope in-memory only (default `true`). */
74
+ persist?: boolean;
75
+ /** Storage backend (default `localStorage`, silently skipped when unavailable). */
76
+ storage?: Pick<Storage, "getItem" | "setItem"> | null;
77
+ }
78
+ /** A live scope bound to one element. */
79
+ interface PantokenScope {
80
+ /** The element carrying the scope attributes. */
81
+ readonly element: HTMLElement;
82
+ /** The instance this scope belongs to. */
83
+ readonly instanceId: string;
84
+ /** The scope's own configuration — what it declares, not what it inherits. */
85
+ get(): ScopeConfig;
86
+ /** Merge a partial configuration in. Pass `null` for a field to clear it and inherit again. */
87
+ set(patch: Partial<Record<keyof ScopeConfig, string | null | undefined>>): void;
88
+ /** Resolve the effective scope for a descendant (defaults to this scope's own element). */
89
+ resolve(el?: Element): ResolvedScope;
90
+ /** Send this scope's state to `frame` and keep it in sync. Returns a detach function. */
91
+ attachFrame(frame: HTMLIFrameElement): () => void;
92
+ /** Observe changes. Returns an unsubscribe function. */
93
+ subscribe(listener: (config: ScopeConfig) => void): () => void;
94
+ /** Remove the attributes, detach frames, and unregister. */
95
+ destroy(): void;
96
+ }
97
+ /** The scope registered for `instanceId` in `doc`, if any. */
98
+ export declare function getScope(instanceId?: string, doc?: Document): PantokenScope | undefined;
99
+ /**
100
+ * Create a scope rooted at `element`.
101
+ *
102
+ * @example Two independent themes on one page
103
+ * ```ts
104
+ * import { createScope } from "@pantoken/scope";
105
+ *
106
+ * createScope(document.querySelector("#app")!, { theme: "rebrand", scheme: "dark" });
107
+ * createScope(document.querySelector("#preview")!, {
108
+ * instanceId: "preview",
109
+ * theme: "canvas",
110
+ * scheme: "light",
111
+ * boundary: true,
112
+ * });
113
+ * ```
114
+ */
115
+ export declare function createScope(element: HTMLElement, options?: CreateScopeOptions): PantokenScope;
116
+ /** Options for {@link connectScope}. */
117
+ interface ConnectScopeOptions {
118
+ /** The instance to accept messages for (default {@link DEFAULT_INSTANCE}). */
119
+ instanceId?: string;
120
+ /** The element to apply received state to (default `document.documentElement`). */
121
+ element?: HTMLElement;
122
+ /** The window to talk to (default `window.parent`). */
123
+ host?: Window | null;
124
+ /** The origin the host must have. Pass `"*"` only for an opaque-origin `srcdoc` document. */
125
+ hostOrigin?: string;
126
+ /** Called after each applied update. */
127
+ onChange?: (config: ScopeConfig) => void;
128
+ }
129
+ /**
130
+ * Run inside a frame: ask the host page's scope for its state and follow it.
131
+ *
132
+ * The frame gets its own scope element, so it can be told to render a different theme or scheme than
133
+ * the host — it is not inheriting ambiently.
134
+ *
135
+ * @returns A disconnect function.
136
+ */
137
+ export declare function connectScope(options?: ConnectScopeOptions): () => void;
138
+ //#endregion
139
+ //#region src/inject.d.ts
140
+ /**
141
+ * Inject `css` into `doc` once. A second call with the same `id` does nothing, whichever instance
142
+ * makes it.
143
+ *
144
+ * @param css - The registrations sheet, e.g. `@pantoken/css/properties.css`.
145
+ * @param options - `id` distinguishes unrelated sheets; `doc` defaults to the current document.
146
+ * @returns `true` when this call performed the injection.
147
+ *
148
+ * @example
149
+ * ```ts
150
+ * import { ensureProperties } from "@pantoken/scope";
151
+ * import properties from "@pantoken/css/properties.css?inline";
152
+ *
153
+ * ensureProperties(properties);
154
+ * ```
155
+ */
156
+ export declare function ensureProperties(css: string, options?: {
157
+ id?: string;
158
+ doc?: Document;
159
+ }): boolean;
160
+ /** True when a registrations sheet with `id` is already present in `doc`. */
161
+ export declare function hasProperties(id?: string, doc?: Document): boolean;
162
+ //#endregion
163
+ //#region src/observe.d.ts
164
+ /**
165
+ * Call `listener` whenever the scope resolved for `target` changes, including when an ancestor's
166
+ * attributes change.
167
+ *
168
+ * @returns A stop function.
169
+ *
170
+ * @example
171
+ * ```ts
172
+ * import { observeScope } from "@pantoken/scope";
173
+ *
174
+ * const stop = observeScope(el, (scope) => render(scope.theme, scope.scheme));
175
+ * ```
176
+ */
177
+ export declare function observeScope(target: Element, listener: (scope: ResolvedScope) => void): () => void;
178
+ //#endregion
179
+ export { BOUNDARY_ATTR, BOUNDARY_CLASS, COLOR_ATTR, type ConnectScopeOptions, type CreateScopeOptions, INSTANCE_ATTR, type PantokenScope, type ResolvedScope, SCHEME_ATTR, SCOPE_ATTRS, type Scheme, type ScopeConfig, type ScopeMessage, THEME_ATTR, colorClass, isScheme, schemeClass, themeClass };
package/dist/scope.mjs ADDED
@@ -0,0 +1,2 @@
1
+ import { _ as THEME_ATTR, a as createScope, b as schemeClass, c as resolveScheme, d as BOUNDARY_CLASS, f as COLOR_ATTR, g as SCOPE_ATTRS, h as SCHEME_ATTR, i as connectScope, l as resolveScope, m as INSTANCE_ATTR, n as ensureProperties, o as getScope, p as DEFAULT_INSTANCE, r as hasProperties, s as resolveInstance, t as observeScope, u as BOUNDARY_ATTR, v as colorClass, x as themeClass, y as isScheme } from "./src-Daa7KXHA.mjs";
2
+ export { BOUNDARY_ATTR, BOUNDARY_CLASS, COLOR_ATTR, DEFAULT_INSTANCE, INSTANCE_ATTR, SCHEME_ATTR, SCOPE_ATTRS, THEME_ATTR, colorClass, connectScope, createScope, ensureProperties, getScope, hasProperties, isScheme, observeScope, resolveInstance, resolveScheme, resolveScope, schemeClass, themeClass };
@@ -0,0 +1,416 @@
1
+ import { BOUNDARY_ATTR, BOUNDARY_CLASS, COLOR_ATTR, INSTANCE_ATTR, SCHEME_ATTR, SCOPE_ATTRS, THEME_ATTR, colorClass, isScheme, schemeClass, themeClass } from "@pantoken/utils/scope";
2
+ //#region src/contract.ts
3
+ /** The default instance id, used when a scope does not name one. */
4
+ const DEFAULT_INSTANCE = "pantoken";
5
+ //#endregion
6
+ //#region src/resolve.ts
7
+ /**
8
+ * Ancestor resolution — the "auto-detect inheritance" half of multi-instance support.
9
+ *
10
+ * Each scope attribute resolves independently: a subtree can override the color scheme while still
11
+ * inheriting the theme from an ancestor, which is what the CSS does too (separate cascade layers,
12
+ * separate attributes). Resolution stops at the first `data-pantoken-boundary` element, and never
13
+ * crosses a shadow root — a shadow root is an implicit boundary.
14
+ *
15
+ * @module
16
+ */
17
+ function attr(el, name) {
18
+ const value = el.getAttribute(name);
19
+ return value === null || value === "" ? void 0 : value;
20
+ }
21
+ /**
22
+ * The value a `.--pantoken-<kind>-<value>` class encodes, if the element carries one.
23
+ *
24
+ * The emitter pairs every scope attribute with a class twin for hosts that strip `data-*`, so
25
+ * resolution has to read both or the runtime and the stylesheet disagree about what's in effect.
26
+ */
27
+ function fromClass(el, kind) {
28
+ const prefix = `--pantoken-${kind}-`;
29
+ for (const cls of el.classList) if (cls.startsWith(prefix) && cls.length > prefix.length) return cls.slice(prefix.length);
30
+ }
31
+ const readTheme = (el) => attr(el, THEME_ATTR) ?? fromClass(el, "theme");
32
+ const readColor = (el) => attr(el, COLOR_ATTR) ?? fromClass(el, "color");
33
+ function readScheme(el) {
34
+ const value = attr(el, SCHEME_ATTR) ?? fromClass(el, "scheme");
35
+ return isScheme(value) ? value : void 0;
36
+ }
37
+ const isBoundary = (el) => el.hasAttribute(BOUNDARY_ATTR) || el.classList.contains(BOUNDARY_CLASS);
38
+ /**
39
+ * Resolve the scope in effect for `start` by walking up its ancestors.
40
+ *
41
+ * @param start - The element to resolve for. Its own attributes participate.
42
+ * @returns The {@link ResolvedScope}; fields no ancestor declared are left `undefined`.
43
+ *
44
+ * @example
45
+ * ```ts
46
+ * import { resolveScope } from "@pantoken/scope";
47
+ *
48
+ * // <div data-pantoken-theme="canvas"><span id="x"></span></div>
49
+ * resolveScope(document.querySelector("#x")!).theme; // "canvas"
50
+ * ```
51
+ */
52
+ function resolveScope(start) {
53
+ let theme;
54
+ let scheme;
55
+ let color;
56
+ let instanceId;
57
+ let element = null;
58
+ let bounded = false;
59
+ for (let el = start; el; el = el.parentElement) {
60
+ let declared = false;
61
+ const nextTheme = readTheme(el);
62
+ if (theme === void 0 && nextTheme !== void 0) {
63
+ theme = nextTheme;
64
+ declared = true;
65
+ }
66
+ const nextScheme = readScheme(el);
67
+ if (scheme === void 0 && nextScheme !== void 0) {
68
+ scheme = nextScheme;
69
+ declared = true;
70
+ }
71
+ const nextColor = readColor(el);
72
+ if (color === void 0 && nextColor !== void 0) {
73
+ color = nextColor;
74
+ declared = true;
75
+ }
76
+ const nextInstance = attr(el, INSTANCE_ATTR);
77
+ if (instanceId === void 0 && nextInstance !== void 0) {
78
+ instanceId = nextInstance;
79
+ declared = true;
80
+ }
81
+ if (declared && element === null) element = el;
82
+ if (isBoundary(el)) {
83
+ bounded = true;
84
+ break;
85
+ }
86
+ }
87
+ return {
88
+ theme,
89
+ scheme,
90
+ color,
91
+ instanceId,
92
+ element,
93
+ bounded
94
+ };
95
+ }
96
+ /**
97
+ * The instance that owns `start`, falling back to {@link DEFAULT_INSTANCE}.
98
+ *
99
+ * Use this to decide whether an event or message is yours before acting on it.
100
+ */
101
+ function resolveInstance(start) {
102
+ return resolveScope(start).instanceId ?? "pantoken";
103
+ }
104
+ /**
105
+ * The effective color scheme for `start`: an explicit pin if any ancestor set one, otherwise the
106
+ * `color-scheme` the element actually computes to, otherwise the OS preference.
107
+ *
108
+ * Never reads `document.documentElement` — that global read is exactly what desynchronises two
109
+ * instances on one page.
110
+ */
111
+ function resolveScheme(start) {
112
+ const pinned = resolveScope(start).scheme;
113
+ if (pinned) return pinned;
114
+ const view = start?.ownerDocument?.defaultView;
115
+ if (start && view) {
116
+ const computed = view.getComputedStyle(start).colorScheme;
117
+ if (computed === "dark") return "dark";
118
+ if (computed === "light") return "light";
119
+ }
120
+ return view?.matchMedia?.("(prefers-color-scheme: dark)").matches ? "dark" : "light";
121
+ }
122
+ //#endregion
123
+ //#region src/scope.ts
124
+ /**
125
+ * Scope instances — the "manual config to avoid collision" half of multi-instance support.
126
+ *
127
+ * A scope owns one element and writes the `data-pantoken-*` attributes onto it. Everything below
128
+ * that element resolves to it; siblings are independent. State is persisted under a key namespaced
129
+ * by instance id, and child frames are messaged individually after opting in via
130
+ * {@link PantokenScope.attachFrame} — never by sweeping the whole document for iframes.
131
+ *
132
+ * @module
133
+ */
134
+ const registries = /* @__PURE__ */ new WeakMap();
135
+ function registryFor(doc) {
136
+ let registry = registries.get(doc);
137
+ if (!registry) {
138
+ registry = /* @__PURE__ */ new Map();
139
+ registries.set(doc, registry);
140
+ }
141
+ return registry;
142
+ }
143
+ /** The scope registered for `instanceId` in `doc`, if any. */
144
+ function getScope(instanceId = DEFAULT_INSTANCE, doc = document) {
145
+ return registryFor(doc).get(instanceId);
146
+ }
147
+ const storageKey = (instanceId, field) => `pantoken:${instanceId}:${field}`;
148
+ function defaultStorage() {
149
+ try {
150
+ return typeof localStorage === "undefined" ? null : localStorage;
151
+ } catch {
152
+ return null;
153
+ }
154
+ }
155
+ /**
156
+ * The origin to post to for `frame`. A `srcdoc` frame has an opaque origin that can only be
157
+ * addressed as `"*"`; anything with a real `src` gets its concrete origin so the message cannot
158
+ * leak to a navigated-away document.
159
+ */
160
+ function frameOrigin(frame) {
161
+ if (!frame.hasAttribute("src")) return "*";
162
+ try {
163
+ return new URL(frame.src, frame.ownerDocument.location.href).origin;
164
+ } catch {
165
+ return frame.ownerDocument.location.origin;
166
+ }
167
+ }
168
+ const FIELDS = {
169
+ theme: THEME_ATTR,
170
+ scheme: SCHEME_ATTR,
171
+ color: COLOR_ATTR
172
+ };
173
+ /**
174
+ * Create a scope rooted at `element`.
175
+ *
176
+ * @example Two independent themes on one page
177
+ * ```ts
178
+ * import { createScope } from "@pantoken/scope";
179
+ *
180
+ * createScope(document.querySelector("#app")!, { theme: "rebrand", scheme: "dark" });
181
+ * createScope(document.querySelector("#preview")!, {
182
+ * instanceId: "preview",
183
+ * theme: "canvas",
184
+ * scheme: "light",
185
+ * boundary: true,
186
+ * });
187
+ * ```
188
+ */
189
+ function createScope(element, options = {}) {
190
+ const { instanceId = DEFAULT_INSTANCE, boundary = false, persist = true, storage = defaultStorage(), ...initial } = options;
191
+ const doc = element.ownerDocument;
192
+ const registry = registryFor(doc);
193
+ registry.get(instanceId)?.destroy();
194
+ const read = (field) => {
195
+ if (!persist || !storage) return void 0;
196
+ try {
197
+ return storage.getItem(storageKey(instanceId, field)) ?? void 0;
198
+ } catch {
199
+ return;
200
+ }
201
+ };
202
+ const config = {
203
+ theme: initial.theme ?? read("theme"),
204
+ scheme: initial.scheme ?? (isScheme(read("scheme")) ? read("scheme") : void 0),
205
+ color: initial.color ?? read("color"),
206
+ instanceId
207
+ };
208
+ const listeners = /* @__PURE__ */ new Set();
209
+ const frames = /* @__PURE__ */ new Set();
210
+ let destroyed = false;
211
+ const write = () => {
212
+ element.setAttribute(INSTANCE_ATTR, instanceId);
213
+ if (boundary) element.setAttribute(BOUNDARY_ATTR, "");
214
+ for (const [field, attribute] of Object.entries(FIELDS)) {
215
+ const value = config[field];
216
+ if (value === void 0) element.removeAttribute(attribute);
217
+ else element.setAttribute(attribute, value);
218
+ }
219
+ element.style.colorScheme = config.scheme ?? "";
220
+ };
221
+ const persistConfig = () => {
222
+ if (!persist || !storage) return;
223
+ for (const field of Object.keys(FIELDS)) try {
224
+ storage.setItem(storageKey(instanceId, field), config[field] ?? "");
225
+ } catch {}
226
+ };
227
+ const post = (frame) => {
228
+ const message = {
229
+ type: "pantoken-scope",
230
+ instanceId,
231
+ ...config
232
+ };
233
+ frame.contentWindow?.postMessage(message, frameOrigin(frame));
234
+ };
235
+ const broadcast = () => {
236
+ for (const frame of frames) post(frame);
237
+ for (const listener of listeners) listener({ ...config });
238
+ };
239
+ const onFrameRequest = (event) => {
240
+ const data = event.data;
241
+ if (data?.type !== "pantoken-scope-request") return;
242
+ if (data.instanceId !== instanceId) return;
243
+ for (const frame of frames) if (frame.contentWindow === event.source) post(frame);
244
+ };
245
+ doc.defaultView?.addEventListener("message", onFrameRequest);
246
+ write();
247
+ persistConfig();
248
+ const scope = {
249
+ element,
250
+ instanceId,
251
+ get: () => ({ ...config }),
252
+ set(patch) {
253
+ if (destroyed) return;
254
+ for (const field of Object.keys(FIELDS)) {
255
+ if (!(field in patch)) continue;
256
+ const value = patch[field];
257
+ if (value === null || value === void 0) delete config[field];
258
+ else if (field === "scheme") config.scheme = isScheme(value) ? value : void 0;
259
+ else config[field] = value;
260
+ }
261
+ write();
262
+ persistConfig();
263
+ broadcast();
264
+ },
265
+ resolve: (el = element) => resolveScope(el),
266
+ attachFrame(frame) {
267
+ frames.add(frame);
268
+ post(frame);
269
+ return () => {
270
+ frames.delete(frame);
271
+ };
272
+ },
273
+ subscribe(listener) {
274
+ listeners.add(listener);
275
+ return () => {
276
+ listeners.delete(listener);
277
+ };
278
+ },
279
+ destroy() {
280
+ if (destroyed) return;
281
+ destroyed = true;
282
+ doc.defaultView?.removeEventListener("message", onFrameRequest);
283
+ frames.clear();
284
+ listeners.clear();
285
+ for (const attribute of [
286
+ ...Object.values(FIELDS),
287
+ INSTANCE_ATTR,
288
+ BOUNDARY_ATTR
289
+ ]) element.removeAttribute(attribute);
290
+ element.style.colorScheme = "";
291
+ if (registry.get(instanceId) === scope) registry.delete(instanceId);
292
+ }
293
+ };
294
+ registry.set(instanceId, scope);
295
+ return scope;
296
+ }
297
+ /**
298
+ * Run inside a frame: ask the host page's scope for its state and follow it.
299
+ *
300
+ * The frame gets its own scope element, so it can be told to render a different theme or scheme than
301
+ * the host — it is not inheriting ambiently.
302
+ *
303
+ * @returns A disconnect function.
304
+ */
305
+ function connectScope(options = {}) {
306
+ const { instanceId = DEFAULT_INSTANCE, element = document.documentElement, host = window.parent, hostOrigin, onChange } = options;
307
+ const local = createScope(element, {
308
+ instanceId,
309
+ persist: false
310
+ });
311
+ const onMessage = (event) => {
312
+ if (host && event.source !== host) return;
313
+ if (hostOrigin && hostOrigin !== "*" && event.origin !== hostOrigin) return;
314
+ const data = event.data;
315
+ if (data?.type !== "pantoken-scope" || data.instanceId !== instanceId) return;
316
+ local.set({
317
+ theme: data.theme,
318
+ scheme: data.scheme,
319
+ color: data.color
320
+ });
321
+ onChange?.(local.get());
322
+ };
323
+ window.addEventListener("message", onMessage);
324
+ const request = {
325
+ type: "pantoken-scope-request",
326
+ instanceId
327
+ };
328
+ host?.postMessage(request, hostOrigin ?? "*");
329
+ return () => {
330
+ window.removeEventListener("message", onMessage);
331
+ local.destroy();
332
+ };
333
+ }
334
+ //#endregion
335
+ //#region src/inject.ts
336
+ /**
337
+ * One-time injection of the document-global half of the token sheet.
338
+ *
339
+ * `@property` registrations are document-scoped, so a second instance injecting its own copy would
340
+ * silently redefine every token's initial-value for the whole page. {@link ensureProperties} makes
341
+ * that a no-op instead.
342
+ *
343
+ * @module
344
+ */
345
+ const MARKER = "data-pantoken-properties";
346
+ /**
347
+ * Inject `css` into `doc` once. A second call with the same `id` does nothing, whichever instance
348
+ * makes it.
349
+ *
350
+ * @param css - The registrations sheet, e.g. `@pantoken/css/properties.css`.
351
+ * @param options - `id` distinguishes unrelated sheets; `doc` defaults to the current document.
352
+ * @returns `true` when this call performed the injection.
353
+ *
354
+ * @example
355
+ * ```ts
356
+ * import { ensureProperties } from "@pantoken/scope";
357
+ * import properties from "@pantoken/css/properties.css?inline";
358
+ *
359
+ * ensureProperties(properties);
360
+ * ```
361
+ */
362
+ function ensureProperties(css, options = {}) {
363
+ const { id = "default", doc = document } = options;
364
+ if (doc.head.querySelector(`style[${MARKER}="${CSS.escape(id)}"]`)) return false;
365
+ const style = doc.createElement("style");
366
+ style.setAttribute(MARKER, id);
367
+ style.textContent = css;
368
+ doc.head.prepend(style);
369
+ return true;
370
+ }
371
+ /** True when a registrations sheet with `id` is already present in `doc`. */
372
+ function hasProperties(id = "default", doc = document) {
373
+ return doc.head.querySelector(`style[${MARKER}="${CSS.escape(id)}"]`) !== null;
374
+ }
375
+ //#endregion
376
+ //#region src/observe.ts
377
+ /**
378
+ * Reacting to scope changes made outside this instance — an ancestor retheming, or another
379
+ * instance's scope element changing above ours.
380
+ *
381
+ * @module
382
+ */
383
+ /**
384
+ * Call `listener` whenever the scope resolved for `target` changes, including when an ancestor's
385
+ * attributes change.
386
+ *
387
+ * @returns A stop function.
388
+ *
389
+ * @example
390
+ * ```ts
391
+ * import { observeScope } from "@pantoken/scope";
392
+ *
393
+ * const stop = observeScope(el, (scope) => render(scope.theme, scope.scheme));
394
+ * ```
395
+ */
396
+ function observeScope(target, listener) {
397
+ const view = target.ownerDocument.defaultView;
398
+ if (!view?.MutationObserver) return () => {};
399
+ let previous = resolveScope(target);
400
+ const observer = new view.MutationObserver(() => {
401
+ const next = resolveScope(target);
402
+ if (next.theme === previous.theme && next.scheme === previous.scheme && next.color === previous.color && next.instanceId === previous.instanceId) return;
403
+ previous = next;
404
+ listener(next);
405
+ });
406
+ observer.observe(target.ownerDocument.documentElement, {
407
+ attributes: true,
408
+ attributeFilter: [...SCOPE_ATTRS, "class"],
409
+ subtree: true
410
+ });
411
+ return () => {
412
+ observer.disconnect();
413
+ };
414
+ }
415
+ //#endregion
416
+ export { THEME_ATTR as _, createScope as a, schemeClass as b, resolveScheme as c, BOUNDARY_CLASS as d, COLOR_ATTR as f, SCOPE_ATTRS as g, SCHEME_ATTR as h, connectScope as i, resolveScope as l, INSTANCE_ATTR as m, ensureProperties as n, getScope as o, DEFAULT_INSTANCE as p, hasProperties as r, resolveInstance as s, observeScope as t, BOUNDARY_ATTR as u, colorClass as v, themeClass as x, isScheme as y };
package/package.json ADDED
@@ -0,0 +1,51 @@
1
+ {
2
+ "name": "@pantoken/scope",
3
+ "version": "0.3.11",
4
+ "description": "Run several pantoken themes and color schemes in one document: per-subtree scopes, ancestor resolution, and hard boundaries.",
5
+ "homepage": "https://pantoken.app",
6
+ "bugs": "https://github.com/thedannywahl/pantoken/issues",
7
+ "license": "MIT",
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "git+https://github.com/thedannywahl/pantoken.git",
11
+ "directory": "formats/scope"
12
+ },
13
+ "files": [
14
+ "dist"
15
+ ],
16
+ "type": "module",
17
+ "sideEffects": false,
18
+ "exports": {
19
+ ".": {
20
+ "types": "./dist/scope.d.mts",
21
+ "default": "./dist/index.mjs"
22
+ },
23
+ "./package.json": "./package.json"
24
+ },
25
+ "publishConfig": {
26
+ "access": "public",
27
+ "provenance": true
28
+ },
29
+ "dependencies": {
30
+ "@pantoken/utils": "1.0.3"
31
+ },
32
+ "devDependencies": {
33
+ "@types/node": "^26.6.1",
34
+ "happy-dom": "^20.14.5",
35
+ "typescript": "^7.0.2",
36
+ "vite": "npm:@voidzero-dev/vite-plus-core@0.3.3",
37
+ "vite-plus": "0.3.3"
38
+ },
39
+ "engines": {
40
+ "node": ">=22.18.0"
41
+ },
42
+ "pantoken": {
43
+ "key": "scope",
44
+ "kind": "namespace"
45
+ },
46
+ "scripts": {
47
+ "dev": "vp pack --watch",
48
+ "test": "vp test",
49
+ "check": "vp check"
50
+ }
51
+ }