@sorb/leaf 0.2.1 → 0.3.0
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/README.md +8 -2
- package/dist/core.js +665 -0
- package/dist/core.js.map +7 -0
- package/dist/core.mjs +643 -0
- package/dist/core.mjs.map +7 -0
- package/dist/index.js +760 -65
- package/dist/index.js.map +4 -4
- package/dist/index.mjs +758 -63
- package/dist/index.mjs.map +4 -4
- package/package.json +8 -3
package/dist/core.mjs
ADDED
|
@@ -0,0 +1,643 @@
|
|
|
1
|
+
// src/sanitize.js
|
|
2
|
+
var ALLOWED_FUNCTIONS = /* @__PURE__ */ new Set([
|
|
3
|
+
"rgb",
|
|
4
|
+
"rgba",
|
|
5
|
+
"hsl",
|
|
6
|
+
"hsla",
|
|
7
|
+
"hwb",
|
|
8
|
+
"lab",
|
|
9
|
+
"lch",
|
|
10
|
+
"oklab",
|
|
11
|
+
"oklch",
|
|
12
|
+
"color",
|
|
13
|
+
"calc",
|
|
14
|
+
"min",
|
|
15
|
+
"max",
|
|
16
|
+
"clamp",
|
|
17
|
+
"var",
|
|
18
|
+
"env"
|
|
19
|
+
]);
|
|
20
|
+
var FUNCTION_CALL = /([a-zA-Z_-][\w-]*)\s*\(/g;
|
|
21
|
+
var CONTROL_CHARS = /[\x00-\x1f]/;
|
|
22
|
+
var CONTEXT_BREAK = /[{};]/;
|
|
23
|
+
var sanitizeCssValue = (value) => {
|
|
24
|
+
if (typeof value !== "string") {
|
|
25
|
+
return { ok: false, value: "", reason: "not-a-string" };
|
|
26
|
+
}
|
|
27
|
+
const raw = value;
|
|
28
|
+
if (raw.length === 0) {
|
|
29
|
+
return { ok: false, value: "", reason: "empty" };
|
|
30
|
+
}
|
|
31
|
+
if (CONTROL_CHARS.test(raw)) {
|
|
32
|
+
return { ok: false, value: raw, reason: "control-char" };
|
|
33
|
+
}
|
|
34
|
+
if (CONTEXT_BREAK.test(raw)) {
|
|
35
|
+
return { ok: false, value: raw, reason: "context-break-char" };
|
|
36
|
+
}
|
|
37
|
+
const lower = raw.toLowerCase();
|
|
38
|
+
const collapsed = lower.replace(/\s+/g, "");
|
|
39
|
+
if (collapsed.includes("@import")) {
|
|
40
|
+
return { ok: false, value: raw, reason: "at-import" };
|
|
41
|
+
}
|
|
42
|
+
if (collapsed.includes("javascript:")) {
|
|
43
|
+
return { ok: false, value: raw, reason: "javascript-scheme" };
|
|
44
|
+
}
|
|
45
|
+
if (collapsed.includes("</")) {
|
|
46
|
+
return { ok: false, value: raw, reason: "markup-break" };
|
|
47
|
+
}
|
|
48
|
+
FUNCTION_CALL.lastIndex = 0;
|
|
49
|
+
let match;
|
|
50
|
+
while ((match = FUNCTION_CALL.exec(raw)) !== null) {
|
|
51
|
+
const name = match[1].toLowerCase();
|
|
52
|
+
if (!ALLOWED_FUNCTIONS.has(name)) {
|
|
53
|
+
return { ok: false, value: raw, reason: `disallowed-function:${name}` };
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
return { ok: true, value: raw };
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
// src/apply.js
|
|
60
|
+
var warnRejected = (key, reason) => {
|
|
61
|
+
try {
|
|
62
|
+
if (typeof process !== "undefined" && process.env && true) {
|
|
63
|
+
console.warn(
|
|
64
|
+
`[sorb] skipped token "--${key}": value failed CSS sanitization` + (reason ? ` (${reason})` : "")
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
} catch (e) {
|
|
68
|
+
}
|
|
69
|
+
};
|
|
70
|
+
var applyTokens = (tokens) => {
|
|
71
|
+
const root = document.documentElement;
|
|
72
|
+
Object.entries(tokens).forEach(([key, value]) => {
|
|
73
|
+
const result = sanitizeCssValue(String(value));
|
|
74
|
+
if (!result.ok) {
|
|
75
|
+
warnRejected(key, result.reason);
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
root.style.setProperty(`--${key}`, result.value);
|
|
79
|
+
});
|
|
80
|
+
};
|
|
81
|
+
var clearTokenOverrides = (tokens) => {
|
|
82
|
+
const root = document.documentElement;
|
|
83
|
+
Object.keys(tokens).forEach((key) => {
|
|
84
|
+
root.style.removeProperty(`--${key}`);
|
|
85
|
+
});
|
|
86
|
+
};
|
|
87
|
+
var MODE_STYLESHEET_ID = "sorb-tokens";
|
|
88
|
+
var injectModeStylesheet = (css) => {
|
|
89
|
+
let tag = document.getElementById(MODE_STYLESHEET_ID);
|
|
90
|
+
if (!tag) {
|
|
91
|
+
tag = document.createElement("style");
|
|
92
|
+
tag.id = MODE_STYLESHEET_ID;
|
|
93
|
+
document.head.appendChild(tag);
|
|
94
|
+
}
|
|
95
|
+
tag.textContent = css;
|
|
96
|
+
};
|
|
97
|
+
var clearModeStylesheet = () => {
|
|
98
|
+
const tag = document.getElementById(MODE_STYLESHEET_ID);
|
|
99
|
+
if (tag && tag.parentNode) tag.parentNode.removeChild(tag);
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
// src/modeStylesheet.js
|
|
103
|
+
var buildModeStylesheet = (lightVars, darkVars, darkMode) => {
|
|
104
|
+
const lightDecls = normalizeDecls(lightVars);
|
|
105
|
+
const hasDark = !!darkMode && !!darkVars && Object.keys(darkVars).length > 0;
|
|
106
|
+
if (!hasDark) {
|
|
107
|
+
return `:root {
|
|
108
|
+
${indent(lightDecls)}
|
|
109
|
+
}
|
|
110
|
+
`;
|
|
111
|
+
}
|
|
112
|
+
const darkDecls = normalizeDecls(darkVars);
|
|
113
|
+
const darkSelector = darkMode.darkSelector;
|
|
114
|
+
const lightSelector = darkMode.lightSelector;
|
|
115
|
+
const mediaScopeSelector = lightSelector ? `:root:not(${lightSelector})` : ":root";
|
|
116
|
+
const lines = [];
|
|
117
|
+
lines.push(":root {");
|
|
118
|
+
lines.push(indent([...lightDecls, "color-scheme: light;"]));
|
|
119
|
+
lines.push("}");
|
|
120
|
+
lines.push("@media (prefers-color-scheme: dark) {");
|
|
121
|
+
lines.push(` ${mediaScopeSelector} {`);
|
|
122
|
+
lines.push(indent([...darkDecls, "color-scheme: dark;"], 2));
|
|
123
|
+
lines.push(" }");
|
|
124
|
+
lines.push("}");
|
|
125
|
+
lines.push(`${darkSelector} {`);
|
|
126
|
+
lines.push(indent([...darkDecls, "color-scheme: dark;"]));
|
|
127
|
+
lines.push("}");
|
|
128
|
+
if (lightSelector) {
|
|
129
|
+
lines.push(`${lightSelector} {`);
|
|
130
|
+
lines.push(indent([...lightDecls, "color-scheme: light;"]));
|
|
131
|
+
lines.push("}");
|
|
132
|
+
}
|
|
133
|
+
return `${lines.join("\n")}
|
|
134
|
+
`;
|
|
135
|
+
};
|
|
136
|
+
var normalizeDecls = (vars) => {
|
|
137
|
+
if (!vars) return [];
|
|
138
|
+
return Object.entries(vars).reduce((acc, [key, value]) => {
|
|
139
|
+
const result = sanitizeCssValue(String(value));
|
|
140
|
+
if (!result.ok) return acc;
|
|
141
|
+
const cssVar = key.startsWith("--") ? key : `--${key}`;
|
|
142
|
+
acc.push(`${cssVar}: ${result.value};`);
|
|
143
|
+
return acc;
|
|
144
|
+
}, []);
|
|
145
|
+
};
|
|
146
|
+
var indent = (lines, level = 1) => {
|
|
147
|
+
const pad = " ".repeat(level);
|
|
148
|
+
return lines.map((l) => `${pad}${l}`).join("\n");
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
// node_modules/@sorb/core/src/index.js
|
|
152
|
+
var TIERS = Object.freeze(["component", "semantic", "primitive"]);
|
|
153
|
+
var TIER_RANK = Object.freeze({ component: 0, semantic: 1, primitive: 2 });
|
|
154
|
+
var connectors = Object.freeze({
|
|
155
|
+
source: /* @__PURE__ */ new Map(),
|
|
156
|
+
codeSource: /* @__PURE__ */ new Map(),
|
|
157
|
+
target: /* @__PURE__ */ new Map()
|
|
158
|
+
});
|
|
159
|
+
function registerTarget(adapter) {
|
|
160
|
+
connectors.target.set(adapter.id, adapter);
|
|
161
|
+
return adapter;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// src/targets/reactBootstrap.js
|
|
165
|
+
var SORB_TOKENSET_FORMAT_ID = "sorb/tokenset-esm";
|
|
166
|
+
var reactBootstrapTarget = {
|
|
167
|
+
id: "react-bootstrap",
|
|
168
|
+
emitFormat: SORB_TOKENSET_FORMAT_ID,
|
|
169
|
+
// The Bootstrap-styled vocab namespace (matches `sorb-demo/src/sorbConfig.js`'s
|
|
170
|
+
// `preview.expectPrefixes: ['bs-']`).
|
|
171
|
+
expectPrefixes: ["bs-"],
|
|
172
|
+
// Left undefined on purpose — see file header.
|
|
173
|
+
inject: void 0,
|
|
174
|
+
// Bootstrap 5.3's native dark-mode convention (real-dark-mode spec D1): a
|
|
175
|
+
// `data-bs-theme` attribute on any ancestor (Bootstrap recommends
|
|
176
|
+
// `<html>`) selects the mode; absent ⇒ OS `prefers-color-scheme` governs.
|
|
177
|
+
darkMode: {
|
|
178
|
+
strategy: "attribute",
|
|
179
|
+
attribute: "data-bs-theme",
|
|
180
|
+
darkSelector: '[data-bs-theme="dark"]',
|
|
181
|
+
lightSelector: '[data-bs-theme="light"]'
|
|
182
|
+
}
|
|
183
|
+
};
|
|
184
|
+
if (typeof registerTarget === "function") {
|
|
185
|
+
registerTarget(reactBootstrapTarget);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// src/previewGuard.js
|
|
189
|
+
var DEFAULT_ORIGIN = "http://localhost:7777";
|
|
190
|
+
var isLocalhostOrigin = (origin) => {
|
|
191
|
+
let url;
|
|
192
|
+
try {
|
|
193
|
+
url = new URL(origin);
|
|
194
|
+
} catch (e) {
|
|
195
|
+
return false;
|
|
196
|
+
}
|
|
197
|
+
if (url.protocol !== "http:" && url.protocol !== "https:") return false;
|
|
198
|
+
const host = url.hostname.toLowerCase();
|
|
199
|
+
return host === "localhost" || host === "127.0.0.1" || host === "::1" || host === "[::1]";
|
|
200
|
+
};
|
|
201
|
+
var toOrigin = (value) => {
|
|
202
|
+
try {
|
|
203
|
+
return new URL(value).origin;
|
|
204
|
+
} catch (e) {
|
|
205
|
+
return null;
|
|
206
|
+
}
|
|
207
|
+
};
|
|
208
|
+
var shouldLoadPreview = (config) => {
|
|
209
|
+
const preview = config && config.preview;
|
|
210
|
+
if (!preview || preview.enabled !== true) {
|
|
211
|
+
return { allowed: false, origin: null, reason: "preview-disabled" };
|
|
212
|
+
}
|
|
213
|
+
const origin = preview.origin ?? DEFAULT_ORIGIN;
|
|
214
|
+
const normalized = toOrigin(origin);
|
|
215
|
+
if (!normalized) {
|
|
216
|
+
return { allowed: false, origin: null, reason: "malformed-origin" };
|
|
217
|
+
}
|
|
218
|
+
if (isLocalhostOrigin(origin)) {
|
|
219
|
+
return { allowed: true, origin };
|
|
220
|
+
}
|
|
221
|
+
const extra = Array.isArray(preview.allowedOrigins) ? preview.allowedOrigins : [];
|
|
222
|
+
const allowed = extra.some((entry) => toOrigin(entry) === normalized);
|
|
223
|
+
if (allowed) {
|
|
224
|
+
return { allowed: true, origin };
|
|
225
|
+
}
|
|
226
|
+
return { allowed: false, origin, reason: "origin-not-allowlisted" };
|
|
227
|
+
};
|
|
228
|
+
|
|
229
|
+
// src/previewVocab.js
|
|
230
|
+
var countMatchingPrefixes = (tokens, prefixes) => {
|
|
231
|
+
const list = Array.isArray(prefixes) ? prefixes : [];
|
|
232
|
+
return Object.keys(tokens || {}).filter((key) => list.some((p) => key.startsWith(p))).length;
|
|
233
|
+
};
|
|
234
|
+
var checkPreviewVocabulary = ({ tokens, expectPrefixes, previewId }) => {
|
|
235
|
+
if (!Array.isArray(expectPrefixes) || expectPrefixes.length === 0) return false;
|
|
236
|
+
const appliedCount = Object.keys(tokens || {}).length;
|
|
237
|
+
if (appliedCount === 0) return false;
|
|
238
|
+
const matched = countMatchingPrefixes(tokens, expectPrefixes);
|
|
239
|
+
if (matched > 0) return false;
|
|
240
|
+
try {
|
|
241
|
+
console.warn(
|
|
242
|
+
`[Sorb] preview ${previewId ? `"${previewId}" ` : ""}applied ${appliedCount} tokens but none match expected prefixes ${JSON.stringify(expectPrefixes)} \u2014 the app may not visibly re-skin (token-vocabulary mismatch).`
|
|
243
|
+
);
|
|
244
|
+
} catch (e) {
|
|
245
|
+
}
|
|
246
|
+
return true;
|
|
247
|
+
};
|
|
248
|
+
|
|
249
|
+
// src/bridgeAuth.js
|
|
250
|
+
var bridgeHeaders = (key, base) => {
|
|
251
|
+
const headers = base ? { ...base } : {};
|
|
252
|
+
if (typeof key === "string" && key.trim() !== "") {
|
|
253
|
+
headers.Authorization = `Bearer ${key.trim()}`;
|
|
254
|
+
}
|
|
255
|
+
return headers;
|
|
256
|
+
};
|
|
257
|
+
|
|
258
|
+
// src/connection.js
|
|
259
|
+
var DEFAULT_CLOUD_BASE = "https://api.sorbcloud.com";
|
|
260
|
+
var getOrgKey = (config) => {
|
|
261
|
+
if (!config) return null;
|
|
262
|
+
const key = config.orgKey || config.publishableKey;
|
|
263
|
+
return typeof key === "string" && key.trim() !== "" ? key.trim() : null;
|
|
264
|
+
};
|
|
265
|
+
var shouldResolveOrgConnection = (config) => {
|
|
266
|
+
const key = getOrgKey(config);
|
|
267
|
+
if (!key) return false;
|
|
268
|
+
const explicitOrigin = config && config.preview && config.preview.origin;
|
|
269
|
+
return !(typeof explicitOrigin === "string" && explicitOrigin.trim() !== "");
|
|
270
|
+
};
|
|
271
|
+
var resolveOrgConnection = async (orgKey, opts) => {
|
|
272
|
+
const { cloudBase = DEFAULT_CLOUD_BASE, fetchImpl } = opts || {};
|
|
273
|
+
const doFetch = fetchImpl || (typeof fetch !== "undefined" ? fetch : null);
|
|
274
|
+
if (!doFetch || typeof orgKey !== "string" || orgKey.trim() === "") return null;
|
|
275
|
+
try {
|
|
276
|
+
const base = cloudBase.replace(/\/$/, "");
|
|
277
|
+
const url = `${base}/api/orgs/resolve?key=${encodeURIComponent(orgKey.trim())}`;
|
|
278
|
+
const res = await doFetch(url);
|
|
279
|
+
if (!res || !res.ok) return null;
|
|
280
|
+
const data = await res.json();
|
|
281
|
+
if (!data || typeof data !== "object") return null;
|
|
282
|
+
if (typeof data.bridgeUrl !== "string" || data.bridgeUrl.trim() === "") return null;
|
|
283
|
+
const bridgeMode = typeof data.bridgeMode === "string" ? data.bridgeMode : "C";
|
|
284
|
+
return {
|
|
285
|
+
bridgeMode,
|
|
286
|
+
bridgeUrl: data.bridgeUrl,
|
|
287
|
+
orgId: typeof data.orgId === "string" ? data.orgId : null,
|
|
288
|
+
tokenSource: typeof data.tokenSource === "string" ? data.tokenSource : null,
|
|
289
|
+
// sorb-cloud's /api/orgs/resolve returns this as a boolean (entitlement
|
|
290
|
+
// flag), not an object — see cloud src/lib/orgResolve.ts.
|
|
291
|
+
previewPersistence: typeof data.previewPersistence === "boolean" ? data.previewPersistence : null,
|
|
292
|
+
transport: data.transport === "poll" || data.transport === "sse" ? data.transport : bridgeMode === "A" ? "sse" : "poll"
|
|
293
|
+
};
|
|
294
|
+
} catch (e) {
|
|
295
|
+
return null;
|
|
296
|
+
}
|
|
297
|
+
};
|
|
298
|
+
var buildEffectivePreviewConfig = (config, resolved) => {
|
|
299
|
+
const base = config && config.preview || {};
|
|
300
|
+
if (!resolved) return base;
|
|
301
|
+
const orgKey = getOrgKey(config);
|
|
302
|
+
return {
|
|
303
|
+
...base,
|
|
304
|
+
enabled: true,
|
|
305
|
+
origin: resolved.bridgeUrl,
|
|
306
|
+
allowedOrigins: [...Array.isArray(base.allowedOrigins) ? base.allowedOrigins : [], resolved.bridgeUrl],
|
|
307
|
+
key: base.key || orgKey || void 0
|
|
308
|
+
};
|
|
309
|
+
};
|
|
310
|
+
var buildEffectiveConfig = (config, resolved) => {
|
|
311
|
+
if (!resolved) return config;
|
|
312
|
+
return { ...config, preview: buildEffectivePreviewConfig(config, resolved) };
|
|
313
|
+
};
|
|
314
|
+
|
|
315
|
+
// src/sse.js
|
|
316
|
+
var buildSubscribeUrl = (bridgeUrl, orgId, previewId, key) => {
|
|
317
|
+
const base = String(bridgeUrl).replace(/\/$/, "");
|
|
318
|
+
const path = `${base}/orgs/${encodeURIComponent(orgId)}/preview/${encodeURIComponent(previewId)}/subscribe`;
|
|
319
|
+
if (typeof key === "string" && key.trim() !== "") {
|
|
320
|
+
return `${path}?key=${encodeURIComponent(key.trim())}`;
|
|
321
|
+
}
|
|
322
|
+
return path;
|
|
323
|
+
};
|
|
324
|
+
var parsePreviewFrame = (raw) => {
|
|
325
|
+
let frame;
|
|
326
|
+
try {
|
|
327
|
+
frame = JSON.parse(raw);
|
|
328
|
+
} catch (e) {
|
|
329
|
+
return null;
|
|
330
|
+
}
|
|
331
|
+
if (!frame || typeof frame !== "object") return null;
|
|
332
|
+
if (frame.type === "ping") return { type: "ping" };
|
|
333
|
+
if (frame.type === "delete") return { type: "delete", tokens: null };
|
|
334
|
+
if ((frame.type === "snapshot" || frame.type === "update") && frame.tokens && typeof frame.tokens === "object") {
|
|
335
|
+
return { type: frame.type, tokens: frame.tokens };
|
|
336
|
+
}
|
|
337
|
+
return null;
|
|
338
|
+
};
|
|
339
|
+
var createPreviewSubscription = ({ EventSourceImpl, url, onTokens, onDelete, onError }) => {
|
|
340
|
+
if (typeof EventSourceImpl !== "function") return null;
|
|
341
|
+
const es = new EventSourceImpl(url);
|
|
342
|
+
es.onmessage = (evt) => {
|
|
343
|
+
const parsed = parsePreviewFrame(evt && evt.data);
|
|
344
|
+
if (!parsed || parsed.type === "ping") return;
|
|
345
|
+
if (parsed.type === "delete") {
|
|
346
|
+
if (onDelete) onDelete();
|
|
347
|
+
return;
|
|
348
|
+
}
|
|
349
|
+
onTokens(parsed.tokens);
|
|
350
|
+
};
|
|
351
|
+
const handleError = (evt) => {
|
|
352
|
+
if (onError) onError(evt);
|
|
353
|
+
};
|
|
354
|
+
if (typeof es.addEventListener === "function") {
|
|
355
|
+
es.addEventListener("error", handleError);
|
|
356
|
+
} else {
|
|
357
|
+
es.onerror = handleError;
|
|
358
|
+
}
|
|
359
|
+
return () => {
|
|
360
|
+
try {
|
|
361
|
+
es.close();
|
|
362
|
+
} catch (e) {
|
|
363
|
+
}
|
|
364
|
+
};
|
|
365
|
+
};
|
|
366
|
+
|
|
367
|
+
// src/previewMode.js
|
|
368
|
+
var isModeAwarePreviewBody = (body) => !!body && typeof body === "object" && !Array.isArray(body) && "tokens" in body;
|
|
369
|
+
var resolvePreviewBody = (body, fallbackDarkMode) => {
|
|
370
|
+
if (!isModeAwarePreviewBody(body)) {
|
|
371
|
+
return { kind: "flat", tokens: (
|
|
372
|
+
/** @type {import('./types').TokenSet} */
|
|
373
|
+
body
|
|
374
|
+
) };
|
|
375
|
+
}
|
|
376
|
+
const wrapper = (
|
|
377
|
+
/** @type {{ tokens: import('./types').TokenSet, darkTokens?: import('./types').TokenSet, darkMode?: import('@sorb/core').DarkModeConvention }} */
|
|
378
|
+
body
|
|
379
|
+
);
|
|
380
|
+
const darkTokens = wrapper.darkTokens;
|
|
381
|
+
if (darkTokens && Object.keys(darkTokens).length > 0) {
|
|
382
|
+
return {
|
|
383
|
+
kind: "mode-aware",
|
|
384
|
+
lightTokens: wrapper.tokens,
|
|
385
|
+
darkTokens,
|
|
386
|
+
darkMode: wrapper.darkMode ?? fallbackDarkMode
|
|
387
|
+
};
|
|
388
|
+
}
|
|
389
|
+
return { kind: "flat", tokens: wrapper.tokens };
|
|
390
|
+
};
|
|
391
|
+
|
|
392
|
+
// src/modeAction.js
|
|
393
|
+
var darkClassName = (darkModeConvention) => {
|
|
394
|
+
const selector = String(darkModeConvention?.darkSelector || "").trim();
|
|
395
|
+
const match = /^\.([a-zA-Z0-9_-]+)$/.exec(selector);
|
|
396
|
+
return match ? match[1] : "dark";
|
|
397
|
+
};
|
|
398
|
+
var resolveModeAction = (darkModeConvention, next) => {
|
|
399
|
+
const strategy = darkModeConvention?.strategy || "attribute";
|
|
400
|
+
if (strategy === "media") {
|
|
401
|
+
return { type: "none" };
|
|
402
|
+
}
|
|
403
|
+
if (strategy === "class") {
|
|
404
|
+
const className = darkClassName(darkModeConvention);
|
|
405
|
+
return next === "dark" ? { type: "class-add", className } : { type: "class-remove", className };
|
|
406
|
+
}
|
|
407
|
+
const attribute = darkModeConvention?.attribute || "data-bs-theme";
|
|
408
|
+
return next === "auto" ? { type: "attr-remove", attribute } : { type: "attr-set", attribute, value: next };
|
|
409
|
+
};
|
|
410
|
+
|
|
411
|
+
// src/core.js
|
|
412
|
+
var EventSourceCtor = typeof EventSource !== "undefined" ? EventSource : null;
|
|
413
|
+
var matchMediaFn = typeof matchMedia !== "undefined" ? matchMedia : null;
|
|
414
|
+
var DARK_MEDIA_QUERY = "(prefers-color-scheme: dark)";
|
|
415
|
+
var devWarn = (msg) => {
|
|
416
|
+
try {
|
|
417
|
+
if (typeof process !== "undefined" && process.env && true) {
|
|
418
|
+
console.warn(`[sorb] ${msg}`);
|
|
419
|
+
}
|
|
420
|
+
} catch (e) {
|
|
421
|
+
}
|
|
422
|
+
};
|
|
423
|
+
var warnedDeprecations = /* @__PURE__ */ new Set();
|
|
424
|
+
function warnDeprecated(resolved) {
|
|
425
|
+
if (typeof process !== "undefined" && false) return;
|
|
426
|
+
for (let i = 0; i < resolved.length; i++) {
|
|
427
|
+
const token = resolved[i];
|
|
428
|
+
if (!token.deprecated) continue;
|
|
429
|
+
if (warnedDeprecations.has(token.id)) continue;
|
|
430
|
+
warnedDeprecations.add(token.id);
|
|
431
|
+
const replacedBy = token.replacedBy || token.$extensions && token.$extensions.sorb && token.$extensions.sorb.replacedBy || null;
|
|
432
|
+
if (replacedBy) {
|
|
433
|
+
console.warn("[@sorb/leaf] Deprecated token: " + token.id + " \u2014 use " + replacedBy + " instead");
|
|
434
|
+
} else {
|
|
435
|
+
console.warn("[@sorb/leaf] Deprecated token: " + token.id + " is deprecated");
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
function sorbInit(config) {
|
|
440
|
+
let activeTokens = config.tokens;
|
|
441
|
+
let isPreview = false;
|
|
442
|
+
let previewId = null;
|
|
443
|
+
let previewMismatch = false;
|
|
444
|
+
let pollId = null;
|
|
445
|
+
let cancelled = false;
|
|
446
|
+
let unsubscribeSSE = null;
|
|
447
|
+
const hasDarkMode = !!(config.darkTokens && Object.keys(config.darkTokens).length > 0);
|
|
448
|
+
const darkModeConvention = config.darkModeConvention || reactBootstrapTarget.darkMode;
|
|
449
|
+
let mode = "auto";
|
|
450
|
+
let systemScheme = matchMediaFn ? matchMediaFn(DARK_MEDIA_QUERY).matches ? "dark" : "light" : "light";
|
|
451
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
452
|
+
const getState = () => ({
|
|
453
|
+
tokens: activeTokens,
|
|
454
|
+
isPreview,
|
|
455
|
+
previewId,
|
|
456
|
+
previewMismatch,
|
|
457
|
+
mode,
|
|
458
|
+
resolvedScheme: mode === "auto" ? systemScheme : mode
|
|
459
|
+
});
|
|
460
|
+
const notify = () => {
|
|
461
|
+
const state = getState();
|
|
462
|
+
listeners.forEach((listener) => listener(state));
|
|
463
|
+
};
|
|
464
|
+
const subscribe = (listener) => {
|
|
465
|
+
listeners.add(listener);
|
|
466
|
+
return () => listeners.delete(listener);
|
|
467
|
+
};
|
|
468
|
+
let mql = null;
|
|
469
|
+
const onSchemeChange = (e) => {
|
|
470
|
+
systemScheme = e.matches ? "dark" : "light";
|
|
471
|
+
notify();
|
|
472
|
+
};
|
|
473
|
+
if (matchMediaFn) {
|
|
474
|
+
mql = matchMediaFn(DARK_MEDIA_QUERY);
|
|
475
|
+
if (typeof mql.addEventListener === "function") mql.addEventListener("change", onSchemeChange);
|
|
476
|
+
else if (typeof mql.addListener === "function") mql.addListener(onSchemeChange);
|
|
477
|
+
}
|
|
478
|
+
let inlineTokens = null;
|
|
479
|
+
const applyFlat = (tokens) => {
|
|
480
|
+
clearModeStylesheet();
|
|
481
|
+
applyTokens(tokens);
|
|
482
|
+
inlineTokens = tokens;
|
|
483
|
+
};
|
|
484
|
+
const applyModeAware = (lightTokens, darkTokens, convention) => {
|
|
485
|
+
if (inlineTokens) {
|
|
486
|
+
clearTokenOverrides(inlineTokens);
|
|
487
|
+
inlineTokens = null;
|
|
488
|
+
}
|
|
489
|
+
injectModeStylesheet(buildModeStylesheet(lightTokens, darkTokens, convention));
|
|
490
|
+
};
|
|
491
|
+
const loadCommitted = () => {
|
|
492
|
+
if (hasDarkMode) {
|
|
493
|
+
applyModeAware(config.tokens, config.darkTokens, darkModeConvention);
|
|
494
|
+
} else {
|
|
495
|
+
applyFlat(config.tokens);
|
|
496
|
+
}
|
|
497
|
+
activeTokens = config.tokens;
|
|
498
|
+
isPreview = false;
|
|
499
|
+
previewId = null;
|
|
500
|
+
previewMismatch = false;
|
|
501
|
+
notify();
|
|
502
|
+
};
|
|
503
|
+
const applyPreviewTokens = (body, id, effectiveConfig) => {
|
|
504
|
+
const resolved = resolvePreviewBody(body, darkModeConvention);
|
|
505
|
+
let flatTokens;
|
|
506
|
+
if (resolved.kind === "mode-aware") {
|
|
507
|
+
applyModeAware(resolved.lightTokens, resolved.darkTokens, resolved.darkMode);
|
|
508
|
+
flatTokens = resolved.lightTokens;
|
|
509
|
+
} else {
|
|
510
|
+
applyFlat(resolved.tokens);
|
|
511
|
+
flatTokens = resolved.tokens;
|
|
512
|
+
}
|
|
513
|
+
activeTokens = flatTokens;
|
|
514
|
+
isPreview = true;
|
|
515
|
+
previewId = id;
|
|
516
|
+
previewMismatch = checkPreviewVocabulary({
|
|
517
|
+
tokens: flatTokens,
|
|
518
|
+
expectPrefixes: effectiveConfig.preview?.expectPrefixes,
|
|
519
|
+
previewId: id
|
|
520
|
+
});
|
|
521
|
+
notify();
|
|
522
|
+
};
|
|
523
|
+
const loadPreview = async (id, effectiveConfig) => {
|
|
524
|
+
const cfg = effectiveConfig || config;
|
|
525
|
+
const guard = shouldLoadPreview(cfg);
|
|
526
|
+
if (!guard.allowed) {
|
|
527
|
+
loadCommitted();
|
|
528
|
+
return false;
|
|
529
|
+
}
|
|
530
|
+
const origin = guard.origin;
|
|
531
|
+
try {
|
|
532
|
+
const res = await fetch(`${origin}/preview/${id}`, {
|
|
533
|
+
headers: bridgeHeaders(cfg.preview?.key)
|
|
534
|
+
});
|
|
535
|
+
if (!res.ok) throw new Error("preview not found");
|
|
536
|
+
const tokens = await res.json();
|
|
537
|
+
applyPreviewTokens(tokens, id, cfg);
|
|
538
|
+
return true;
|
|
539
|
+
} catch (e) {
|
|
540
|
+
loadCommitted();
|
|
541
|
+
return false;
|
|
542
|
+
}
|
|
543
|
+
};
|
|
544
|
+
const clearPreview = () => {
|
|
545
|
+
if (pollId) {
|
|
546
|
+
clearInterval(pollId);
|
|
547
|
+
pollId = null;
|
|
548
|
+
}
|
|
549
|
+
if (typeof location !== "undefined" && typeof history !== "undefined") {
|
|
550
|
+
const params = new URLSearchParams(location.search);
|
|
551
|
+
params.delete("preview");
|
|
552
|
+
const qs = params.toString();
|
|
553
|
+
history.replaceState(null, "", qs ? `?${qs}` : location.pathname);
|
|
554
|
+
}
|
|
555
|
+
loadCommitted();
|
|
556
|
+
};
|
|
557
|
+
const setMode = (next) => {
|
|
558
|
+
mode = next;
|
|
559
|
+
if (typeof document !== "undefined") {
|
|
560
|
+
const action = resolveModeAction(darkModeConvention, next);
|
|
561
|
+
switch (action.type) {
|
|
562
|
+
case "attr-set":
|
|
563
|
+
document.documentElement.setAttribute(action.attribute, action.value);
|
|
564
|
+
break;
|
|
565
|
+
case "attr-remove":
|
|
566
|
+
document.documentElement.removeAttribute(action.attribute);
|
|
567
|
+
break;
|
|
568
|
+
case "class-add":
|
|
569
|
+
document.documentElement.classList.add(action.className);
|
|
570
|
+
break;
|
|
571
|
+
case "class-remove":
|
|
572
|
+
document.documentElement.classList.remove(action.className);
|
|
573
|
+
break;
|
|
574
|
+
case "none":
|
|
575
|
+
default:
|
|
576
|
+
break;
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
notify();
|
|
580
|
+
};
|
|
581
|
+
const destroy = () => {
|
|
582
|
+
cancelled = true;
|
|
583
|
+
if (pollId) clearInterval(pollId);
|
|
584
|
+
if (unsubscribeSSE) unsubscribeSSE();
|
|
585
|
+
if (mql) {
|
|
586
|
+
if (typeof mql.removeEventListener === "function") mql.removeEventListener("change", onSchemeChange);
|
|
587
|
+
else if (typeof mql.removeListener === "function") mql.removeListener(onSchemeChange);
|
|
588
|
+
}
|
|
589
|
+
listeners.clear();
|
|
590
|
+
};
|
|
591
|
+
const init = async () => {
|
|
592
|
+
if (config.resolved && config.resolved.length) warnDeprecated(config.resolved);
|
|
593
|
+
let effectiveConfig = config;
|
|
594
|
+
let resolvedConnection = null;
|
|
595
|
+
if (shouldResolveOrgConnection(config)) {
|
|
596
|
+
resolvedConnection = await resolveOrgConnection(getOrgKey(config), {
|
|
597
|
+
cloudBase: config.cloudBase
|
|
598
|
+
});
|
|
599
|
+
if (cancelled) return;
|
|
600
|
+
effectiveConfig = buildEffectiveConfig(config, resolvedConnection);
|
|
601
|
+
}
|
|
602
|
+
const guard = shouldLoadPreview(effectiveConfig);
|
|
603
|
+
const id = typeof location !== "undefined" ? new URLSearchParams(location.search).get("preview") : null;
|
|
604
|
+
if (!guard.allowed || !id) {
|
|
605
|
+
if (id && !guard.allowed) {
|
|
606
|
+
devWarn(
|
|
607
|
+
`ignoring ?preview= \u2014 preview not permitted (${guard.reason ?? "blocked"}); loading committed tokens`
|
|
608
|
+
);
|
|
609
|
+
}
|
|
610
|
+
loadCommitted();
|
|
611
|
+
return;
|
|
612
|
+
}
|
|
613
|
+
const ok = await loadPreview(id, effectiveConfig);
|
|
614
|
+
if (!ok || cancelled) return;
|
|
615
|
+
const useSSE = resolvedConnection && resolvedConnection.transport === "sse" && resolvedConnection.orgId && EventSourceCtor;
|
|
616
|
+
if (useSSE) {
|
|
617
|
+
const url = buildSubscribeUrl(
|
|
618
|
+
resolvedConnection.bridgeUrl,
|
|
619
|
+
resolvedConnection.orgId,
|
|
620
|
+
id,
|
|
621
|
+
effectiveConfig.preview?.key
|
|
622
|
+
);
|
|
623
|
+
unsubscribeSSE = createPreviewSubscription({
|
|
624
|
+
EventSourceImpl: EventSourceCtor,
|
|
625
|
+
url,
|
|
626
|
+
onTokens: (tokens) => applyPreviewTokens(tokens, id, effectiveConfig),
|
|
627
|
+
onDelete: () => loadCommitted(),
|
|
628
|
+
onError: () => devWarn("SSE preview subscription error \u2014 preview may be stale")
|
|
629
|
+
});
|
|
630
|
+
}
|
|
631
|
+
if (!unsubscribeSSE) {
|
|
632
|
+
const interval = effectiveConfig.preview?.pollInterval ?? 1500;
|
|
633
|
+
pollId = setInterval(() => loadPreview(id, effectiveConfig), interval);
|
|
634
|
+
}
|
|
635
|
+
};
|
|
636
|
+
init();
|
|
637
|
+
return { getState, subscribe, setMode, clearPreview, destroy };
|
|
638
|
+
}
|
|
639
|
+
export {
|
|
640
|
+
sorbInit,
|
|
641
|
+
warnedDeprecations
|
|
642
|
+
};
|
|
643
|
+
//# sourceMappingURL=core.mjs.map
|