@stnd/lab 0.2.0 → 1.0.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/Lab.svelte +1809 -0
- package/README.md +75 -0
- package/{lab.js → old.standard.lab.js} +342 -125
- package/package.json +18 -3
- package/CHANGELOG.md +0 -7
package/Lab.svelte
ADDED
|
@@ -0,0 +1,1809 @@
|
|
|
1
|
+
<script>
|
|
2
|
+
/**
|
|
3
|
+
* @stnd/lab — The Design Lab
|
|
4
|
+
*
|
|
5
|
+
* Svelte 5 island: full-height side panel for live-editing
|
|
6
|
+
* the seed-first color system, typography, and scale tokens.
|
|
7
|
+
* Inspired by https://rsms.me/inter/lab/
|
|
8
|
+
*
|
|
9
|
+
* Integration: <Lab client:load /> inside a transition:persist zone.
|
|
10
|
+
* Keyboard: ⇧⌘S toggle · ⇧⌘D debug · Esc close
|
|
11
|
+
*/
|
|
12
|
+
import { onMount } from "svelte";
|
|
13
|
+
import { fly, slide } from "@stnd/ui/transitions.js";
|
|
14
|
+
import bezier from "bezier-easing";
|
|
15
|
+
import { modules as loadedModules } from "virtual:stnd/modules-metadata";
|
|
16
|
+
import { confirm } from "@stnd/ui/dialog.js";
|
|
17
|
+
import Combobox from "../ui/Combobox.svelte";
|
|
18
|
+
|
|
19
|
+
// Sidebar transition parameters (defaults)
|
|
20
|
+
let sidebarDuration = $state(2000); // milliseconds
|
|
21
|
+
let customEasing = $state(bezier(0.22, 1, 0.36, 1));
|
|
22
|
+
|
|
23
|
+
// =========================================================================
|
|
24
|
+
// MODULE METADATA — dynamic theme & font discovery
|
|
25
|
+
// =========================================================================
|
|
26
|
+
const THEMES = [
|
|
27
|
+
{ value: "", label: "None" },
|
|
28
|
+
{ value: "default", label: "Default" },
|
|
29
|
+
...loadedModules
|
|
30
|
+
.filter((m) => m.meta?.type === "theme")
|
|
31
|
+
.map((m) => ({ value: m.meta.themeId, label: m.meta.label }))
|
|
32
|
+
.sort((a, b) => a.label.localeCompare(b.label)),
|
|
33
|
+
];
|
|
34
|
+
|
|
35
|
+
const FONTS = loadedModules
|
|
36
|
+
.filter((m) => m.meta?.type === "font")
|
|
37
|
+
.map((m) => m.meta.label)
|
|
38
|
+
.sort((a, b) => a.localeCompare(b));
|
|
39
|
+
|
|
40
|
+
// =========================================================================
|
|
41
|
+
// STATE
|
|
42
|
+
// =========================================================================
|
|
43
|
+
let view = $state("dormant"); // dormant | open
|
|
44
|
+
let closeTimer;
|
|
45
|
+
let isScrolling = $state(false);
|
|
46
|
+
let pinned = $state(false);
|
|
47
|
+
let scrollTimer;
|
|
48
|
+
let sidebarEl = $state();
|
|
49
|
+
|
|
50
|
+
// Font dropdown state: tracks which font field is showing the picker
|
|
51
|
+
let fontDropdown = $state(null); // null | "--font-text" | "--font-header" | ...
|
|
52
|
+
let fontSearch = $state("");
|
|
53
|
+
let fontHighlight = $state(-1);
|
|
54
|
+
|
|
55
|
+
let filteredFonts = $derived(
|
|
56
|
+
fontSearch.trim() === ""
|
|
57
|
+
? FONTS
|
|
58
|
+
: FONTS.filter((f) => f.toLowerCase().includes(fontSearch.toLowerCase())),
|
|
59
|
+
);
|
|
60
|
+
|
|
61
|
+
function openFontDropdown(token) {
|
|
62
|
+
fontDropdown = token;
|
|
63
|
+
fontSearch = "";
|
|
64
|
+
fontHighlight = -1;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function closeFontDropdown() {
|
|
68
|
+
fontDropdown = null;
|
|
69
|
+
fontSearch = "";
|
|
70
|
+
fontHighlight = -1;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function selectFont(token, name) {
|
|
74
|
+
const val =
|
|
75
|
+
name.includes(" ") && !name.startsWith('"') ? `"${name}"` : name;
|
|
76
|
+
writeVar(token, val);
|
|
77
|
+
loadGoogleFont(name);
|
|
78
|
+
if (token === "--font-text") fontText = val;
|
|
79
|
+
else if (token === "--font-header") fontHeader = val;
|
|
80
|
+
else if (token === "--font-interface") fontInterface = val;
|
|
81
|
+
else if (token === "--font-monospace") fontMono = val;
|
|
82
|
+
closeFontDropdown();
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function onFontKeydown(token, e) {
|
|
86
|
+
if (e.key === "ArrowDown") {
|
|
87
|
+
e.preventDefault();
|
|
88
|
+
fontHighlight = Math.min(fontHighlight + 1, filteredFonts.length - 1);
|
|
89
|
+
} else if (e.key === "ArrowUp") {
|
|
90
|
+
e.preventDefault();
|
|
91
|
+
fontHighlight = Math.max(fontHighlight - 1, -1);
|
|
92
|
+
} else if (e.key === "Enter" && fontHighlight >= 0) {
|
|
93
|
+
e.preventDefault();
|
|
94
|
+
selectFont(token, filteredFonts[fontHighlight]);
|
|
95
|
+
} else if (e.key === "Escape") {
|
|
96
|
+
closeFontDropdown();
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function openLab() {
|
|
101
|
+
clearTimeout(closeTimer);
|
|
102
|
+
view = "open";
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function requestCloseLab() {
|
|
106
|
+
if (pinned) return;
|
|
107
|
+
// Keep open if user is interacting with inputs inside
|
|
108
|
+
if (
|
|
109
|
+
sidebarEl &&
|
|
110
|
+
typeof document !== "undefined" &&
|
|
111
|
+
sidebarEl.contains(document.activeElement)
|
|
112
|
+
) {
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
clearTimeout(closeTimer);
|
|
117
|
+
closeTimer = setTimeout(() => {
|
|
118
|
+
view = "dormant";
|
|
119
|
+
}, 100); // Increased delay for smoother transitions
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function cancelCloseLab() {
|
|
123
|
+
clearTimeout(closeTimer);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
let modified = $state(new Set());
|
|
127
|
+
let version = $state("");
|
|
128
|
+
let viewportW = $state(0);
|
|
129
|
+
|
|
130
|
+
// Collapsible sections
|
|
131
|
+
let sectionsOpen = $state({
|
|
132
|
+
theme: false,
|
|
133
|
+
colors: false,
|
|
134
|
+
typography: false,
|
|
135
|
+
scale: false,
|
|
136
|
+
spacing: false,
|
|
137
|
+
debug: false,
|
|
138
|
+
links: false,
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
// Color scheme detection
|
|
142
|
+
let systemScheme = $state(
|
|
143
|
+
typeof window !== "undefined" &&
|
|
144
|
+
window.matchMedia?.("(prefers-color-scheme: dark)").matches
|
|
145
|
+
? "dark"
|
|
146
|
+
: "light",
|
|
147
|
+
);
|
|
148
|
+
let userScheme = $state("auto");
|
|
149
|
+
let scheme = $derived(userScheme === "auto" ? systemScheme : userScheme);
|
|
150
|
+
|
|
151
|
+
$effect(() => {
|
|
152
|
+
if (typeof sessionStorage !== "undefined") {
|
|
153
|
+
sessionStorage.setItem("stnd-scheme", userScheme);
|
|
154
|
+
sessionStorage.setItem("stnd-pinned", String(pinned));
|
|
155
|
+
sessionStorage.setItem("stnd-sections", JSON.stringify(sectionsOpen));
|
|
156
|
+
}
|
|
157
|
+
if (typeof document !== "undefined") {
|
|
158
|
+
if (userScheme === "auto") {
|
|
159
|
+
document.documentElement.removeAttribute("data-lab-scheme");
|
|
160
|
+
} else {
|
|
161
|
+
document.documentElement.setAttribute("data-lab-scheme", userScheme);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
});
|
|
165
|
+
let bgToken = $derived(
|
|
166
|
+
scheme === "dark" ? "--color-dark-background" : "--color-light-background",
|
|
167
|
+
);
|
|
168
|
+
let fgToken = $derived(
|
|
169
|
+
scheme === "dark" ? "--color-dark-foreground" : "--color-light-foreground",
|
|
170
|
+
);
|
|
171
|
+
|
|
172
|
+
// Quick panel seeds (scheme-aware)
|
|
173
|
+
let seedBg = $state("");
|
|
174
|
+
let seedFg = $state("");
|
|
175
|
+
let seedBgHex = $state("#ffffff");
|
|
176
|
+
let seedFgHex = $state("#262626");
|
|
177
|
+
let fontText = $state("");
|
|
178
|
+
let fontHeader = $state("");
|
|
179
|
+
let fontInterface = $state("");
|
|
180
|
+
let fontMono = $state("");
|
|
181
|
+
let fontSize = $state(1);
|
|
182
|
+
let lineHeight = $state(1.414);
|
|
183
|
+
let rhythmGap = $state(1);
|
|
184
|
+
let ratio = $state(1.414);
|
|
185
|
+
let gapBody = $state(1);
|
|
186
|
+
let radius = $state(0);
|
|
187
|
+
let debug = $state(false);
|
|
188
|
+
let eink = $state(false);
|
|
189
|
+
let ink = $state(false);
|
|
190
|
+
let texture = $state(false);
|
|
191
|
+
let stnd1 = $state(false);
|
|
192
|
+
let stnd2 = $state(false);
|
|
193
|
+
let stnd3 = $state(false);
|
|
194
|
+
let theme = $state("");
|
|
195
|
+
|
|
196
|
+
// Google Fonts tracking
|
|
197
|
+
const fontsLoaded = new Set();
|
|
198
|
+
|
|
199
|
+
// Stored defaults for reset
|
|
200
|
+
let defaults = {};
|
|
201
|
+
|
|
202
|
+
// =========================================================================
|
|
203
|
+
// HELPERS
|
|
204
|
+
// =========================================================================
|
|
205
|
+
|
|
206
|
+
/** Read a CSS custom property, resolving one level of var() */
|
|
207
|
+
function readVar(token) {
|
|
208
|
+
const raw = getComputedStyle(document.documentElement)
|
|
209
|
+
.getPropertyValue(token)
|
|
210
|
+
.trim();
|
|
211
|
+
if (!raw) return "";
|
|
212
|
+
const m = raw.match(/^var\(--([\w-]+)\)$/);
|
|
213
|
+
return m
|
|
214
|
+
? getComputedStyle(document.documentElement)
|
|
215
|
+
.getPropertyValue(`--${m[1]}`)
|
|
216
|
+
.trim()
|
|
217
|
+
: raw;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/** Write an inline override and mark as modified */
|
|
221
|
+
function writeVar(token, value) {
|
|
222
|
+
document.documentElement.style.setProperty(token, value);
|
|
223
|
+
modified = new Set([...modified, token]);
|
|
224
|
+
|
|
225
|
+
// Persist to session
|
|
226
|
+
try {
|
|
227
|
+
const overrides = JSON.parse(
|
|
228
|
+
sessionStorage.getItem("stnd-overrides") || "{}",
|
|
229
|
+
);
|
|
230
|
+
overrides[token] = value;
|
|
231
|
+
sessionStorage.setItem("stnd-overrides", JSON.stringify(overrides));
|
|
232
|
+
} catch {
|
|
233
|
+
/* noop */
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/** Convert any CSS color to #hex for <input type="color"> */
|
|
238
|
+
function toHex(css) {
|
|
239
|
+
if (!css) return "#000000";
|
|
240
|
+
if (/^#[0-9a-f]{6}$/i.test(css.trim())) return css.trim();
|
|
241
|
+
if (css.trim().toLowerCase() === "white") return "#ffffff";
|
|
242
|
+
if (css.trim().toLowerCase() === "black") return "#000000";
|
|
243
|
+
|
|
244
|
+
// Use a temp element to resolve any CSS color format
|
|
245
|
+
try {
|
|
246
|
+
const el = document.createElement("div");
|
|
247
|
+
el.style.color = css;
|
|
248
|
+
document.body.appendChild(el);
|
|
249
|
+
const resolved = getComputedStyle(el).color;
|
|
250
|
+
el.remove();
|
|
251
|
+
const m = resolved.match(/(\d+)/g);
|
|
252
|
+
if (m && m.length >= 3) {
|
|
253
|
+
return (
|
|
254
|
+
"#" +
|
|
255
|
+
m
|
|
256
|
+
.slice(0, 3)
|
|
257
|
+
.map((n) => parseInt(n).toString(16).padStart(2, "0"))
|
|
258
|
+
.join("")
|
|
259
|
+
);
|
|
260
|
+
}
|
|
261
|
+
} catch {
|
|
262
|
+
/* noop */
|
|
263
|
+
}
|
|
264
|
+
return "#000000";
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/** Resolve a CSS length (var, calc, unit) to pixels */
|
|
268
|
+
function resolvePx(value) {
|
|
269
|
+
if (typeof document === "undefined" || !value) return 0;
|
|
270
|
+
try {
|
|
271
|
+
const el = document.createElement("div");
|
|
272
|
+
el.style.position = "absolute";
|
|
273
|
+
el.style.visibility = "hidden";
|
|
274
|
+
el.style.width = value;
|
|
275
|
+
document.body.appendChild(el);
|
|
276
|
+
const px = parseFloat(getComputedStyle(el).width);
|
|
277
|
+
el.remove();
|
|
278
|
+
return isNaN(px) ? 0 : px;
|
|
279
|
+
} catch {
|
|
280
|
+
return 0;
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/** Read all token values from the current DOM cascade */
|
|
285
|
+
function syncFromDOM() {
|
|
286
|
+
// Detect current scheme
|
|
287
|
+
systemScheme = window.matchMedia?.("(prefers-color-scheme: dark)").matches
|
|
288
|
+
? "dark"
|
|
289
|
+
: "light";
|
|
290
|
+
|
|
291
|
+
// Read the active scheme's seeds
|
|
292
|
+
seedBg = readVar(bgToken);
|
|
293
|
+
seedFg = readVar(fgToken);
|
|
294
|
+
seedBgHex = toHex(seedBg);
|
|
295
|
+
seedFgHex = toHex(seedFg);
|
|
296
|
+
fontText = readVar("--font-text");
|
|
297
|
+
fontHeader = readVar("--font-header");
|
|
298
|
+
fontInterface = readVar("--font-interface");
|
|
299
|
+
fontMono = readVar("--font-monospace");
|
|
300
|
+
fontSize = parseFloat(readVar("--font-size")) || 1;
|
|
301
|
+
lineHeight = parseFloat(readVar("--line-height")) || 1.414;
|
|
302
|
+
ratio = parseFloat(readVar("--optical-ratio")) || 1.414;
|
|
303
|
+
|
|
304
|
+
const rawGap = readVar("--gap-body");
|
|
305
|
+
const pxSpace = resolvePx("var(--space)");
|
|
306
|
+
|
|
307
|
+
// Calculate body gap multiplier
|
|
308
|
+
const pxGap = resolvePx(rawGap);
|
|
309
|
+
gapBody = pxSpace ? pxGap / pxSpace : 1;
|
|
310
|
+
|
|
311
|
+
// Calculate rhythm gap multiplier
|
|
312
|
+
const pxRhythm = resolvePx("var(--rhythm-gap-new-line)");
|
|
313
|
+
rhythmGap = pxSpace ? pxRhythm / pxSpace : 0.5;
|
|
314
|
+
|
|
315
|
+
// Calculate radius multiplier
|
|
316
|
+
const rawRadius = readVar("--radius");
|
|
317
|
+
const pxRadius = resolvePx(rawRadius);
|
|
318
|
+
radius = pxSpace ? pxRadius / pxSpace : 0;
|
|
319
|
+
|
|
320
|
+
debug = document.documentElement.classList.contains("stnd-debug");
|
|
321
|
+
eink = document.documentElement.classList.contains("standard-eink");
|
|
322
|
+
theme = document.documentElement.getAttribute("data-theme") || "";
|
|
323
|
+
viewportW = window.innerWidth;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
/** Store current computed values as defaults for reset */
|
|
327
|
+
function storeDefaults() {
|
|
328
|
+
const tokens = [
|
|
329
|
+
"--color-light-background",
|
|
330
|
+
"--color-light-foreground",
|
|
331
|
+
"--color-dark-background",
|
|
332
|
+
"--color-dark-foreground",
|
|
333
|
+
"--font-text",
|
|
334
|
+
"--font-header",
|
|
335
|
+
"--font-interface",
|
|
336
|
+
"--font-monospace",
|
|
337
|
+
"--font-size",
|
|
338
|
+
"--line-height",
|
|
339
|
+
"--optical-ratio",
|
|
340
|
+
"--gap-body",
|
|
341
|
+
"--rhythm-gap-new-line",
|
|
342
|
+
"--radius",
|
|
343
|
+
];
|
|
344
|
+
for (const t of tokens) {
|
|
345
|
+
defaults[t] = readVar(t);
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
/** Load a Google Font by family name */
|
|
350
|
+
function loadGoogleFont(family) {
|
|
351
|
+
const clean = family.trim();
|
|
352
|
+
const system = [
|
|
353
|
+
"system-ui",
|
|
354
|
+
"sans-serif",
|
|
355
|
+
"serif",
|
|
356
|
+
"monospace",
|
|
357
|
+
"cursive",
|
|
358
|
+
"ui-monospace",
|
|
359
|
+
"ui-sans-serif",
|
|
360
|
+
"ui-serif",
|
|
361
|
+
"ui-rounded",
|
|
362
|
+
"Georgia",
|
|
363
|
+
"Times New Roman",
|
|
364
|
+
"Arial",
|
|
365
|
+
];
|
|
366
|
+
if (!clean || system.includes(clean) || fontsLoaded.has(clean)) return;
|
|
367
|
+
fontsLoaded.add(clean);
|
|
368
|
+
const encoded = encodeURIComponent(clean);
|
|
369
|
+
try {
|
|
370
|
+
const link = document.createElement("link");
|
|
371
|
+
link.rel = "stylesheet";
|
|
372
|
+
link.href = `https://fonts.googleapis.com/css2?family=${encoded}:wght@100..900&display=swap`;
|
|
373
|
+
document.head.appendChild(link);
|
|
374
|
+
} catch {
|
|
375
|
+
/* noop */
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
/** Copy current overrides as CSS to clipboard */
|
|
380
|
+
function copyCSS() {
|
|
381
|
+
const tokens = [
|
|
382
|
+
"--color-light-background",
|
|
383
|
+
"--color-light-foreground",
|
|
384
|
+
"--color-dark-background",
|
|
385
|
+
"--color-dark-foreground",
|
|
386
|
+
"--font-text",
|
|
387
|
+
"--font-header",
|
|
388
|
+
"--font-interface",
|
|
389
|
+
"--font-monospace",
|
|
390
|
+
"--font-size",
|
|
391
|
+
"--line-height",
|
|
392
|
+
"--optical-ratio",
|
|
393
|
+
"--gap-body",
|
|
394
|
+
"--rhythm-gap-new-line",
|
|
395
|
+
"--radius",
|
|
396
|
+
];
|
|
397
|
+
const css = tokens
|
|
398
|
+
.filter((t) => modified.has(t))
|
|
399
|
+
.map(
|
|
400
|
+
(t) =>
|
|
401
|
+
` ${t}: ${document.documentElement.style.getPropertyValue(t).trim()};`,
|
|
402
|
+
)
|
|
403
|
+
.join("\n");
|
|
404
|
+
if (css) {
|
|
405
|
+
navigator.clipboard?.writeText(`:root {\n${css}\n}`);
|
|
406
|
+
window.toast?.("CSS copied", "success");
|
|
407
|
+
} else {
|
|
408
|
+
window.toast?.("No overrides to copy", "info");
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
function getBreakpoint(w) {
|
|
413
|
+
if (w < 640) return "Mobile";
|
|
414
|
+
if (w < 768) return "Small";
|
|
415
|
+
if (w < 1024) return "Medium";
|
|
416
|
+
if (w < 1280) return "Large";
|
|
417
|
+
return "X-Large";
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
/** Toggle debug overlay panels */
|
|
421
|
+
function syncDebugOverlays(on) {
|
|
422
|
+
document.querySelectorAll("[data-debug-overlay]").forEach((el) => {
|
|
423
|
+
el.style.display = on ? "" : "none";
|
|
424
|
+
});
|
|
425
|
+
|
|
426
|
+
const overlay = document.getElementById("stnd-debug-overlay");
|
|
427
|
+
if (overlay) {
|
|
428
|
+
overlay.style.display = on ? "" : "none";
|
|
429
|
+
} else if (on) {
|
|
430
|
+
const div = document.createElement("div");
|
|
431
|
+
div.id = "stnd-debug-overlay";
|
|
432
|
+
div.setAttribute("data-debug-overlay", "");
|
|
433
|
+
document.body.appendChild(div);
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
// =========================================================================
|
|
438
|
+
// LIFECYCLE
|
|
439
|
+
// =========================================================================
|
|
440
|
+
onMount(() => {
|
|
441
|
+
version = document.documentElement.dataset.standardVersion || "";
|
|
442
|
+
viewportW = window.innerWidth;
|
|
443
|
+
|
|
444
|
+
// Attempt to read CSS transition variables so the Svelte fly transition
|
|
445
|
+
// matches the CSS page margin transition. Use the single trusted token
|
|
446
|
+
// `--transition` first and fall back to `--transition-slow` if not present.
|
|
447
|
+
try {
|
|
448
|
+
const cs = getComputedStyle(document.documentElement);
|
|
449
|
+
|
|
450
|
+
// Prefer --transition; fall back to --transition-slow if missing.
|
|
451
|
+
let transRaw = (cs.getPropertyValue("--transition") || "").trim();
|
|
452
|
+
if (!transRaw) {
|
|
453
|
+
transRaw = (cs.getPropertyValue("--transition-slow") || "").trim();
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
if (transRaw) {
|
|
457
|
+
// Extract duration (e.g. "1s cubic-bezier(...)" or "200ms ease")
|
|
458
|
+
const durMatch = transRaw.match(/([\d.]+)(ms|s)/);
|
|
459
|
+
if (durMatch) {
|
|
460
|
+
sidebarDuration =
|
|
461
|
+
durMatch[2] === "ms"
|
|
462
|
+
? parseFloat(durMatch[1])
|
|
463
|
+
: parseFloat(durMatch[1]) * 1000;
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
// Extract easing curve from the same variable (cubic-bezier)
|
|
467
|
+
const easeMatch = transRaw.match(/cubic-bezier\(([^)]+)\)/);
|
|
468
|
+
if (easeMatch && easeMatch[1]) {
|
|
469
|
+
const nums = easeMatch[1].split(",").map((n) => parseFloat(n.trim()));
|
|
470
|
+
if (nums.length === 4 && nums.every((v) => !Number.isNaN(v))) {
|
|
471
|
+
customEasing = bezier(nums[0], nums[1], nums[2], nums[3]);
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
} catch (err) {
|
|
476
|
+
// If anything goes wrong (SSR, missing vars, parsing), fall back to defaults.
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
// Listen for system color scheme changes
|
|
480
|
+
const mql = window.matchMedia("(prefers-color-scheme: dark)");
|
|
481
|
+
const onSchemeChange = (e) => {
|
|
482
|
+
systemScheme = e.matches ? "dark" : "light";
|
|
483
|
+
requestAnimationFrame(syncFromDOM);
|
|
484
|
+
};
|
|
485
|
+
mql.addEventListener("change", onSchemeChange);
|
|
486
|
+
|
|
487
|
+
const restoreLabState = () => {
|
|
488
|
+
if (typeof document === "undefined") return;
|
|
489
|
+
|
|
490
|
+
const savedScheme = sessionStorage.getItem("stnd-scheme");
|
|
491
|
+
if (savedScheme && ["auto", "light", "dark"].includes(savedScheme)) {
|
|
492
|
+
userScheme = savedScheme;
|
|
493
|
+
}
|
|
494
|
+
if (userScheme !== "auto") {
|
|
495
|
+
document.documentElement.setAttribute("data-lab-scheme", userScheme);
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
// Re-apply Lab theme (last word)
|
|
499
|
+
const savedTheme = sessionStorage.getItem("stnd-theme");
|
|
500
|
+
if (savedTheme !== null) {
|
|
501
|
+
if (savedTheme) {
|
|
502
|
+
document.documentElement.setAttribute("data-theme", savedTheme);
|
|
503
|
+
} else {
|
|
504
|
+
document.documentElement.removeAttribute("data-theme");
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
const savedPinned = sessionStorage.getItem("stnd-pinned");
|
|
509
|
+
if (savedPinned === "true") pinned = true;
|
|
510
|
+
|
|
511
|
+
const savedDebug = sessionStorage.getItem("stnd-debug-mode");
|
|
512
|
+
if (savedDebug === "true") {
|
|
513
|
+
document.documentElement.classList.add("stnd-debug");
|
|
514
|
+
debug = true;
|
|
515
|
+
} else {
|
|
516
|
+
document.documentElement.classList.remove("stnd-debug");
|
|
517
|
+
debug = false;
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
// Restore debug class toggles (.ink, .texture, .stnd-1/2/3)
|
|
521
|
+
const classes = [
|
|
522
|
+
{ k: "stnd-class-ink", c: "ink", s: (v) => (ink = v) },
|
|
523
|
+
{ k: "stnd-class-texture", c: "texture", s: (v) => (texture = v) },
|
|
524
|
+
{ k: "stnd-class-stnd-1", c: "stnd-1", s: (v) => (stnd1 = v) },
|
|
525
|
+
{ k: "stnd-class-stnd-2", c: "stnd-2", s: (v) => (stnd2 = v) },
|
|
526
|
+
{ k: "stnd-class-stnd-3", c: "stnd-3", s: (v) => (stnd3 = v) },
|
|
527
|
+
];
|
|
528
|
+
|
|
529
|
+
classes.forEach(({ k, c, s }) => {
|
|
530
|
+
const val = sessionStorage.getItem(k) === "true";
|
|
531
|
+
document.documentElement.classList.toggle(c, val);
|
|
532
|
+
s(val);
|
|
533
|
+
});
|
|
534
|
+
|
|
535
|
+
const savedPanel = sessionStorage.getItem("stnd-panel-state");
|
|
536
|
+
if (savedPanel === "open") view = "open";
|
|
537
|
+
|
|
538
|
+
// Restore sections
|
|
539
|
+
try {
|
|
540
|
+
const savedSections = JSON.parse(
|
|
541
|
+
sessionStorage.getItem("stnd-sections") || "{}",
|
|
542
|
+
);
|
|
543
|
+
if (Object.keys(savedSections).length > 0) {
|
|
544
|
+
sectionsOpen = { ...sectionsOpen, ...savedSections };
|
|
545
|
+
}
|
|
546
|
+
} catch {
|
|
547
|
+
/* noop */
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
// Restore overrides
|
|
551
|
+
try {
|
|
552
|
+
const savedOverrides = JSON.parse(
|
|
553
|
+
sessionStorage.getItem("stnd-overrides") || "{}",
|
|
554
|
+
);
|
|
555
|
+
if (Object.keys(savedOverrides).length > 0) {
|
|
556
|
+
for (const [token, value] of Object.entries(savedOverrides)) {
|
|
557
|
+
document.documentElement.style.setProperty(token, value);
|
|
558
|
+
}
|
|
559
|
+
modified = new Set([...Object.keys(savedOverrides)]);
|
|
560
|
+
} else {
|
|
561
|
+
modified = new Set();
|
|
562
|
+
}
|
|
563
|
+
} catch {
|
|
564
|
+
/* noop */
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
// Read current values from cascade
|
|
568
|
+
syncFromDOM();
|
|
569
|
+
storeDefaults();
|
|
570
|
+
syncDebugOverlays(
|
|
571
|
+
document.documentElement.classList.contains("stnd-debug"),
|
|
572
|
+
);
|
|
573
|
+
};
|
|
574
|
+
|
|
575
|
+
restoreLabState();
|
|
576
|
+
document.addEventListener("astro:after-swap", restoreLabState);
|
|
577
|
+
|
|
578
|
+
// Global API for dev toolbar compatibility
|
|
579
|
+
window.StandardLab = {
|
|
580
|
+
get state() {
|
|
581
|
+
return view;
|
|
582
|
+
},
|
|
583
|
+
open() {
|
|
584
|
+
view = "open";
|
|
585
|
+
},
|
|
586
|
+
close() {
|
|
587
|
+
view = "dormant";
|
|
588
|
+
},
|
|
589
|
+
toggle() {
|
|
590
|
+
view = view === "dormant" ? "open" : "dormant";
|
|
591
|
+
},
|
|
592
|
+
refresh: syncFromDOM,
|
|
593
|
+
};
|
|
594
|
+
|
|
595
|
+
// Keyboard shortcuts
|
|
596
|
+
function onKey(e) {
|
|
597
|
+
const mod = navigator.platform.includes("Mac") ? e.metaKey : e.ctrlKey;
|
|
598
|
+
|
|
599
|
+
// Toggle lab with Cmd/Ctrl+S (override default save shortcut)
|
|
600
|
+
if (mod && !e.shiftKey && e.key.toLowerCase() === "s") {
|
|
601
|
+
e.preventDefault();
|
|
602
|
+
view = view === "dormant" ? "open" : "dormant";
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
if (e.shiftKey && mod && e.key.toLowerCase() === "s") {
|
|
606
|
+
e.preventDefault();
|
|
607
|
+
view = view === "dormant" ? "open" : "dormant";
|
|
608
|
+
}
|
|
609
|
+
if (mod && !e.shiftKey && e.key.toLowerCase() === "d") {
|
|
610
|
+
e.preventDefault();
|
|
611
|
+
view = view === "dormant" ? "open" : "dormant";
|
|
612
|
+
}
|
|
613
|
+
if (e.shiftKey && mod && e.key.toLowerCase() === "d") {
|
|
614
|
+
e.preventDefault();
|
|
615
|
+
debug = !debug;
|
|
616
|
+
document.documentElement.classList.toggle("stnd-debug", debug);
|
|
617
|
+
sessionStorage.setItem("stnd-debug-mode", String(debug));
|
|
618
|
+
syncDebugOverlays(debug);
|
|
619
|
+
window.toast?.(`Debug ${debug ? "on" : "off"}`, "info");
|
|
620
|
+
}
|
|
621
|
+
if (e.key === "Escape" && view !== "dormant") {
|
|
622
|
+
view = "dormant";
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
function onResize() {
|
|
627
|
+
viewportW = window.innerWidth;
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
function onScroll() {
|
|
631
|
+
isScrolling = true;
|
|
632
|
+
clearTimeout(scrollTimer);
|
|
633
|
+
scrollTimer = setTimeout(() => {
|
|
634
|
+
isScrolling = false;
|
|
635
|
+
}, 500);
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
function onGlobalClick(e) {
|
|
639
|
+
if (view !== "open" || pinned) return;
|
|
640
|
+
if (sidebarEl && sidebarEl.contains(e.target)) return;
|
|
641
|
+
if (
|
|
642
|
+
e.target.closest(".lab-handler") ||
|
|
643
|
+
e.target.closest(".lab-handler-top")
|
|
644
|
+
)
|
|
645
|
+
return;
|
|
646
|
+
view = "dormant";
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
document.addEventListener("keydown", onKey);
|
|
650
|
+
document.addEventListener("click", onGlobalClick);
|
|
651
|
+
window.addEventListener("resize", onResize);
|
|
652
|
+
window.addEventListener("scroll", onScroll, { passive: true });
|
|
653
|
+
|
|
654
|
+
return () => {
|
|
655
|
+
document.removeEventListener("keydown", onKey);
|
|
656
|
+
document.removeEventListener("click", onGlobalClick);
|
|
657
|
+
document.removeEventListener("astro:after-swap", restoreLabState);
|
|
658
|
+
window.removeEventListener("resize", onResize);
|
|
659
|
+
window.removeEventListener("scroll", onScroll);
|
|
660
|
+
mql.removeEventListener("change", onSchemeChange);
|
|
661
|
+
delete window.StandardLab;
|
|
662
|
+
};
|
|
663
|
+
});
|
|
664
|
+
|
|
665
|
+
// =========================================================================
|
|
666
|
+
// EFFECTS
|
|
667
|
+
// =========================================================================
|
|
668
|
+
|
|
669
|
+
// Persist panel state + dispatch event for dev toolbar
|
|
670
|
+
// Shrink the page when the sidebar is open (like DevTools)
|
|
671
|
+
$effect(() => {
|
|
672
|
+
if (typeof document === "undefined") return;
|
|
673
|
+
const el = document.documentElement;
|
|
674
|
+
if (view === "open") {
|
|
675
|
+
el.classList.add("lab-open");
|
|
676
|
+
} else {
|
|
677
|
+
el.classList.remove("lab-open");
|
|
678
|
+
}
|
|
679
|
+
});
|
|
680
|
+
|
|
681
|
+
$effect(() => {
|
|
682
|
+
if (typeof window === "undefined") return;
|
|
683
|
+
if (view === "dormant") {
|
|
684
|
+
sessionStorage.removeItem("stnd-panel-state");
|
|
685
|
+
} else {
|
|
686
|
+
sessionStorage.setItem("stnd-panel-state", view);
|
|
687
|
+
}
|
|
688
|
+
try {
|
|
689
|
+
window.dispatchEvent(
|
|
690
|
+
new CustomEvent("standardlab:state", { detail: { state: view } }),
|
|
691
|
+
);
|
|
692
|
+
} catch {
|
|
693
|
+
/* SSR guard */
|
|
694
|
+
}
|
|
695
|
+
});
|
|
696
|
+
|
|
697
|
+
// =========================================================================
|
|
698
|
+
// HANDLERS — Theme
|
|
699
|
+
// =========================================================================
|
|
700
|
+
function onThemeChange(e) {
|
|
701
|
+
const t = e.target.value;
|
|
702
|
+
theme = t;
|
|
703
|
+
|
|
704
|
+
// Clear ALL inline token overrides so the new theme's cascade wins
|
|
705
|
+
const tokensToClear = [
|
|
706
|
+
"--color-light-background",
|
|
707
|
+
"--color-light-foreground",
|
|
708
|
+
"--color-dark-background",
|
|
709
|
+
"--color-dark-foreground",
|
|
710
|
+
"--font-text",
|
|
711
|
+
"--font-header",
|
|
712
|
+
"--font-interface",
|
|
713
|
+
"--font-monospace",
|
|
714
|
+
"--font-size",
|
|
715
|
+
"--line-height",
|
|
716
|
+
"--optical-ratio",
|
|
717
|
+
"--gap-body",
|
|
718
|
+
"--rhythm-gap-new-line",
|
|
719
|
+
"--radius",
|
|
720
|
+
];
|
|
721
|
+
for (const tok of tokensToClear) {
|
|
722
|
+
document.documentElement.style.removeProperty(tok);
|
|
723
|
+
}
|
|
724
|
+
modified = new Set();
|
|
725
|
+
|
|
726
|
+
if (t) {
|
|
727
|
+
document.documentElement.setAttribute("data-theme", t);
|
|
728
|
+
} else {
|
|
729
|
+
document.documentElement.removeAttribute("data-theme");
|
|
730
|
+
}
|
|
731
|
+
sessionStorage.setItem("stnd-theme", t);
|
|
732
|
+
|
|
733
|
+
// Re-read all values after cascade settles
|
|
734
|
+
requestAnimationFrame(() => {
|
|
735
|
+
syncFromDOM();
|
|
736
|
+
storeDefaults();
|
|
737
|
+
});
|
|
738
|
+
window.toast?.(`Theme: ${t || "None"}`, "info");
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
// =========================================================================
|
|
742
|
+
// HANDLERS — Seed Colors
|
|
743
|
+
// =========================================================================
|
|
744
|
+
function onColorSwatch(role, e) {
|
|
745
|
+
const hex = e.target.value;
|
|
746
|
+
const token = role === "bg" ? bgToken : fgToken;
|
|
747
|
+
if (role === "bg") {
|
|
748
|
+
seedBg = hex;
|
|
749
|
+
seedBgHex = hex;
|
|
750
|
+
} else {
|
|
751
|
+
seedFg = hex;
|
|
752
|
+
seedFgHex = hex;
|
|
753
|
+
}
|
|
754
|
+
writeVar(token, hex);
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
function onColorText(role, e) {
|
|
758
|
+
const val = e.target.value;
|
|
759
|
+
const token = role === "bg" ? bgToken : fgToken;
|
|
760
|
+
if (role === "bg") {
|
|
761
|
+
seedBg = val;
|
|
762
|
+
seedBgHex = toHex(val);
|
|
763
|
+
} else {
|
|
764
|
+
seedFg = val;
|
|
765
|
+
seedFgHex = toHex(val);
|
|
766
|
+
}
|
|
767
|
+
writeVar(token, val);
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
// =========================================================================
|
|
771
|
+
// HANDLERS — Fonts
|
|
772
|
+
// =========================================================================
|
|
773
|
+
function onFontInput(token, e) {
|
|
774
|
+
const raw = e.target.value;
|
|
775
|
+
// Wrap multi-word font names in quotes for valid CSS font-family
|
|
776
|
+
const val = raw.includes(" ") && !raw.startsWith('"') ? `"${raw}"` : raw;
|
|
777
|
+
writeVar(token, val);
|
|
778
|
+
loadGoogleFont(raw);
|
|
779
|
+
// Re-read the specific font state
|
|
780
|
+
if (token === "--font-text") fontText = val;
|
|
781
|
+
else if (token === "--font-header") fontHeader = val;
|
|
782
|
+
else if (token === "--font-interface") fontInterface = val;
|
|
783
|
+
else if (token === "--font-monospace") fontMono = val;
|
|
784
|
+
// Update search to match typed value for filtering
|
|
785
|
+
fontSearch = raw;
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
// =========================================================================
|
|
789
|
+
// HANDLERS — Scale
|
|
790
|
+
// =========================================================================
|
|
791
|
+
function onFontSizeInput(e) {
|
|
792
|
+
fontSize = parseFloat(e.target.value);
|
|
793
|
+
writeVar("--font-size", `${fontSize}rem`);
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
function onLineHeightInput(e) {
|
|
797
|
+
lineHeight = parseFloat(e.target.value);
|
|
798
|
+
writeVar("--line-height", String(lineHeight));
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
function onRatioInput(e) {
|
|
802
|
+
ratio = parseFloat(e.target.value);
|
|
803
|
+
writeVar("--optical-ratio", String(ratio));
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
function onGapInput(e) {
|
|
807
|
+
gapBody = parseFloat(e.target.value);
|
|
808
|
+
writeVar("--gap-body", `calc(var(--space) * ${gapBody})`);
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
function onRhythmGapInput(e) {
|
|
812
|
+
rhythmGap = parseFloat(e.target.value);
|
|
813
|
+
writeVar("--rhythm-gap-new-line", `calc(var(--space) * ${rhythmGap})`);
|
|
814
|
+
}
|
|
815
|
+
|
|
816
|
+
function onRadiusInput(e) {
|
|
817
|
+
radius = parseFloat(e.target.value);
|
|
818
|
+
writeVar("--radius", `calc(var(--space) * ${radius})`);
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
// =========================================================================
|
|
822
|
+
// HANDLERS — Debug
|
|
823
|
+
// =========================================================================
|
|
824
|
+
function onDebugToggle(e) {
|
|
825
|
+
debug = e.target.checked;
|
|
826
|
+
document.documentElement.classList.toggle("stnd-debug", debug);
|
|
827
|
+
sessionStorage.setItem("stnd-debug-mode", String(debug));
|
|
828
|
+
syncDebugOverlays(debug);
|
|
829
|
+
window.toast?.(`Debug ${debug ? "on" : "off"}`, "info");
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
function onEinkToggle(e) {
|
|
833
|
+
eink = e.target.checked;
|
|
834
|
+
document.documentElement.classList.toggle("standard-eink", eink);
|
|
835
|
+
// E-ink state is often auto-detected or managed by client/eink.js
|
|
836
|
+
// but here we allow manual override. We don't persist it to session
|
|
837
|
+
// to avoid conflict with auto-detection on reload, or we could if desired.
|
|
838
|
+
// For now, let's treat it as a temporary override in the Lab.
|
|
839
|
+
window.toast?.(`E-Ink Mode ${eink ? "on" : "off"}`, "info");
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
/** Toggle a global debug class and persist to sessionStorage */
|
|
843
|
+
function onClassToggle(className, storageKey, e) {
|
|
844
|
+
const checked = e.target.checked;
|
|
845
|
+
// Update corresponding local state
|
|
846
|
+
if (className === "ink") ink = checked;
|
|
847
|
+
else if (className === "texture") texture = checked;
|
|
848
|
+
else if (className === "stnd-1") stnd1 = checked;
|
|
849
|
+
else if (className === "stnd-2") stnd2 = checked;
|
|
850
|
+
else if (className === "stnd-3") stnd3 = checked;
|
|
851
|
+
|
|
852
|
+
document.documentElement.classList.toggle(className, checked);
|
|
853
|
+
try {
|
|
854
|
+
sessionStorage.setItem(storageKey, String(checked));
|
|
855
|
+
} catch {
|
|
856
|
+
/* noop */
|
|
857
|
+
}
|
|
858
|
+
window.toast?.(`${className} ${checked ? "on" : "off"}`, "info");
|
|
859
|
+
}
|
|
860
|
+
|
|
861
|
+
// =========================================================================
|
|
862
|
+
// HANDLERS — Reset
|
|
863
|
+
// =========================================================================
|
|
864
|
+
async function resetAll() {
|
|
865
|
+
console.log("[Lab] Resetting all preferences...");
|
|
866
|
+
document.documentElement.removeAttribute("style");
|
|
867
|
+
document.documentElement.removeAttribute("data-theme");
|
|
868
|
+
document.documentElement.classList.remove("stnd-debug");
|
|
869
|
+
sessionStorage.removeItem("stnd-theme");
|
|
870
|
+
sessionStorage.removeItem("stnd-debug-mode");
|
|
871
|
+
sessionStorage.removeItem("stnd-panel-state");
|
|
872
|
+
sessionStorage.removeItem("stnd-overrides");
|
|
873
|
+
sessionStorage.removeItem("stnd-sections");
|
|
874
|
+
// Clear theme preferences from storage
|
|
875
|
+
localStorage.removeItem("stnd-theme-global"); // theme-utils persistence
|
|
876
|
+
console.log(
|
|
877
|
+
"[Lab] localStorage after clear:",
|
|
878
|
+
localStorage.getItem("stnd-theme-global"),
|
|
879
|
+
);
|
|
880
|
+
document.cookie =
|
|
881
|
+
"std_prefs=; path=/; expires=Thu, 01 Jan 1970 00:00:00 UTC;";
|
|
882
|
+
document.documentElement.removeAttribute("data-color-mode");
|
|
883
|
+
userScheme = "auto";
|
|
884
|
+
// Reset server-side colorMode preference so page reloads don't re-apply it
|
|
885
|
+
try {
|
|
886
|
+
const { actions } = await import("astro:actions");
|
|
887
|
+
await actions.updatePreferences({ colorMode: "auto" });
|
|
888
|
+
} catch {}
|
|
889
|
+
modified = new Set();
|
|
890
|
+
theme = "";
|
|
891
|
+
debug = false;
|
|
892
|
+
syncDebugOverlays(false);
|
|
893
|
+
requestAnimationFrame(() => {
|
|
894
|
+
syncFromDOM();
|
|
895
|
+
storeDefaults();
|
|
896
|
+
});
|
|
897
|
+
window.toast?.info("All tokens reset");
|
|
898
|
+
console.log("[Lab] Reset complete");
|
|
899
|
+
}
|
|
900
|
+
|
|
901
|
+
function toggleSection(key) {
|
|
902
|
+
sectionsOpen = { ...sectionsOpen, [key]: !sectionsOpen[key] };
|
|
903
|
+
}
|
|
904
|
+
|
|
905
|
+
// =========================================================================
|
|
906
|
+
// DERIVED
|
|
907
|
+
// =========================================================================
|
|
908
|
+
let modCount = $derived(modified.size);
|
|
909
|
+
</script>
|
|
910
|
+
|
|
911
|
+
<!-- ======================================================================
|
|
912
|
+
TRIGGER / HANDLER
|
|
913
|
+
====================================================================== -->
|
|
914
|
+
<div
|
|
915
|
+
class="lab-handler-top m-0"
|
|
916
|
+
role="button"
|
|
917
|
+
tabindex="0"
|
|
918
|
+
aria-label="Open Design Lab"
|
|
919
|
+
onmouseenter={openLab}
|
|
920
|
+
onmouseleave={requestCloseLab}
|
|
921
|
+
onclick={() => (view = view === "dormant" ? "open" : "dormant")}
|
|
922
|
+
onkeydown={(e) => e.key === "Enter" && (view = "open")}
|
|
923
|
+
>
|
|
924
|
+
<div class="lab-dot"></div>
|
|
925
|
+
</div>
|
|
926
|
+
|
|
927
|
+
<div
|
|
928
|
+
class="lab-handler m-0"
|
|
929
|
+
class:scrolling={isScrolling}
|
|
930
|
+
class:modified={modCount > 0}
|
|
931
|
+
onmouseenter={openLab}
|
|
932
|
+
onmouseleave={requestCloseLab}
|
|
933
|
+
role="button"
|
|
934
|
+
tabindex="0"
|
|
935
|
+
aria-label="Open Design Lab"
|
|
936
|
+
onclick={() => (view = view === "dormant" ? "open" : "dormant")}
|
|
937
|
+
onkeydown={(e) => e.key === "Enter" && (view = "open")}
|
|
938
|
+
>
|
|
939
|
+
<div class="lab-handler-bar"></div>
|
|
940
|
+
</div>
|
|
941
|
+
|
|
942
|
+
<!-- ======================================================================
|
|
943
|
+
SIDE PANEL
|
|
944
|
+
====================================================================== -->
|
|
945
|
+
{#if view === "open"}
|
|
946
|
+
<div
|
|
947
|
+
bind:this={sidebarEl}
|
|
948
|
+
transition:fly={{
|
|
949
|
+
x: 320,
|
|
950
|
+
duration: sidebarDuration,
|
|
951
|
+
easing: customEasing,
|
|
952
|
+
opacity: false,
|
|
953
|
+
}}
|
|
954
|
+
class="lab-sidebar no-rhythm mt-0"
|
|
955
|
+
role="dialog"
|
|
956
|
+
tabindex="-1"
|
|
957
|
+
aria-label="Design Lab"
|
|
958
|
+
onmouseenter={cancelCloseLab}
|
|
959
|
+
onmouseleave={requestCloseLab}
|
|
960
|
+
>
|
|
961
|
+
<!-- ═══════════ HEADER ═══════════ -->
|
|
962
|
+
<header class="lab-header">
|
|
963
|
+
<div class="lab-header-title">
|
|
964
|
+
<h2>stnd</h2>
|
|
965
|
+
{#if version}<span class="lab-version">v{version}</span>{/if}
|
|
966
|
+
</div>
|
|
967
|
+
<div class="lab-header-meta">
|
|
968
|
+
<span class="badge ghost small text-subtle" title="Viewport Width">
|
|
969
|
+
{viewportW}px
|
|
970
|
+
</span>
|
|
971
|
+
<span class="badge ghost small text-subtle"
|
|
972
|
+
>{getBreakpoint(viewportW)}</span
|
|
973
|
+
>
|
|
974
|
+
<button
|
|
975
|
+
class="smaller ghost"
|
|
976
|
+
onclick={() => {
|
|
977
|
+
const modes = ["auto", "light", "dark"];
|
|
978
|
+
userScheme = modes[(modes.indexOf(userScheme) + 1) % modes.length];
|
|
979
|
+
}}
|
|
980
|
+
title="Toggle Theme (Auto / Light / Dark)"
|
|
981
|
+
>
|
|
982
|
+
{userScheme === "auto" ? "auto" : scheme}
|
|
983
|
+
</button>
|
|
984
|
+
|
|
985
|
+
<button
|
|
986
|
+
class="smaller icon ghost"
|
|
987
|
+
onclick={() => (pinned = !pinned)}
|
|
988
|
+
aria-label={pinned ? "Unpin Lab" : "Pin Lab"}
|
|
989
|
+
title={pinned ? "Unpin Lab" : "Pin Lab"}
|
|
990
|
+
>
|
|
991
|
+
{pinned ? "●" : "○"}
|
|
992
|
+
</button>
|
|
993
|
+
<button
|
|
994
|
+
class="smaller icon ghost"
|
|
995
|
+
onclick={() => (view = "dormant")}
|
|
996
|
+
aria-label="Close Lab"
|
|
997
|
+
>
|
|
998
|
+
✕
|
|
999
|
+
</button>
|
|
1000
|
+
</div>
|
|
1001
|
+
</header>
|
|
1002
|
+
|
|
1003
|
+
<!-- ═══════════ SCROLLABLE BODY ═══════════ -->
|
|
1004
|
+
<div class="lab-body">
|
|
1005
|
+
<!-- ─── THEME ─── -->
|
|
1006
|
+
<section class="lab-section">
|
|
1007
|
+
<button class="lab-section-head" onclick={() => toggleSection("theme")}>
|
|
1008
|
+
<span class="lab-section-arrow" class:open={sectionsOpen.theme}
|
|
1009
|
+
>▶</span
|
|
1010
|
+
>
|
|
1011
|
+
<span class="lab-section-title">Theme</span>
|
|
1012
|
+
</button>
|
|
1013
|
+
{#if sectionsOpen.theme}
|
|
1014
|
+
<div class="lab-section-body" transition:slide>
|
|
1015
|
+
<div class="lab-field">
|
|
1016
|
+
<label class="lab-field-label" for="lab-preset">Preset</label>
|
|
1017
|
+
<select
|
|
1018
|
+
id="lab-preset"
|
|
1019
|
+
class="lab-select"
|
|
1020
|
+
value={theme}
|
|
1021
|
+
onchange={onThemeChange}
|
|
1022
|
+
>
|
|
1023
|
+
{#each THEMES as t (t.value)}
|
|
1024
|
+
<option value={t.value} selected={t.value === theme}
|
|
1025
|
+
>{t.label}</option
|
|
1026
|
+
>
|
|
1027
|
+
{/each}
|
|
1028
|
+
</select>
|
|
1029
|
+
</div>
|
|
1030
|
+
</div>
|
|
1031
|
+
{/if}
|
|
1032
|
+
</section>
|
|
1033
|
+
|
|
1034
|
+
<!-- ─── COLORS ─── -->
|
|
1035
|
+
<section class="lab-section">
|
|
1036
|
+
<button
|
|
1037
|
+
class="lab-section-head"
|
|
1038
|
+
onclick={() => toggleSection("colors")}
|
|
1039
|
+
>
|
|
1040
|
+
<span class="lab-section-arrow" class:open={sectionsOpen.colors}
|
|
1041
|
+
>▶</span
|
|
1042
|
+
>
|
|
1043
|
+
<span class="lab-section-title">Colors</span>
|
|
1044
|
+
</button>
|
|
1045
|
+
{#if sectionsOpen.colors}
|
|
1046
|
+
<div class="lab-section-body" transition:slide>
|
|
1047
|
+
<div class="lab-field">
|
|
1048
|
+
<label class="lab-field-label" for="lab-bg-text">Background</label
|
|
1049
|
+
>
|
|
1050
|
+
<div class="lab-color-row">
|
|
1051
|
+
<input
|
|
1052
|
+
type="color"
|
|
1053
|
+
class="lab-swatch"
|
|
1054
|
+
value={seedBgHex}
|
|
1055
|
+
title="{scheme} background seed"
|
|
1056
|
+
oninput={(e) => onColorSwatch("bg", e)}
|
|
1057
|
+
/>
|
|
1058
|
+
<input
|
|
1059
|
+
id="lab-bg-text"
|
|
1060
|
+
type="text"
|
|
1061
|
+
class="lab-text-input"
|
|
1062
|
+
value={seedBg}
|
|
1063
|
+
spellcheck="false"
|
|
1064
|
+
placeholder="e.g. #fafafa"
|
|
1065
|
+
oninput={(e) => onColorText("bg", e)}
|
|
1066
|
+
/>
|
|
1067
|
+
</div>
|
|
1068
|
+
</div>
|
|
1069
|
+
<div class="lab-field">
|
|
1070
|
+
<label class="lab-field-label" for="lab-fg-text">Foreground</label
|
|
1071
|
+
>
|
|
1072
|
+
<div class="lab-color-row">
|
|
1073
|
+
<input
|
|
1074
|
+
type="color"
|
|
1075
|
+
class="lab-swatch"
|
|
1076
|
+
value={seedFgHex}
|
|
1077
|
+
title="{scheme} foreground seed"
|
|
1078
|
+
oninput={(e) => onColorSwatch("fg", e)}
|
|
1079
|
+
/>
|
|
1080
|
+
<input
|
|
1081
|
+
id="lab-fg-text"
|
|
1082
|
+
type="text"
|
|
1083
|
+
class="lab-text-input"
|
|
1084
|
+
value={seedFg}
|
|
1085
|
+
spellcheck="false"
|
|
1086
|
+
placeholder="e.g. #1a1a1a"
|
|
1087
|
+
oninput={(e) => onColorText("fg", e)}
|
|
1088
|
+
/>
|
|
1089
|
+
</div>
|
|
1090
|
+
</div>
|
|
1091
|
+
</div>
|
|
1092
|
+
{/if}
|
|
1093
|
+
</section>
|
|
1094
|
+
|
|
1095
|
+
<!-- ─── TYPOGRAPHY ─── -->
|
|
1096
|
+
<section class="lab-section">
|
|
1097
|
+
<button
|
|
1098
|
+
class="lab-section-head"
|
|
1099
|
+
onclick={() => toggleSection("typography")}
|
|
1100
|
+
>
|
|
1101
|
+
<span class="lab-section-arrow" class:open={sectionsOpen.typography}
|
|
1102
|
+
>▶</span
|
|
1103
|
+
>
|
|
1104
|
+
<span class="lab-section-title">Typography</span>
|
|
1105
|
+
</button>
|
|
1106
|
+
{#if sectionsOpen.typography}
|
|
1107
|
+
<div class="lab-section-body" transition:slide>
|
|
1108
|
+
{#snippet fontField(inputId, labelText, token, currentValue)}
|
|
1109
|
+
<div class="lab-field lab-font-field">
|
|
1110
|
+
<label class="lab-field-label" for={inputId}>{labelText}</label>
|
|
1111
|
+
|
|
1112
|
+
<Combobox
|
|
1113
|
+
options={FONTS.map((f) => ({ label: f, value: f }))}
|
|
1114
|
+
value={(currentValue || "")
|
|
1115
|
+
.split(",")[0]
|
|
1116
|
+
.trim()
|
|
1117
|
+
.replace(/^"|"$/g, "")}
|
|
1118
|
+
placeholder="Font family or Google Font"
|
|
1119
|
+
searchPlaceholder="Search fonts..."
|
|
1120
|
+
emptyText="No fonts found."
|
|
1121
|
+
allowCustom={true}
|
|
1122
|
+
preview={true}
|
|
1123
|
+
previewDebounce={120}
|
|
1124
|
+
loadFont={(name) => loadGoogleFont(name)}
|
|
1125
|
+
class="lab-font-combobox"
|
|
1126
|
+
aria-controls={inputId}
|
|
1127
|
+
on:preview={(e) => loadGoogleFont(e.detail?.value)}
|
|
1128
|
+
on:change={(e) => {
|
|
1129
|
+
// Combobox dispatches change with detail.value (base font name)
|
|
1130
|
+
selectFont(token, e.detail?.value);
|
|
1131
|
+
}}
|
|
1132
|
+
/>
|
|
1133
|
+
</div>
|
|
1134
|
+
{/snippet}
|
|
1135
|
+
|
|
1136
|
+
{@render fontField(
|
|
1137
|
+
"lab-font-body",
|
|
1138
|
+
"Body",
|
|
1139
|
+
"--font-text",
|
|
1140
|
+
fontText,
|
|
1141
|
+
)}
|
|
1142
|
+
{@render fontField(
|
|
1143
|
+
"lab-font-head",
|
|
1144
|
+
"Headings",
|
|
1145
|
+
"--font-header",
|
|
1146
|
+
fontHeader,
|
|
1147
|
+
)}
|
|
1148
|
+
{@render fontField(
|
|
1149
|
+
"lab-font-ui",
|
|
1150
|
+
"Interface",
|
|
1151
|
+
"--font-interface",
|
|
1152
|
+
fontInterface,
|
|
1153
|
+
)}
|
|
1154
|
+
{@render fontField(
|
|
1155
|
+
"lab-font-mono",
|
|
1156
|
+
"Monospace",
|
|
1157
|
+
"--font-monospace",
|
|
1158
|
+
fontMono,
|
|
1159
|
+
)}
|
|
1160
|
+
</div>
|
|
1161
|
+
{/if}
|
|
1162
|
+
</section>
|
|
1163
|
+
|
|
1164
|
+
<!-- ─── SCALE ─── -->
|
|
1165
|
+
<section class="lab-section">
|
|
1166
|
+
<button class="lab-section-head" onclick={() => toggleSection("scale")}>
|
|
1167
|
+
<span class="lab-section-arrow" class:open={sectionsOpen.scale}
|
|
1168
|
+
>▶</span
|
|
1169
|
+
>
|
|
1170
|
+
<span class="lab-section-title">Scale</span>
|
|
1171
|
+
</button>
|
|
1172
|
+
{#if sectionsOpen.scale}
|
|
1173
|
+
<div class="lab-section-body" transition:slide>
|
|
1174
|
+
<div class="lab-field">
|
|
1175
|
+
<label class="lab-field-label" for="lab-font-size"
|
|
1176
|
+
>Font Size</label
|
|
1177
|
+
>
|
|
1178
|
+
<div class="lab-slider-wrapper">
|
|
1179
|
+
<input
|
|
1180
|
+
id="lab-font-size"
|
|
1181
|
+
type="range"
|
|
1182
|
+
class="lab-rangess"
|
|
1183
|
+
value={fontSize}
|
|
1184
|
+
min="0.75"
|
|
1185
|
+
max="1.5"
|
|
1186
|
+
step="0.05"
|
|
1187
|
+
oninput={onFontSizeInput}
|
|
1188
|
+
/>
|
|
1189
|
+
<span class="lab-readout">{fontSize.toFixed(2)}rem</span>
|
|
1190
|
+
</div>
|
|
1191
|
+
</div>
|
|
1192
|
+
<div class="lab-field">
|
|
1193
|
+
<label class="lab-field-label" for="lab-line-height"
|
|
1194
|
+
>Line Height</label
|
|
1195
|
+
>
|
|
1196
|
+
<div class="lab-slider-wrapper">
|
|
1197
|
+
<input
|
|
1198
|
+
id="lab-line-height"
|
|
1199
|
+
type="range"
|
|
1200
|
+
class="lab-range"
|
|
1201
|
+
value={lineHeight}
|
|
1202
|
+
min="1"
|
|
1203
|
+
max="2"
|
|
1204
|
+
step="0.05"
|
|
1205
|
+
oninput={onLineHeightInput}
|
|
1206
|
+
/>
|
|
1207
|
+
<span class="lab-readout">{lineHeight.toFixed(2)}</span>
|
|
1208
|
+
</div>
|
|
1209
|
+
</div>
|
|
1210
|
+
<div class="lab-field">
|
|
1211
|
+
<label class="lab-field-label" for="lab-ratio"
|
|
1212
|
+
>Optical Ratio</label
|
|
1213
|
+
>
|
|
1214
|
+
<div class="lab-slider-wrapper">
|
|
1215
|
+
<input
|
|
1216
|
+
id="lab-ratio"
|
|
1217
|
+
type="range"
|
|
1218
|
+
class="lab-range"
|
|
1219
|
+
value={ratio}
|
|
1220
|
+
min="1"
|
|
1221
|
+
max="2"
|
|
1222
|
+
step="0.01"
|
|
1223
|
+
oninput={onRatioInput}
|
|
1224
|
+
/>
|
|
1225
|
+
<span class="lab-readout">{ratio.toFixed(2)}</span>
|
|
1226
|
+
</div>
|
|
1227
|
+
</div>
|
|
1228
|
+
</div>
|
|
1229
|
+
{/if}
|
|
1230
|
+
</section>
|
|
1231
|
+
|
|
1232
|
+
<!-- ─── SPACING ─── -->
|
|
1233
|
+
<section class="lab-section">
|
|
1234
|
+
<button
|
|
1235
|
+
class="lab-section-head"
|
|
1236
|
+
onclick={() => toggleSection("spacing")}
|
|
1237
|
+
>
|
|
1238
|
+
<span class="lab-section-arrow" class:open={sectionsOpen.spacing}
|
|
1239
|
+
>▶</span
|
|
1240
|
+
>
|
|
1241
|
+
<span class="lab-section-title">Spacing</span>
|
|
1242
|
+
</button>
|
|
1243
|
+
{#if sectionsOpen.spacing}
|
|
1244
|
+
<div class="lab-section-body" transition:slide>
|
|
1245
|
+
<div class="lab-field">
|
|
1246
|
+
<label class="lab-field-label" for="lab-gap-body">Body Gap</label>
|
|
1247
|
+
<div class="lab-slider-wrapper">
|
|
1248
|
+
<input
|
|
1249
|
+
id="lab-gap-body"
|
|
1250
|
+
type="range"
|
|
1251
|
+
class="lab-range"
|
|
1252
|
+
value={gapBody}
|
|
1253
|
+
min="0"
|
|
1254
|
+
max="4"
|
|
1255
|
+
step="0.25"
|
|
1256
|
+
oninput={onGapInput}
|
|
1257
|
+
/>
|
|
1258
|
+
<span class="lab-readout">{gapBody.toFixed(2)}x</span>
|
|
1259
|
+
</div>
|
|
1260
|
+
</div>
|
|
1261
|
+
<div class="lab-field">
|
|
1262
|
+
<label class="lab-field-label" for="lab-rhythm-gap"
|
|
1263
|
+
>Rhythm Gap</label
|
|
1264
|
+
>
|
|
1265
|
+
<div class="lab-slider-wrapper">
|
|
1266
|
+
<input
|
|
1267
|
+
id="lab-rhythm-gap"
|
|
1268
|
+
type="range"
|
|
1269
|
+
class="lab-range"
|
|
1270
|
+
value={rhythmGap}
|
|
1271
|
+
min="0"
|
|
1272
|
+
max="4"
|
|
1273
|
+
step="0.25"
|
|
1274
|
+
oninput={onRhythmGapInput}
|
|
1275
|
+
/>
|
|
1276
|
+
<span class="lab-readout">{rhythmGap.toFixed(2)}x</span>
|
|
1277
|
+
</div>
|
|
1278
|
+
</div>
|
|
1279
|
+
<div class="lab-field">
|
|
1280
|
+
<label class="lab-field-label" for="lab-radius">Radius</label>
|
|
1281
|
+
<div class="lab-slider-wrapper">
|
|
1282
|
+
<input
|
|
1283
|
+
id="lab-radius"
|
|
1284
|
+
type="range"
|
|
1285
|
+
class="lab-range"
|
|
1286
|
+
value={radius}
|
|
1287
|
+
min="0"
|
|
1288
|
+
max="4"
|
|
1289
|
+
step="0.25"
|
|
1290
|
+
oninput={onRadiusInput}
|
|
1291
|
+
/>
|
|
1292
|
+
<span class="lab-readout">{radius.toFixed(2)}x</span>
|
|
1293
|
+
</div>
|
|
1294
|
+
</div>
|
|
1295
|
+
</div>
|
|
1296
|
+
{/if}
|
|
1297
|
+
</section>
|
|
1298
|
+
|
|
1299
|
+
<!-- ─── DEBUG ─── -->
|
|
1300
|
+
<section class="lab-section">
|
|
1301
|
+
<button class="lab-section-head" onclick={() => toggleSection("debug")}>
|
|
1302
|
+
<span class="lab-section-arrow" class:open={sectionsOpen.debug}
|
|
1303
|
+
>▶</span
|
|
1304
|
+
>
|
|
1305
|
+
<span class="lab-section-title">Debug</span>
|
|
1306
|
+
</button>
|
|
1307
|
+
{#if sectionsOpen.debug}
|
|
1308
|
+
<div class="lab-section-body" transition:slide>
|
|
1309
|
+
<label class="lab-toggle" for="lab-debug-overlays">
|
|
1310
|
+
<input
|
|
1311
|
+
id="lab-debug-overlays"
|
|
1312
|
+
type="checkbox"
|
|
1313
|
+
checked={debug}
|
|
1314
|
+
onchange={onDebugToggle}
|
|
1315
|
+
/>
|
|
1316
|
+
<span>Debug Overlays</span>
|
|
1317
|
+
<kbd>⇧⌘D</kbd>
|
|
1318
|
+
</label>
|
|
1319
|
+
<div class="lab-field mt-half">
|
|
1320
|
+
<button
|
|
1321
|
+
class="lab-btn lab-btn-secondary w-full"
|
|
1322
|
+
onclick={() => {
|
|
1323
|
+
document.cookie = "std_prefs=; Max-Age=0; path=/";
|
|
1324
|
+
sessionStorage.removeItem("stnd-theme");
|
|
1325
|
+
sessionStorage.removeItem("stnd-scheme");
|
|
1326
|
+
sessionStorage.removeItem("stnd-overrides");
|
|
1327
|
+
localStorage.removeItem("stnd-theme-global");
|
|
1328
|
+
theme = "";
|
|
1329
|
+
userScheme = "auto";
|
|
1330
|
+
modified = new Set();
|
|
1331
|
+
document.documentElement.removeAttribute("data-theme");
|
|
1332
|
+
document.documentElement.removeAttribute("data-lab-scheme");
|
|
1333
|
+
document.documentElement.removeAttribute("style");
|
|
1334
|
+
requestAnimationFrame(syncFromDOM);
|
|
1335
|
+
window.toast?.("Preferences cleared", "info");
|
|
1336
|
+
}}
|
|
1337
|
+
>
|
|
1338
|
+
Clear Preferences
|
|
1339
|
+
</button>
|
|
1340
|
+
</div>
|
|
1341
|
+
</div>
|
|
1342
|
+
{/if}
|
|
1343
|
+
</section>
|
|
1344
|
+
|
|
1345
|
+
<!-- ─── LINKS ─── -->
|
|
1346
|
+
<section class="lab-section">
|
|
1347
|
+
<button class="lab-section-head" onclick={() => toggleSection("links")}>
|
|
1348
|
+
<span class="lab-section-arrow" class:open={sectionsOpen.links}
|
|
1349
|
+
>▶</span
|
|
1350
|
+
>
|
|
1351
|
+
<span class="lab-section-title">Links</span>
|
|
1352
|
+
</button>
|
|
1353
|
+
{#if sectionsOpen.links}
|
|
1354
|
+
<div class="lab-section-body" transition:slide>
|
|
1355
|
+
<div class="lab-links">
|
|
1356
|
+
<a
|
|
1357
|
+
href="https://stnd.build/playground"
|
|
1358
|
+
target="_blank"
|
|
1359
|
+
rel="noopener"
|
|
1360
|
+
class="lab-link"
|
|
1361
|
+
>
|
|
1362
|
+
↗ Playground
|
|
1363
|
+
</a>
|
|
1364
|
+
<a
|
|
1365
|
+
href="https://stnd.build"
|
|
1366
|
+
target="_blank"
|
|
1367
|
+
rel="noopener"
|
|
1368
|
+
class="lab-link"
|
|
1369
|
+
>
|
|
1370
|
+
↗ Manual
|
|
1371
|
+
</a>
|
|
1372
|
+
<a
|
|
1373
|
+
href="https://stnd.build/icons"
|
|
1374
|
+
target="_blank"
|
|
1375
|
+
rel="noopener"
|
|
1376
|
+
class="lab-link"
|
|
1377
|
+
>
|
|
1378
|
+
↗ Icons
|
|
1379
|
+
</a>
|
|
1380
|
+
</div>
|
|
1381
|
+
</div>
|
|
1382
|
+
{/if}
|
|
1383
|
+
</section>
|
|
1384
|
+
</div>
|
|
1385
|
+
|
|
1386
|
+
<!-- ═══════════ FOOTER ═══════════ -->
|
|
1387
|
+
<footer class="lab-footer">
|
|
1388
|
+
<button class="lab-btn lab-btn-secondary" onclick={resetAll}>
|
|
1389
|
+
Reset{modCount > 0 ? ` (${modCount})` : ""}
|
|
1390
|
+
</button>
|
|
1391
|
+
<button class="lab-btn lab-btn-primary" onclick={copyCSS}>
|
|
1392
|
+
Copy CSS
|
|
1393
|
+
</button>
|
|
1394
|
+
</footer>
|
|
1395
|
+
</div>
|
|
1396
|
+
{/if}
|
|
1397
|
+
|
|
1398
|
+
<!-- ======================================================================
|
|
1399
|
+
STYLES — Fully scoped, isolated from design tokens
|
|
1400
|
+
====================================================================== -->
|
|
1401
|
+
<style>
|
|
1402
|
+
/* ═══════════ PAGE SHRINK (like DevTools) ═══════════ */
|
|
1403
|
+
:global(html.lab-open) {
|
|
1404
|
+
margin-right: var(--lab-sidebar-w, 320px);
|
|
1405
|
+
transition: margin-right var(--transition);
|
|
1406
|
+
}
|
|
1407
|
+
:global(html:not(.lab-open)) {
|
|
1408
|
+
transition: margin-right var(--transition);
|
|
1409
|
+
}
|
|
1410
|
+
|
|
1411
|
+
/* ═══════════ DESIGN CONSTANTS ═══════════ */
|
|
1412
|
+
:root {
|
|
1413
|
+
--lab-font-size: 12px;
|
|
1414
|
+
--lab-sidebar-w: 320px;
|
|
1415
|
+
}
|
|
1416
|
+
|
|
1417
|
+
/* ═══════════ HANDLER TRIGGER ═══════════ */
|
|
1418
|
+
.lab-handler-top {
|
|
1419
|
+
position: fixed;
|
|
1420
|
+
z-index: 200;
|
|
1421
|
+
top: 0;
|
|
1422
|
+
right: 0;
|
|
1423
|
+
padding: var(--space-2);
|
|
1424
|
+
cursor: pointer;
|
|
1425
|
+
opacity: 0.1;
|
|
1426
|
+
transition: opacity var(--transition);
|
|
1427
|
+
display: none;
|
|
1428
|
+
}
|
|
1429
|
+
.lab-handler-top:hover {
|
|
1430
|
+
opacity: 1;
|
|
1431
|
+
}
|
|
1432
|
+
.lab-dot {
|
|
1433
|
+
width: var(--space-d4);
|
|
1434
|
+
height: var(--space-d4);
|
|
1435
|
+
background: var(--color-foreground);
|
|
1436
|
+
border-radius: 50%;
|
|
1437
|
+
}
|
|
1438
|
+
|
|
1439
|
+
.lab-handler {
|
|
1440
|
+
position: fixed;
|
|
1441
|
+
z-index: 200;
|
|
1442
|
+
top: 20%;
|
|
1443
|
+
right: 0;
|
|
1444
|
+
padding: var(--space); /* Bigger hit area */
|
|
1445
|
+
padding-right: 20px;
|
|
1446
|
+
cursor: normal;
|
|
1447
|
+
display: flex;
|
|
1448
|
+
flex-direction: row;
|
|
1449
|
+
align-items: center;
|
|
1450
|
+
transition: all var(--transition);
|
|
1451
|
+
}
|
|
1452
|
+
.lab-handler.scrolling .lab-handler-bar {
|
|
1453
|
+
background: oklch(from var(--color-foreground) l c h / 0.3);
|
|
1454
|
+
}
|
|
1455
|
+
.lab-handler-bar {
|
|
1456
|
+
width: var(--space-d4);
|
|
1457
|
+
height: var(--space-2);
|
|
1458
|
+
background: oklch(from var(--color-foreground) l c h / 0);
|
|
1459
|
+
border-radius: var(--radius);
|
|
1460
|
+
box-shadow: var(--shadow-inset) var(--shadow-ring);
|
|
1461
|
+
transition: all var(--transition);
|
|
1462
|
+
cursor: normal;
|
|
1463
|
+
backdrop-filter: var(--filter-glass);
|
|
1464
|
+
}
|
|
1465
|
+
.lab-handler.modified .lab-handler-bar {
|
|
1466
|
+
background: oklch(from var(--color-accent) l c h / 0.1);
|
|
1467
|
+
}
|
|
1468
|
+
|
|
1469
|
+
/* ═══════════ SIDEBAR ═══════════ */
|
|
1470
|
+
.lab-sidebar {
|
|
1471
|
+
position: fixed;
|
|
1472
|
+
z-index: 9992;
|
|
1473
|
+
top: 0;
|
|
1474
|
+
right: 0;
|
|
1475
|
+
width: var(--lab-sidebar-w);
|
|
1476
|
+
height: 100dvh;
|
|
1477
|
+
|
|
1478
|
+
color: var(--color-foreground);
|
|
1479
|
+
font: var(--lab-font-size) / 1.5 var(--font-monospace);
|
|
1480
|
+
border-left: var(--border);
|
|
1481
|
+
display: flex;
|
|
1482
|
+
flex-direction: column;
|
|
1483
|
+
box-shadow: var(--shadow-ring), var(--shadow-glow);
|
|
1484
|
+
background: var(--color-surface);
|
|
1485
|
+
backdrop-filter: var(--filter-blur);
|
|
1486
|
+
/* Ensure the sidebar stays fully opaque and never animates opacity */
|
|
1487
|
+
opacity: 1 !important;
|
|
1488
|
+
/* Explicitly disable opacity animation while preserving other transitions */
|
|
1489
|
+
transition:
|
|
1490
|
+
transform var(--transition),
|
|
1491
|
+
box-shadow var(--transition),
|
|
1492
|
+
background var(--transition),
|
|
1493
|
+
opacity 0s;
|
|
1494
|
+
|
|
1495
|
+
h2 {
|
|
1496
|
+
margin: 0 !important;
|
|
1497
|
+
}
|
|
1498
|
+
}
|
|
1499
|
+
|
|
1500
|
+
/* Reset children to Lab font */
|
|
1501
|
+
.lab-sidebar * {
|
|
1502
|
+
font-family: var(--font-monospace) !important;
|
|
1503
|
+
box-sizing: border-box;
|
|
1504
|
+
/* Disable opacity transitions on child elements to prevent fading when opening the lab.
|
|
1505
|
+
Other property transitions for children remain allowed outside of opacity. */
|
|
1506
|
+
transition: opacity 0s !important;
|
|
1507
|
+
}
|
|
1508
|
+
|
|
1509
|
+
/* ═══════════ HEADER ═══════════ */
|
|
1510
|
+
.lab-header {
|
|
1511
|
+
display: flex;
|
|
1512
|
+
flex-wrap: wrap;
|
|
1513
|
+
align-items: center;
|
|
1514
|
+
gap: 0;
|
|
1515
|
+
padding: var(--space-d6) var(--space-d2);
|
|
1516
|
+
border-bottom: var(--border);
|
|
1517
|
+
flex-shrink: 0;
|
|
1518
|
+
|
|
1519
|
+
background: var(--color-surface-low);
|
|
1520
|
+
}
|
|
1521
|
+
.lab-header-title {
|
|
1522
|
+
display: flex;
|
|
1523
|
+
align-items: center;
|
|
1524
|
+
gap: 0px;
|
|
1525
|
+
flex: 1;
|
|
1526
|
+
min-width: 0;
|
|
1527
|
+
}
|
|
1528
|
+
.lab-header h2 {
|
|
1529
|
+
font-size: var(--size-sm) !important;
|
|
1530
|
+
line-height: 1 !important;
|
|
1531
|
+
}
|
|
1532
|
+
.lab-version {
|
|
1533
|
+
font-size: var(--size-xs) !important;
|
|
1534
|
+
color: var(--text-muted);
|
|
1535
|
+
}
|
|
1536
|
+
.lab-header-meta {
|
|
1537
|
+
display: flex;
|
|
1538
|
+
align-items: center;
|
|
1539
|
+
gap: var(--trim);
|
|
1540
|
+
}
|
|
1541
|
+
|
|
1542
|
+
/* ═══════════ SCROLLABLE BODY ═══════════ */
|
|
1543
|
+
.lab-body {
|
|
1544
|
+
flex: 1;
|
|
1545
|
+
overflow-y: auto;
|
|
1546
|
+
overflow-x: hidden;
|
|
1547
|
+
gap: 0;
|
|
1548
|
+
}
|
|
1549
|
+
.lab-body::-webkit-scrollbar {
|
|
1550
|
+
width: 4px;
|
|
1551
|
+
}
|
|
1552
|
+
.lab-body::-webkit-scrollbar-track {
|
|
1553
|
+
background: transparent;
|
|
1554
|
+
}
|
|
1555
|
+
.lab-body::-webkit-scrollbar-thumb {
|
|
1556
|
+
background: color-mix(in srgb, var(--color-foreground) 15%, transparent);
|
|
1557
|
+
border-radius: 2px;
|
|
1558
|
+
}
|
|
1559
|
+
|
|
1560
|
+
/* ═══════════ SECTIONS ═══════════ */
|
|
1561
|
+
.lab-section {
|
|
1562
|
+
box-shadow: var(--shadow-ring);
|
|
1563
|
+
}
|
|
1564
|
+
.lab-section-head {
|
|
1565
|
+
all: unset;
|
|
1566
|
+
display: flex;
|
|
1567
|
+
align-items: center;
|
|
1568
|
+
gap: 0;
|
|
1569
|
+
width: 100%;
|
|
1570
|
+
padding: var(--space-d6) 0;
|
|
1571
|
+
cursor: pointer;
|
|
1572
|
+
user-select: none;
|
|
1573
|
+
background: var(--color-surface);
|
|
1574
|
+
box-shadow: var(--shadow-raised);
|
|
1575
|
+
}
|
|
1576
|
+
.lab-section-head:hover {
|
|
1577
|
+
background: var(--color-surface);
|
|
1578
|
+
}
|
|
1579
|
+
|
|
1580
|
+
.lab-section-arrow {
|
|
1581
|
+
font-size: var(--size-2xs) !important;
|
|
1582
|
+
color: var(--text-subtle);
|
|
1583
|
+
transition: transform var(--transition);
|
|
1584
|
+
flex-shrink: 0;
|
|
1585
|
+
width: var(--space);
|
|
1586
|
+
display: inline-flex;
|
|
1587
|
+
justify-content: center;
|
|
1588
|
+
}
|
|
1589
|
+
.lab-section-arrow.open {
|
|
1590
|
+
transform: rotate(90deg);
|
|
1591
|
+
}
|
|
1592
|
+
|
|
1593
|
+
.lab-section-title {
|
|
1594
|
+
font-size: var(--size-2xs) !important;
|
|
1595
|
+
font-weight: 600;
|
|
1596
|
+
text-transform: uppercase;
|
|
1597
|
+
letter-spacing: 0.08em;
|
|
1598
|
+
color: var(--text-muted);
|
|
1599
|
+
}
|
|
1600
|
+
|
|
1601
|
+
.lab-section-body {
|
|
1602
|
+
padding: var(--space-d2) var(--space);
|
|
1603
|
+
background: var(--color-surface);
|
|
1604
|
+
box-shadow: var(--shadow);
|
|
1605
|
+
}
|
|
1606
|
+
|
|
1607
|
+
/* ═══════════ FIELDS ═══════════ */
|
|
1608
|
+
.lab-field {
|
|
1609
|
+
margin-bottom: var(--space-half);
|
|
1610
|
+
display: grid;
|
|
1611
|
+
min-height: var(--space);
|
|
1612
|
+
grid-template-columns: 80px 1fr;
|
|
1613
|
+
gap: 0;
|
|
1614
|
+
align-items: center;
|
|
1615
|
+
}
|
|
1616
|
+
.lab-field:last-child {
|
|
1617
|
+
margin-bottom: 0;
|
|
1618
|
+
}
|
|
1619
|
+
|
|
1620
|
+
.lab-field-label {
|
|
1621
|
+
display: block;
|
|
1622
|
+
font-size: var(--size-3xs) !important;
|
|
1623
|
+
color: var(--color-muted);
|
|
1624
|
+
text-transform: uppercase;
|
|
1625
|
+
line-height: 1;
|
|
1626
|
+
}
|
|
1627
|
+
|
|
1628
|
+
/* ═══════════ TEXT INPUT ═══════════ */
|
|
1629
|
+
.lab-text-input {
|
|
1630
|
+
width: 100%;
|
|
1631
|
+
font-size: var(--size-2xs) !important;
|
|
1632
|
+
line-height: 1 !important;
|
|
1633
|
+
transition: all var(--transition);
|
|
1634
|
+
margin: 0 !important;
|
|
1635
|
+
}
|
|
1636
|
+
|
|
1637
|
+
/* ═══════════ COLOR ROW ═══════════ */
|
|
1638
|
+
.lab-color-row {
|
|
1639
|
+
display: flex;
|
|
1640
|
+
align-items: center;
|
|
1641
|
+
gap: var(--trim);
|
|
1642
|
+
}
|
|
1643
|
+
.lab-swatch {
|
|
1644
|
+
width: var(--space);
|
|
1645
|
+
height: var(--space);
|
|
1646
|
+
border: var(--border);
|
|
1647
|
+
border-radius: var(--radius);
|
|
1648
|
+
cursor: pointer;
|
|
1649
|
+
padding: 0px;
|
|
1650
|
+
background: var(--color-surface);
|
|
1651
|
+
flex-shrink: 0;
|
|
1652
|
+
}
|
|
1653
|
+
.lab-swatch::-webkit-color-swatch-wrapper {
|
|
1654
|
+
padding: 0;
|
|
1655
|
+
}
|
|
1656
|
+
.lab-swatch::-webkit-color-swatch {
|
|
1657
|
+
border: none;
|
|
1658
|
+
border-radius: var(--radius);
|
|
1659
|
+
}
|
|
1660
|
+
.lab-color-row .lab-text-input {
|
|
1661
|
+
flex: 1;
|
|
1662
|
+
min-width: 0;
|
|
1663
|
+
}
|
|
1664
|
+
|
|
1665
|
+
/* ═══════════ SELECT ═══════════ */
|
|
1666
|
+
.lab-select {
|
|
1667
|
+
font-size: var(--size-2xs) !important;
|
|
1668
|
+
}
|
|
1669
|
+
.lab-select:focus {
|
|
1670
|
+
border-color: var(--color-accent);
|
|
1671
|
+
}
|
|
1672
|
+
|
|
1673
|
+
/* ═══════════ SLIDERS ═══════════ */
|
|
1674
|
+
/* ═══════════ FONT DROPDOWN ═══════════ */
|
|
1675
|
+
.lab-font-field {
|
|
1676
|
+
position: relative;
|
|
1677
|
+
}
|
|
1678
|
+
|
|
1679
|
+
.lab-slider-wrapper {
|
|
1680
|
+
display: flex;
|
|
1681
|
+
align-items: center;
|
|
1682
|
+
gap: var(--space-half);
|
|
1683
|
+
width: 100%;
|
|
1684
|
+
}
|
|
1685
|
+
|
|
1686
|
+
/* ═══════════ LINKS ═══════════ */
|
|
1687
|
+
.lab-links {
|
|
1688
|
+
display: flex;
|
|
1689
|
+
flex-direction: column;
|
|
1690
|
+
gap: var(--space-d4);
|
|
1691
|
+
}
|
|
1692
|
+
|
|
1693
|
+
.lab-link {
|
|
1694
|
+
font-size: var(--size-2xs) !important;
|
|
1695
|
+
color: var(--color-accent);
|
|
1696
|
+
text-decoration: none;
|
|
1697
|
+
padding: var(--space-d4) 0;
|
|
1698
|
+
transition: opacity var(--transition-fast);
|
|
1699
|
+
|
|
1700
|
+
&:hover {
|
|
1701
|
+
opacity: 0.7;
|
|
1702
|
+
}
|
|
1703
|
+
}
|
|
1704
|
+
|
|
1705
|
+
/* ═══════════ FOOTER ═══════════ */
|
|
1706
|
+
.lab-footer {
|
|
1707
|
+
display: flex;
|
|
1708
|
+
justify-content: space-evenly;
|
|
1709
|
+
gap: var(--space-half);
|
|
1710
|
+
padding: var(--space-half) var(--space);
|
|
1711
|
+
box-shadow: var(--shadow-raised);
|
|
1712
|
+
flex-shrink: 0;
|
|
1713
|
+
background: var(--color-surface-low);
|
|
1714
|
+
}
|
|
1715
|
+
|
|
1716
|
+
/* ═══════════ RESPONSIVE ═══════════ */
|
|
1717
|
+
@media (max-width: 480px) {
|
|
1718
|
+
.lab-sidebar {
|
|
1719
|
+
width: 100vw;
|
|
1720
|
+
}
|
|
1721
|
+
}
|
|
1722
|
+
@media print {
|
|
1723
|
+
.lab-handler,
|
|
1724
|
+
.lab-sidebar {
|
|
1725
|
+
display: none !important;
|
|
1726
|
+
}
|
|
1727
|
+
}
|
|
1728
|
+
|
|
1729
|
+
/* ═══════════ GLOBAL SCHEME OVERRIDES ═══════════ */
|
|
1730
|
+
:global(:root[data-lab-scheme="dark"]) {
|
|
1731
|
+
--color-background: var(--color-dark-background);
|
|
1732
|
+
--color-foreground: var(--color-dark-foreground);
|
|
1733
|
+
--color-accent: var(--color-dark-accent);
|
|
1734
|
+
--color-header: var(--color-dark-header, var(--color-foreground));
|
|
1735
|
+
--color-surface: color-mix(
|
|
1736
|
+
in srgb,
|
|
1737
|
+
var(--color-background) 100%,
|
|
1738
|
+
transparent
|
|
1739
|
+
);
|
|
1740
|
+
}
|
|
1741
|
+
:global(:root[data-lab-scheme="light"]) {
|
|
1742
|
+
--color-background: var(--color-light-background);
|
|
1743
|
+
--color-foreground: var(--color-light-foreground);
|
|
1744
|
+
--color-accent: var(--color-light-accent);
|
|
1745
|
+
--color-header: var(--color-light-header, var(--color-foreground));
|
|
1746
|
+
--color-surface: color-mix(
|
|
1747
|
+
in srgb,
|
|
1748
|
+
var(--color-background) 100%,
|
|
1749
|
+
transparent
|
|
1750
|
+
);
|
|
1751
|
+
}
|
|
1752
|
+
/* Combobox styles tuned for the Lab panel — make the combobox visually match lab inputs */
|
|
1753
|
+
:global(.lab-font-combobox .std-combobox-trigger) {
|
|
1754
|
+
width: 100%;
|
|
1755
|
+
padding: calc(var(--space-d3) - 2px) var(--space-d4);
|
|
1756
|
+
font-size: var(--size-2xs);
|
|
1757
|
+
line-height: 1;
|
|
1758
|
+
background: transparent;
|
|
1759
|
+
border: var(--border);
|
|
1760
|
+
border-radius: var(--radius);
|
|
1761
|
+
color: var(--color-foreground);
|
|
1762
|
+
box-shadow: var(--shadow-raised);
|
|
1763
|
+
text-align: left;
|
|
1764
|
+
display: inline-flex;
|
|
1765
|
+
align-items: center;
|
|
1766
|
+
justify-content: space-between;
|
|
1767
|
+
}
|
|
1768
|
+
|
|
1769
|
+
:global(.lab-font-combobox .std-combobox-trigger .truncate) {
|
|
1770
|
+
overflow: hidden;
|
|
1771
|
+
text-overflow: ellipsis;
|
|
1772
|
+
white-space: nowrap;
|
|
1773
|
+
}
|
|
1774
|
+
|
|
1775
|
+
:global(.lab-font-combobox .std-combobox-popover) {
|
|
1776
|
+
left: 0;
|
|
1777
|
+
right: 0;
|
|
1778
|
+
min-width: 100%;
|
|
1779
|
+
max-width: none;
|
|
1780
|
+
margin-top: var(--space-d2);
|
|
1781
|
+
border: var(--border);
|
|
1782
|
+
border-radius: var(--radius-sm);
|
|
1783
|
+
box-shadow: var(--shadow-xl), var(--shadow-ring);
|
|
1784
|
+
background: var(--color-surface, var(--color-background));
|
|
1785
|
+
z-index: 120;
|
|
1786
|
+
}
|
|
1787
|
+
|
|
1788
|
+
/* Make the search input inside the popover feel compact like lab inputs */
|
|
1789
|
+
:global(.lab-font-combobox .std-combobox-header) {
|
|
1790
|
+
padding: 0 var(--space-d4) var(--space-d3);
|
|
1791
|
+
margin-bottom: var(--space-d2);
|
|
1792
|
+
}
|
|
1793
|
+
:global(.lab-font-combobox .std-combobox-header input) {
|
|
1794
|
+
font-size: var(--size-sm);
|
|
1795
|
+
padding: var(--space-half) 0;
|
|
1796
|
+
width: 100%;
|
|
1797
|
+
background: transparent;
|
|
1798
|
+
border: none;
|
|
1799
|
+
outline: none;
|
|
1800
|
+
}
|
|
1801
|
+
|
|
1802
|
+
/* Ensure options are comfortably spaced in the Lab */
|
|
1803
|
+
:global(.lab-font-combobox .std-combobox-listbox .std-menu-item) {
|
|
1804
|
+
padding: var(--space-d2) var(--space-d4);
|
|
1805
|
+
font-size: var(--size-sm);
|
|
1806
|
+
}
|
|
1807
|
+
|
|
1808
|
+
/* Preserve the rest of the stylesheet closing tag */
|
|
1809
|
+
</style>
|