@stnd/lab 1.1.0 → 1.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.
@@ -1,1545 +0,0 @@
1
- (function () {
2
- /*!
3
- * Standard System Inspector - Minimal Debug Interface
4
- *
5
- * A minimalist debug interface for the Standard Framework
6
- * Philosophy: Felt, not seen. Present when needed, invisible when not.
7
- *
8
- * HTMX-COMPATIBLE VERSION: Includes a .refresh() method and stable IDs
9
- * to work with htmx:load events and hx-preserve.
10
- *
11
- * @version @VERSION_PLACEHOLDER@
12
- */
13
-
14
- class StandardLab {
15
- constructor() {
16
- this.state = "dormant"; // dormant, quick, full
17
- this.badge = null;
18
- this.panel = null;
19
- this.currentTab = "typography";
20
- this.googleFontsLoaded = new Set();
21
- this.colorScheme = this.detectColorScheme();
22
- this.modifiedTokens = new Set();
23
- this.position = this.loadPosition();
24
- this.version = document.documentElement.dataset.standardVersion;
25
- this.customThemes = []; // Store custom themes added by users
26
-
27
- // Slider configuration for numeric tokens
28
- this.sliderConfig = {
29
- "--optical-ratio": { min: 1, max: 2, step: 0.01, unit: "" },
30
- "--line-height": { min: 1, max: 2, step: 0.05, unit: "" },
31
- "--font-size": { min: 0, max: 2, step: 0.05, unit: "em" },
32
- "--ratio-golden": { min: 1, max: 2, step: 0.01, unit: "" },
33
- "--ratio-silver": { min: 1, max: 3, step: 0.01, unit: "" },
34
- "--ratio-wholestep": { min: 1, max: 3, step: 0.01, unit: "" },
35
- "--ratio-halfstep": { min: 1, max: 2, step: 0.01, unit: "" },
36
- "--font-weight": { min: 100, max: 900, step: 100, unit: "" },
37
- "--font-weight-bold": { min: 100, max: 900, step: 100, unit: "" },
38
- "--radius": { min: 0, max: 2, step: 0.05, unit: "rem" },
39
- "--line-width": { min: 0, max: 10, step: 0.5, unit: "px" },
40
- "--base": { min: 0.5, max: 3, step: 0.05, unit: "rem" },
41
- "--base-gap": { min: 0, max: 3, step: 0.05, unit: "rem" },
42
- "--base-block-gap": { min: 0, max: 3, step: 0.05, unit: "rem" },
43
- "--body-padding": { min: 0, max: 5, step: 0.25, unit: "rem" },
44
- "--gap-body": { min: 0, max: 4, step: 0.25, unit: "rem" },
45
- "--gap": { min: 0, max: 3, step: 0.05, unit: "rem" },
46
- "--grid-gap": { min: 0, max: 3, step: 0.05, unit: "rem" },
47
- };
48
-
49
- this.coreTokens = {
50
- typography: [
51
- "--font-text",
52
- "--font-header",
53
- "--font-monospace",
54
- "--font-interface",
55
- "--font-size",
56
- "--optical-ratio",
57
- "--line-height",
58
- "--font-weight",
59
- "--font-weight-bold",
60
- "--font-letter-spacing",
61
- "--font-header-letter-spacing",
62
- "--font-weight-header",
63
- ],
64
- layout: [
65
- "--line-width",
66
- "--line-width-xs",
67
- "--line-width-sm",
68
- "--line-width-md",
69
- "--line-width-lg",
70
- "--line-width-xl",
71
- "--base",
72
- "--base-gap",
73
- "--base-block-gap",
74
- "--body-padding",
75
- "--gap",
76
- "--grid-gap",
77
- ],
78
- colors: this.getColorTokens(),
79
- advanced: [
80
- "--ratio-golden",
81
- "--ratio-silver",
82
- "--ratio-wholestep",
83
- "--ratio-halfstep",
84
- "--radius",
85
- "--filter-blur",
86
- "--transition",
87
- "--line-width",
88
- "--shadow",
89
- "--border",
90
- ],
91
- };
92
-
93
- // NEW: CSS Variable configuration for StandardLab styling
94
- this.cssVars = {
95
- "--stnd-lab-bg":
96
- "color-mix( in srgb, var(--color-dark-background) 90%, var(--color-dark-foreground))",
97
- "--stnd-lab-text":
98
- "color-mix( in srgb, var(--color-dark-background) 10%, var(--color-dark-foreground))",
99
- "--stnd-lab-text-size": "12px",
100
- "--stnd-lab-opacity": "1",
101
- "--stnd-lab-accent": "var(--color-accent)",
102
- "--stnd-lab-border": "var(--border)",
103
- "--stnd-lab-shadow": "var(--shadow)",
104
- "--stnd-lab-blur": "var(--filter-blur)",
105
- "--stnd-lab-font-family": "var(--font-monospace)",
106
- };
107
-
108
- this.defaults = {};
109
- this.initCSSVars();
110
- this.init();
111
- }
112
-
113
- // NEW: Initialize CSS variables on the root element
114
- initCSSVars() {
115
- Object.entries(this.cssVars).forEach(([varName, value]) => {
116
- document.documentElement.style.setProperty(varName, value);
117
- });
118
- }
119
-
120
- /**
121
- * Set a custom CSS variable for StandardLab styling
122
- * @param {string} varName - Variable name (e.g., "--stnd-lab-bg")
123
- * @param {string} value - CSS value
124
- *
125
- * @example
126
- * window.StandardLab.setCSSVar("--stnd-lab-bg", "#1a1a1a");
127
- * window.StandardLab.setCSSVar("--stnd-lab-text", "#fff");
128
- * window.StandardLab.setCSSVar("--stnd-lab-text-size", "14px");
129
- * window.StandardLab.setCSSVar("--stnd-lab-opacity", "0.95");
130
- */
131
- setCSSVar(varName, value) {
132
- this.cssVars[varName] = value;
133
- document.documentElement.style.setProperty(varName, value);
134
-
135
- // Refresh panel if it's open to apply new styles
136
- if (this.state !== "dormant" && this.panel) {
137
- this.refresh();
138
- }
139
- }
140
-
141
- /**
142
- * Get a custom CSS variable value
143
- * @param {string} varName - Variable name
144
- * @returns {string} CSS value
145
- */
146
- getCSSVar(varName) {
147
- return this.cssVars[varName] || "";
148
- }
149
-
150
- init() {
151
- this.storeDefaults();
152
- this.createBadge();
153
- this.initKeyboardShortcuts();
154
-
155
- this.restoreTheme();
156
- this.restoreDebugMode(); // NEW: Restore debug mode from sessionStorage
157
- this.restorePanelState(); // NEW: Restore panel state from sessionStorage
158
-
159
- // Check if debug mode is already active OR if blueprint theme is active
160
- const currentTheme = document.documentElement.getAttribute("data-theme");
161
- const isDebugActive =
162
- document.documentElement.classList.contains("stnd-debug");
163
- const isBlueprintActive = currentTheme === "blueprint";
164
-
165
- if (isDebugActive || isBlueprintActive) {
166
- this.injectGridDebugOverlays();
167
- }
168
-
169
- if (window.matchMedia) {
170
- window
171
- .matchMedia("(prefers-color-scheme: dark)")
172
- .addEventListener("change", (e) => {
173
- this.colorScheme = e.matches ? "dark" : "light";
174
- if (this.state === "full") {
175
- this.refreshTokenList();
176
- }
177
- });
178
- }
179
- this.initModificationTracking();
180
- }
181
-
182
- restoreTheme() {
183
- // Use sessionStorage instead of localStorage for tab-specific persistence
184
- const savedTheme = sessionStorage.getItem("stnd-theme");
185
-
186
- if (savedTheme) {
187
- document.documentElement.setAttribute("data-theme", savedTheme);
188
- }
189
- }
190
-
191
- // NEW: Restore debug mode from sessionStorage
192
- restoreDebugMode() {
193
- const savedDebugMode = sessionStorage.getItem("stnd-debug-mode");
194
-
195
- if (savedDebugMode === "true") {
196
- document.documentElement.classList.add("stnd-debug");
197
- } else if (savedDebugMode === "false") {
198
- document.documentElement.classList.remove("stnd-debug");
199
- }
200
- }
201
-
202
- // NEW: Save debug mode to sessionStorage
203
- saveDebugMode(isEnabled) {
204
- sessionStorage.setItem("stnd-debug-mode", isEnabled.toString());
205
- }
206
-
207
- // NEW: Restore panel state from sessionStorage
208
- restorePanelState() {
209
- const savedPanelState = sessionStorage.getItem("stnd-panel-state");
210
-
211
- if (savedPanelState === "quick") {
212
- this.openQuick();
213
- } else if (savedPanelState === "full") {
214
- this.openFull();
215
- }
216
- }
217
-
218
- // NEW: Save panel state to sessionStorage
219
- savePanelState(state) {
220
- if (state === "dormant") {
221
- sessionStorage.removeItem("stnd-panel-state");
222
- } else {
223
- sessionStorage.setItem("stnd-panel-state", state);
224
- }
225
- }
226
-
227
- // Emit a window event when the panel state changes so external clients
228
- // (for example the dev toolbar) can stay in sync with the Lab visibility.
229
- dispatchStateEvent() {
230
- try {
231
- if (
232
- typeof window !== "undefined" &&
233
- typeof window.dispatchEvent === "function"
234
- ) {
235
- window.dispatchEvent(
236
- new CustomEvent("standardlab:state", {
237
- detail: { state: this.state },
238
- }),
239
- );
240
- }
241
- } catch (e) {
242
- console.warn("[StandardLab] dispatchStateEvent failed", e);
243
- }
244
- }
245
-
246
- /**
247
- * Add a custom theme to the theme selector
248
- * @param {string} value - Theme attribute value (e.g., "custom_dark")
249
- * @param {string} label - Display label (e.g., "Custom Dark")
250
- *
251
- * @example
252
- * window.StandardLab.addTheme("my-theme", "My Theme");
253
- */
254
- addTheme(value, label) {
255
- // Check if theme already exists
256
- const exists = this.customThemes.some((theme) => theme.value === value);
257
- if (!exists) {
258
- this.customThemes.push({ value, label });
259
-
260
- // Refresh panel if it's open
261
- if (this.state === "quick") {
262
- this.createQuickPanel();
263
- }
264
- }
265
- }
266
-
267
- storeDefaults() {
268
- const allTokens = [
269
- ...this.coreTokens.typography,
270
- ...this.coreTokens.layout,
271
- ...this.coreTokens.colors,
272
- ...this.coreTokens.advanced,
273
- ];
274
- allTokens.forEach((token) => {
275
- const value = getComputedStyle(document.documentElement)
276
- .getPropertyValue(token)
277
- .trim();
278
- if (value) {
279
- this.defaults[token] = value;
280
- }
281
- });
282
- }
283
-
284
- getColorTokens() {
285
- const scheme = this.colorScheme === "light" ? "light" : "dark";
286
- return [
287
- `--color-${scheme}-background`,
288
- `--color-${scheme}-foreground`,
289
- `--color-${scheme}-accent`,
290
- `--color-${scheme}-red`,
291
- `--color-${scheme}-orange`,
292
- `--color-${scheme}-yellow`,
293
- `--color-${scheme}-green`,
294
- `--color-${scheme}-cyan`,
295
- `--color-${scheme}-blue`,
296
- `--color-${scheme}-magenta`,
297
- ];
298
- }
299
-
300
- detectColorScheme() {
301
- if (
302
- window.matchMedia &&
303
- window.matchMedia("(prefers-color-scheme: dark)").matches
304
- ) {
305
- return "dark";
306
- }
307
- return "light";
308
- }
309
-
310
- loadPosition() {
311
- const saved = localStorage.getItem("standard-lab-position");
312
- if (saved) {
313
- return JSON.parse(saved);
314
- }
315
- return { bottom: 16, right: 16 };
316
- }
317
-
318
- savePosition() {
319
- localStorage.setItem(
320
- "standard-lab-position",
321
- JSON.stringify(this.position),
322
- );
323
- }
324
-
325
- createBadge() {
326
- if (this.badge) return;
327
- this.badge = document.createElement("button");
328
- this.badge.id = "standard-lab-badge-container";
329
- this.badge.className = "standard-lab-badge";
330
- this.badge.setAttribute("aria-label", "Open Standard Inspector");
331
- this.badge.setAttribute("title", "Standard Inspector (Shift+Cmd+S)");
332
- this.badge.setAttribute("style", "color:var(--color-subtle);");
333
- this.badge.style.bottom = `${this.position.bottom}px`;
334
- this.badge.style.right = `${this.position.right}px`;
335
- this.badge.innerHTML = ``;
336
- this.badge.addEventListener("click", () => this.openQuick());
337
- this.makeDraggable(this.badge);
338
-
339
- // Append to body
340
- document.body.appendChild(this.badge);
341
-
342
- this.updateBadge();
343
- this.injectStyles();
344
- }
345
-
346
- updateBadge() {
347
- if (!this.badge) return;
348
- const count = this.modifiedTokens.size;
349
- if (count > 0) {
350
- this.badge.setAttribute("data-modified", count);
351
- } else {
352
- this.badge.removeAttribute("data-modified");
353
- }
354
- }
355
-
356
- makeDraggable(element) {
357
- let isDragging = false,
358
- startX,
359
- startY,
360
- initialBottom,
361
- initialRight;
362
- const onMouseDown = (e) => {
363
- if (
364
- e.target !== element &&
365
- !element.classList.contains("standard-lab-panel-header")
366
- )
367
- return;
368
- isDragging = true;
369
- startX = e.clientX;
370
- startY = e.clientY;
371
- const rect = element.getBoundingClientRect();
372
- initialBottom = window.innerHeight - rect.bottom;
373
- initialRight = window.innerWidth - rect.right;
374
- document.addEventListener("mousemove", onMouseMove);
375
- document.addEventListener("mouseup", onMouseUp);
376
- element.style.cursor = "grabbing";
377
- e.preventDefault();
378
- };
379
- const onMouseMove = (e) => {
380
- if (!isDragging) return;
381
- const deltaX = startX - e.clientX;
382
- const deltaY = e.clientY - startY;
383
- this.position.right = Math.max(0, initialRight + deltaX);
384
- this.position.bottom = Math.max(0, initialBottom - deltaY);
385
- element.style.right = `${this.position.right}px`;
386
- element.style.bottom = `${this.position.bottom}px`;
387
- };
388
- const onMouseUp = () => {
389
- isDragging = false;
390
- element.style.cursor = "";
391
- document.removeEventListener("mousemove", onMouseMove);
392
- document.removeEventListener("mouseup", onMouseUp);
393
- this.savePosition();
394
- };
395
- element.addEventListener("mousedown", onMouseDown);
396
- }
397
-
398
- openQuick() {
399
- if (this.state === "quick") {
400
- this.close();
401
- return;
402
- }
403
- this.state = "quick";
404
- this.savePanelState("quick");
405
- this.createQuickPanel();
406
- this.dispatchStateEvent();
407
- }
408
-
409
- openFull() {
410
- this.state = "full";
411
- this.savePanelState("full");
412
- this.createFullPanel();
413
- this.dispatchStateEvent();
414
- }
415
-
416
- close() {
417
- if (this.panel) {
418
- this.panel.style.opacity = "0";
419
- this.panel.style.transform = "translateY(10px)";
420
- setTimeout(() => {
421
- if (this.panel) {
422
- this.panel.remove();
423
- this.panel = null;
424
- }
425
- }, 200);
426
- }
427
- this.state = "dormant";
428
- this.savePanelState("dormant");
429
- this.dispatchStateEvent();
430
- }
431
-
432
- /**
433
- * Public method to refresh the panel's UI.
434
- * Ideal for calling after HTMX swaps change the page state (e.g., theme).
435
- */
436
- refresh() {
437
- console.log("refresh");
438
- this.storeDefaults();
439
-
440
- this.badge = null;
441
- this.init();
442
-
443
- // Re-inject overlays if debug is active
444
- if (document.documentElement.classList.contains("stnd-debug")) {
445
- this.removeGridDebugOverlays();
446
- this.injectGridDebugOverlays();
447
- }
448
-
449
- // Do nothing if the panel is closed
450
- if (this.state === "dormant") {
451
- return;
452
- }
453
-
454
- // Re-run the appropriate panel creation method to rebuild the UI
455
- if (this.state === "quick") {
456
- this.createQuickPanel();
457
- } else if (this.state === "full") {
458
- const showModifiedOnly =
459
- this.panel?.querySelector('[data-filter="modified"]')?.checked ||
460
- false;
461
- this.createFullPanel();
462
- this.switchTab(this.currentTab);
463
- const modifiedFilter = this.panel?.querySelector(
464
- '[data-filter="modified"]',
465
- );
466
- if (modifiedFilter) {
467
- modifiedFilter.checked = showModifiedOnly;
468
- this.refreshTokenList(showModifiedOnly);
469
- }
470
- }
471
- }
472
-
473
- /** Read current seed / token value, resolving computed style */
474
- _seedValue(token) {
475
- return (
476
- document.documentElement.style.getPropertyValue(token).trim() ||
477
- getComputedStyle(document.documentElement)
478
- .getPropertyValue(token)
479
- .trim()
480
- );
481
- }
482
-
483
- createQuickPanel() {
484
- if (this.panel) this.panel.remove();
485
- this.panel = document.createElement("div");
486
- this.panel.id = "standard-lab-panel-container";
487
- this.panel.className = "standard-lab-panel standard-lab-panel-quick";
488
- this.panel.style.bottom = `calc(${this.position.bottom}px + var(--space-2))`;
489
- this.panel.style.right = `${this.position.right}px`;
490
-
491
- const debugActive =
492
- document.documentElement.classList.contains("stnd-debug");
493
- const modifiedCount = this.modifiedTokens.size;
494
-
495
- // Current seed colors
496
- const lightBg = this._seedValue("--color-light-background");
497
- const lightFg = this._seedValue("--color-light-foreground");
498
- const lightBgHex = this.extractHexColor(lightBg);
499
- const lightFgHex = this.extractHexColor(lightFg);
500
-
501
- // Current fonts
502
- const fontText = this._seedValue("--font-text");
503
- const fontHeader = this._seedValue("--font-header");
504
-
505
- // Current ratio & padding
506
- const ratio = this._seedValue("--optical-ratio");
507
- const ratioNum = this.extractNumericValue(ratio) || 1.414;
508
- const gapBody = this._seedValue("--gap-body");
509
- const gapNum = this.extractNumericValue(gapBody) || 1;
510
-
511
- this.panel.innerHTML = `
512
- <header class="standard-lab-panel-header">
513
- <div style="display:inline-flex;align-items:center;gap:6px;">
514
- <h2 style="color:var(--color-dark-foreground);display:inline-block;">Lab v${this.version}</h2>
515
- <div class="standard-lab-viewport-info" style="display:inline-block;">${window.innerWidth}px</div>
516
- </div>
517
- <button class="standard-lab-btn-icon" data-action="close" title="Close (Esc)">×</button>
518
- </header>
519
-
520
- <!-- Theme -->
521
- <section class="standard-lab-section">
522
- <label class="standard-lab-label">Theme</label>
523
- <select class="standard-lab-select" data-action="theme">
524
- ${this.getThemeOptions()}
525
- </select>
526
- </section>
527
-
528
- <!-- Seed Colors -->
529
- <section class="standard-lab-section">
530
- <label class="standard-lab-label">Colors</label>
531
- <div class="standard-lab-seed-row">
532
- <input type="color" class="standard-lab-color-swatch" data-seed="--color-light-background" value="${lightBgHex}" title="Light background">
533
- <input type="text" class="standard-lab-seed-input" data-seed="--color-light-background" value="${lightBg}" spellcheck="false" placeholder="background">
534
- <span class="standard-lab-seed-tag">bg</span>
535
- </div>
536
- <div class="standard-lab-seed-row">
537
- <input type="color" class="standard-lab-color-swatch" data-seed="--color-light-foreground" value="${lightFgHex}" title="Light foreground">
538
- <input type="text" class="standard-lab-seed-input" data-seed="--color-light-foreground" value="${lightFg}" spellcheck="false" placeholder="foreground">
539
- <span class="standard-lab-seed-tag">fg</span>
540
- </div>
541
- </section>
542
-
543
- <!-- Typography -->
544
- <section class="standard-lab-section">
545
- <label class="standard-lab-label">Typography</label>
546
- <div class="standard-lab-font-row">
547
- <span class="standard-lab-font-tag">Body</span>
548
- <input type="text" class="standard-lab-seed-input" data-seed="--font-text" value="${fontText}" spellcheck="false" placeholder="Font family">
549
- </div>
550
- <div class="standard-lab-font-row">
551
- <span class="standard-lab-font-tag">Head</span>
552
- <input type="text" class="standard-lab-seed-input" data-seed="--font-header" value="${fontHeader}" spellcheck="false" placeholder="Font family">
553
- </div>
554
- </section>
555
-
556
- <!-- Scale -->
557
- <section class="standard-lab-section">
558
- <label class="standard-lab-label">Scale</label>
559
- <div class="standard-lab-quick-slider">
560
- <span class="standard-lab-font-tag">Ratio</span>
561
- <input type="range" class="standard-lab-slider-input" data-seed="--optical-ratio"
562
- value="${ratioNum}" min="1" max="2" step="0.01">
563
- <span class="standard-lab-slider-readout" data-readout="--optical-ratio">${ratioNum}</span>
564
- </div>
565
- <div class="standard-lab-quick-slider">
566
- <span class="standard-lab-font-tag">Gap</span>
567
- <input type="range" class="standard-lab-slider-input" data-seed="--gap-body"
568
- value="${gapNum}" min="0" max="4" step="0.25">
569
- <span class="standard-lab-slider-readout" data-readout="--gap-body">${gapNum}rem</span>
570
- </div>
571
- </section>
572
-
573
- <!-- Debug -->
574
- <section class="standard-lab-section">
575
- <label class="standard-lab-toggle">
576
- <input type="checkbox" ${debugActive ? "checked" : ""} data-toggle="debug">
577
- <span>Debug Mode</span>
578
- <kbd>⇧⌘D</kbd>
579
- </label>
580
- </section>
581
-
582
- <!-- Actions -->
583
- <div class="standard-lab-actions">
584
- <button class="standard-lab-btn" data-action="edit-tokens">
585
- ${modifiedCount > 0 ? `All Tokens (${modifiedCount})` : "All Tokens"}
586
- </button>
587
- ${modifiedCount > 0 ? `<button class="standard-lab-btn standard-lab-btn-secondary" data-action="reset">Reset</button>` : ""}
588
- <button class="standard-lab-btn standard-lab-btn-secondary" data-action="copy">Copy CSS</button>
589
- </div>
590
- `;
591
- this.attachQuickPanelListeners();
592
- document.body.appendChild(this.panel);
593
- requestAnimationFrame(() => {
594
- this.panel.style.opacity = "1";
595
- this.panel.style.transform = "translateY(0)";
596
- });
597
- }
598
-
599
- getThemeOptions() {
600
- const defaultThemes = [
601
- { value: "", label: "Empty" },
602
- { value: "default", label: "Default" },
603
-
604
- { value: "system", label: "System" },
605
- { value: "claude", label: "Claude" },
606
- { value: "bauhaus", label: "Bauhaus" },
607
- { value: "venetian", label: "Venetian" },
608
- { value: "newspaper", label: "Newspaper" },
609
- { value: "didone", label: "Didone" },
610
- { value: "transitional", label: "Transitional" },
611
-
612
- { value: "english-oldstyle", label: "English Oldstyle" },
613
-
614
- { value: "paper", label: "Paper" },
615
- { value: "swiss", label: "Swiss" },
616
- { value: "kernel", label: "Kernel" },
617
- { value: "forest", label: "Forest" },
618
- { value: "book", label: "Book" },
619
- { value: "occult", label: "Occult" },
620
- { value: "baroque", label: "Baroque" },
621
- { value: "botanist", label: "Botanist" },
622
- { value: "chalky", label: "Chalky" },
623
- { value: "minimal", label: "Minimal" },
624
- { value: "marauder", label: "Marauder" },
625
- { value: "blueprint", label: "Blueprint" },
626
- { value: "custom_1", label: "Custom 1" },
627
- { value: "custom_2", label: "Custom 2" },
628
- { value: "custom_3", label: "Custom 3" },
629
- ];
630
-
631
- // Merge default themes with custom themes
632
- const allThemes = [...defaultThemes, ...this.customThemes];
633
-
634
- const current = document.documentElement.getAttribute("data-theme") || "";
635
- return allThemes
636
- .map(
637
- (theme) =>
638
- `<option value="${theme.value}" ${current === theme.value ? "selected" : ""}>${theme.label}</option>`,
639
- )
640
- .join("");
641
- }
642
-
643
- attachQuickPanelListeners() {
644
- // Close / minimize / full-panel
645
- this.panel
646
- .querySelector('[data-action="close"]')
647
- ?.addEventListener("click", () => this.close());
648
- this.panel
649
- .querySelector('[data-action="edit-tokens"]')
650
- ?.addEventListener("click", () => this.openFull());
651
- this.panel
652
- .querySelector('[data-action="reset"]')
653
- ?.addEventListener("click", () => this.resetAll());
654
- this.panel
655
- .querySelector('[data-action="copy"]')
656
- ?.addEventListener("click", () => this.copyCSS());
657
-
658
- // Debug toggle
659
- this.panel
660
- .querySelector('[data-toggle="debug"]')
661
- ?.addEventListener("change", (e) => {
662
- const isEnabled = e.target.checked;
663
- document.documentElement.classList.toggle("stnd-debug", isEnabled);
664
- this.saveDebugMode(isEnabled);
665
-
666
- const currentTheme =
667
- document.documentElement.getAttribute("data-theme");
668
- const isBlueprintActive = currentTheme === "blueprint";
669
-
670
- if (isEnabled || isBlueprintActive) {
671
- this.injectGridDebugOverlays();
672
- } else {
673
- this.removeGridDebugOverlays();
674
- }
675
- window.toast?.info(
676
- `Debug mode ${e.target.checked ? "enabled" : "disabled"}`,
677
- );
678
- });
679
-
680
- // Theme selector
681
- this.panel
682
- .querySelector('[data-action="theme"]')
683
- ?.addEventListener("change", (e) => {
684
- const theme = e.target.value;
685
- if (theme) {
686
- document.documentElement.setAttribute("data-theme", theme);
687
- } else {
688
- document.documentElement.removeAttribute("data-theme");
689
- }
690
- sessionStorage.setItem("stnd-theme", theme);
691
- this.handleBlueprintDebugException(theme);
692
-
693
- // Re-read seed values after theme switch and update controls
694
- this._syncQuickSeedInputs();
695
- window.toast?.info(`Theme: ${theme || "Default"}`);
696
- });
697
-
698
- // Seed color pickers (swatch)
699
- this.panel
700
- .querySelectorAll(".standard-lab-color-swatch")
701
- .forEach((swatch) => {
702
- swatch.addEventListener("input", (e) => {
703
- const token = e.target.dataset.seed;
704
- const color = e.target.value;
705
- this.updateToken(token, color);
706
- // Sync companion text input
707
- const textInput = this.panel.querySelector(
708
- `.standard-lab-seed-input[data-seed="${token}"]`,
709
- );
710
- if (textInput) textInput.value = color;
711
- });
712
- });
713
-
714
- // Seed color text inputs
715
- this.panel
716
- .querySelectorAll(
717
- '.standard-lab-seed-input[data-seed^="--color-light-"]',
718
- )
719
- .forEach((input) => {
720
- input.addEventListener("input", (e) => {
721
- const token = e.target.dataset.seed;
722
- const value = e.target.value;
723
- this.updateToken(token, value);
724
- // Sync companion swatch
725
- const swatch = this.panel.querySelector(
726
- `.standard-lab-color-swatch[data-seed="${token}"]`,
727
- );
728
- if (swatch && this.isColorValue(value)) {
729
- swatch.value = this.extractHexColor(value);
730
- }
731
- });
732
- });
733
-
734
- // Font family inputs
735
- this.panel
736
- .querySelectorAll('.standard-lab-seed-input[data-seed^="--font-"]')
737
- .forEach((input) => {
738
- input.addEventListener("input", (e) => {
739
- const token = e.target.dataset.seed;
740
- const value = e.target.value;
741
- this.updateToken(token, value);
742
- this.loadGoogleFont(value);
743
- });
744
- });
745
-
746
- // Quick sliders (ratio, gap)
747
- this.panel
748
- .querySelectorAll(
749
- ".standard-lab-quick-slider .standard-lab-slider-input",
750
- )
751
- .forEach((slider) => {
752
- slider.addEventListener("input", (e) => {
753
- const token = e.target.dataset.seed;
754
- const num = e.target.value;
755
- const config = this.sliderConfig[token];
756
- const unit = config?.unit || "";
757
- const fullValue = `${num}${unit}`;
758
- this.updateToken(token, fullValue);
759
- // Sync readout
760
- const readout = this.panel.querySelector(
761
- `.standard-lab-slider-readout[data-readout="${token}"]`,
762
- );
763
- if (readout) readout.textContent = fullValue || num;
764
- });
765
- });
766
- }
767
-
768
- /** Re-read seed values from computed style and sync quick-panel inputs */
769
- _syncQuickSeedInputs() {
770
- if (!this.panel) return;
771
- // Small delay so theme CSS cascades before we read computed values
772
- requestAnimationFrame(() => {
773
- const seeds = [
774
- "--color-light-background",
775
- "--color-light-foreground",
776
- "--font-text",
777
- "--font-header",
778
- ];
779
- seeds.forEach((token) => {
780
- const val = this._seedValue(token);
781
- const textInput = this.panel?.querySelector(
782
- `.standard-lab-seed-input[data-seed="${token}"]`,
783
- );
784
- if (textInput) textInput.value = val;
785
- const swatch = this.panel?.querySelector(
786
- `.standard-lab-color-swatch[data-seed="${token}"]`,
787
- );
788
- if (swatch && this.isColorValue(val)) {
789
- swatch.value = this.extractHexColor(val);
790
- }
791
- });
792
- // Sliders
793
- ["--optical-ratio", "--gap-body"].forEach((token) => {
794
- const val = this._seedValue(token);
795
- const num = this.extractNumericValue(val);
796
- const slider = this.panel?.querySelector(
797
- `.standard-lab-slider-input[data-seed="${token}"]`,
798
- );
799
- if (slider) slider.value = num;
800
- const readout = this.panel?.querySelector(
801
- `.standard-lab-slider-readout[data-readout="${token}"]`,
802
- );
803
- if (readout) {
804
- const config = this.sliderConfig[token];
805
- readout.textContent = `${num}${config?.unit || ""}`;
806
- }
807
- });
808
- });
809
- }
810
- /**
811
- * Handle the blueprint theme exception for debug grid overlay
812
- * @param {string} theme - Current theme name
813
- */
814
- handleBlueprintDebugException(theme) {
815
- const isBlueprint = theme === "blueprint";
816
- const hasDebugClass =
817
- document.documentElement.classList.contains("stnd-debug");
818
-
819
- if (isBlueprint && !hasDebugClass) {
820
- // Blueprint theme activated - show grid overlays
821
- this.injectGridDebugOverlays();
822
- } else if (!isBlueprint && !hasDebugClass) {
823
- // Non-blueprint theme and debug not active - remove overlays
824
- this.removeGridDebugOverlays();
825
- }
826
- // If debug is explicitly active, let it control the overlays
827
- }
828
- createFullPanel() {
829
- if (this.panel) this.panel.remove();
830
- this.panel = document.createElement("div");
831
- this.panel.id = "standard-lab-panel-container";
832
- this.panel.className = "standard-lab-panel standard-lab-panel-full";
833
- this.panel.style.bottom = `calc(${this.position.bottom}px + var(--space-2))`;
834
- this.panel.style.right = `${this.position.right}px`;
835
- const modifiedCount = this.modifiedTokens.size;
836
- const showModifiedOnly = modifiedCount > 0;
837
- this.panel.innerHTML = `
838
- <header class="standard-lab-panel-header" style="cursor: grab;">
839
- <h2 style="display:inline-block;">Token Editor</h2>
840
- <div class="standard-lab-header-actions">
841
- <button class="standard-lab-btn-icon" data-action="minimize" title="Minimize">−</button>
842
- <button class="standard-lab-btn-icon" data-action="close" title="Close (Esc)">×</button>
843
- </div>
844
- </header>
845
- <nav class="standard-lab-tabs">
846
- <button class="standard-lab-tab" data-tab="typography" aria-selected="true">Typography</button>
847
- <button class="standard-lab-tab" data-tab="layout">Layout</button>
848
- <button class="standard-lab-tab" data-tab="colors">Colors</button>
849
- <button class="standard-lab-tab" data-tab="advanced">Advanced</button>
850
- </nav>
851
- ${modifiedCount > 0 ? `<div class="standard-lab-filter"><label class="standard-lab-toggle"><input type="checkbox" ${showModifiedOnly ? "checked" : ""} data-filter="modified"><span>Show modified only (${modifiedCount})</span></label></div>` : ""}
852
- <div class="standard-lab-content">${this.renderTokenList("typography", showModifiedOnly)}</div>
853
- <div class="standard-lab-actions">
854
- <button class="standard-lab-btn standard-lab-btn-secondary" data-action="reset">Reset All</button>
855
- <button class="standard-lab-btn" data-action="copy">Copy CSS</button>
856
- </div>
857
- `;
858
- this.attachFullPanelListeners();
859
- this.makeDraggable(
860
- this.panel.querySelector(".standard-lab-panel-header"),
861
- );
862
- document.body.appendChild(this.panel);
863
- requestAnimationFrame(() => {
864
- this.panel.style.opacity = "1";
865
- this.panel.style.transform = "translateY(0)";
866
- });
867
- }
868
-
869
- renderTokenList(category, modifiedOnly = false) {
870
- const tokens = this.coreTokens[category] || [];
871
- let filteredTokens = modifiedOnly
872
- ? tokens.filter((token) => this.modifiedTokens.has(token))
873
- : tokens;
874
- if (filteredTokens.length === 0) {
875
- return `<div class="standard-lab-empty">No tokens to display</div>`;
876
- }
877
- return `<div class="standard-lab-token-list">${filteredTokens.map((token) => this.renderTokenRow(token)).join("")}</div>`;
878
- }
879
-
880
- /**
881
- * Check if a value is numeric (with optional unit)
882
- */
883
- isNumericValue(value) {
884
- if (!value) return false;
885
- // Match patterns like: 1.5, 1.5rem, 16px, 1.618, 500
886
- return /^-?\d+(\.\d+)?(rem|em|px|%|ch|vh|vw)?$/.test(value.trim());
887
- }
888
-
889
- /**
890
- * Extract numeric portion from a CSS value
891
- */
892
- extractNumericValue(value) {
893
- if (!value) return 0;
894
- const match = value.match(/^(-?\d+(\.\d+)?)/);
895
- return match ? parseFloat(match[1]) : 0;
896
- }
897
-
898
- /**
899
- * Extract unit from a CSS value (rem, px, em, etc.)
900
- */
901
- extractUnit(value) {
902
- if (!value) return "";
903
- const match = value.match(/[a-z%]+$/i);
904
- return match ? match[0] : "";
905
- }
906
-
907
- renderTokenRow(token) {
908
- const value = this.getPropertyValue(token);
909
- const isModified = this.modifiedTokens.has(token);
910
- const isColor = token.includes("color") && this.isColorValue(value);
911
- const sliderConfig = this.sliderConfig[token];
912
-
913
- // Detect if this is a slider-enabled numeric token
914
- const isSlider = sliderConfig && this.isNumericValue(value);
915
-
916
- if (isSlider) {
917
- const numericValue = this.extractNumericValue(value);
918
- return `
919
- <div class="standard-lab-token-row ${isModified ? "is-modified" : ""}">
920
- <div class="standard-lab-slider-container">
921
- <label class="standard-lab-slider-label">${token}</label>
922
- <div class="standard-lab-slider-controls">
923
- <input
924
- type="range"
925
- class="standard-lab-slider-input"
926
- data-token="${token}"
927
- value="${numericValue}"
928
- min="${sliderConfig.min}"
929
- max="${sliderConfig.max}"
930
- step="${sliderConfig.step}"
931
- title="${token}">
932
- <input
933
- type="text"
934
- class="standard-lab-text-input standard-lab-slider-value"
935
- data-token="${token}"
936
- value="${value}"
937
- spellcheck="false">
938
- </div>
939
- </div>
940
- <button class="standard-lab-btn-icon standard-lab-btn-reset" data-action="reset-token" data-token="${token}" title="Reset" ${!isModified ? "disabled" : ""}>↺</button>
941
- </div>
942
- `;
943
- }
944
-
945
- if (isColor) {
946
- return `
947
- <div class="standard-lab-token-row ${isModified ? "is-modified" : ""}">
948
- <div class="standard-lab-color-container">
949
- <label class="standard-lab-color-label">${token}</label>
950
- <div class="standard-lab-color-controls">
951
- <input
952
- type="color"
953
- class="standard-lab-color-input"
954
- data-token="${token}"
955
- value="${this.extractHexColor(value)}"
956
- title="Pick color">
957
- <input
958
- type="text"
959
- class="standard-lab-text-input standard-lab-color-value"
960
- data-token="${token}"
961
- value="${value}"
962
- placeholder="${token}"
963
- spellcheck="false">
964
- </div>
965
- </div>
966
- <button class="standard-lab-btn-icon standard-lab-btn-reset" data-action="reset-token" data-token="${token}" title="Reset" ${!isModified ? "disabled" : ""}>↺</button>
967
- </div>
968
- `;
969
- }
970
-
971
- // Default text input (for font names, etc.)
972
- return `
973
- <div class="standard-lab-token-row ${isModified ? "is-modified" : ""}">
974
- <div class="standard-lab-text-container">
975
- <label class="standard-lab-text-label">${token}</label>
976
- <input type="text" class="standard-lab-text-input" data-token="${token}" value="${value}" placeholder="${token}" spellcheck="false">
977
- </div>
978
- <button class="standard-lab-btn-icon standard-lab-btn-reset" data-action="reset-token" data-token="${token}" title="Reset" ${!isModified ? "disabled" : ""}>↺</button>
979
- </div>
980
- `;
981
- }
982
-
983
- attachFullPanelListeners() {
984
- this.panel
985
- .querySelector('[data-action="close"]')
986
- ?.addEventListener("click", () => this.close());
987
- this.panel
988
- .querySelector('[data-action="minimize"]')
989
- ?.addEventListener("click", () => this.openQuick());
990
- this.panel.querySelectorAll(".standard-lab-tab").forEach((tab) => {
991
- tab.addEventListener("click", (e) =>
992
- this.switchTab(e.target.dataset.tab),
993
- );
994
- });
995
- this.panel
996
- .querySelector('[data-filter="modified"]')
997
- ?.addEventListener("change", (e) =>
998
- this.refreshTokenList(e.target.checked),
999
- );
1000
-
1001
- // Slider input listeners
1002
- this.panel
1003
- .querySelectorAll(".standard-lab-slider-input")
1004
- .forEach((slider) => {
1005
- slider.addEventListener("input", (e) => {
1006
- const token = e.target.dataset.token;
1007
- const numericValue = e.target.value;
1008
- const config = this.sliderConfig[token];
1009
- const unit =
1010
- config.unit || this.extractUnit(this.getPropertyValue(token));
1011
- const fullValue = `${numericValue}${unit}`;
1012
-
1013
- this.updateToken(token, fullValue);
1014
-
1015
- // Update the text input to reflect slider value
1016
- const textInput = this.panel.querySelector(
1017
- `.standard-lab-slider-value[data-token="${token}"]`,
1018
- );
1019
- if (textInput) textInput.value = fullValue;
1020
- });
1021
- });
1022
-
1023
- // Text input for sliders (allows manual override)
1024
- this.panel
1025
- .querySelectorAll(".standard-lab-slider-value")
1026
- .forEach((input) => {
1027
- input.addEventListener("input", (e) => {
1028
- const token = e.target.dataset.token;
1029
- const value = e.target.value;
1030
- const numericValue = this.extractNumericValue(value);
1031
-
1032
- this.updateToken(token, value);
1033
-
1034
- // Sync slider position
1035
- const slider = this.panel.querySelector(
1036
- `.standard-lab-slider-input[data-token="${token}"]`,
1037
- );
1038
- if (slider) slider.value = numericValue;
1039
- });
1040
- });
1041
-
1042
- // Regular text inputs
1043
- this.panel
1044
- .querySelectorAll(
1045
- ".standard-lab-text-input:not(.standard-lab-slider-value):not(.standard-lab-color-value)",
1046
- )
1047
- .forEach((input) => {
1048
- input.addEventListener("input", (e) =>
1049
- this.updateToken(e.target.dataset.token, e.target.value),
1050
- );
1051
- });
1052
-
1053
- // Color pickers
1054
- this.panel
1055
- .querySelectorAll(".standard-lab-color-input")
1056
- .forEach((input) => {
1057
- input.addEventListener("input", (e) => {
1058
- const token = e.target.dataset.token;
1059
- const color = e.target.value;
1060
- this.updateToken(token, color);
1061
- const textInput = this.panel.querySelector(
1062
- `.standard-lab-color-value[data-token="${token}"]`,
1063
- );
1064
- if (textInput) textInput.value = color;
1065
- });
1066
- });
1067
-
1068
- // Color text inputs
1069
- this.panel
1070
- .querySelectorAll(".standard-lab-color-value")
1071
- .forEach((input) => {
1072
- input.addEventListener("input", (e) => {
1073
- const token = e.target.dataset.token;
1074
- const value = e.target.value;
1075
- this.updateToken(token, value);
1076
- const colorInput = this.panel.querySelector(
1077
- `.standard-lab-color-input[data-token="${token}"]`,
1078
- );
1079
- if (colorInput && this.isColorValue(value)) {
1080
- colorInput.value = this.extractHexColor(value);
1081
- }
1082
- });
1083
- });
1084
-
1085
- // Reset buttons
1086
- this.panel
1087
- .querySelectorAll('[data-action="reset-token"]')
1088
- .forEach((btn) => {
1089
- btn.addEventListener("click", (e) =>
1090
- this.resetToken(e.target.dataset.token),
1091
- );
1092
- });
1093
- this.panel
1094
- .querySelector('[data-action="reset"]')
1095
- ?.addEventListener("click", () => this.resetAll());
1096
- this.panel
1097
- .querySelector('[data-action="copy"]')
1098
- ?.addEventListener("click", () => this.copyCSS());
1099
- }
1100
-
1101
- switchTab(category) {
1102
- this.currentTab = category;
1103
- this.panel.querySelectorAll(".standard-lab-tab").forEach((tab) => {
1104
- tab.setAttribute("aria-selected", tab.dataset.tab === category);
1105
- });
1106
- const showModifiedOnly =
1107
- this.panel.querySelector('[data-filter="modified"]')?.checked || false;
1108
- this.refreshTokenList(showModifiedOnly);
1109
- }
1110
-
1111
- refreshTokenList(modifiedOnly = false) {
1112
- const content = this.panel?.querySelector(".standard-lab-content");
1113
- if (!content) return;
1114
- content.innerHTML = this.renderTokenList(this.currentTab, modifiedOnly);
1115
- this.attachFullPanelListeners();
1116
- }
1117
-
1118
- updateToken(token, value) {
1119
- document.documentElement.style.setProperty(token, value);
1120
- if (value !== this.defaults[token]) {
1121
- this.modifiedTokens.add(token);
1122
- } else {
1123
- this.modifiedTokens.delete(token);
1124
- }
1125
- this.updateBadge();
1126
- const resetBtn = this.panel?.querySelector(
1127
- `[data-action="reset-token"][data-token="${token}"]`,
1128
- );
1129
- if (resetBtn) resetBtn.disabled = !this.modifiedTokens.has(token);
1130
- const row = this.panel?.querySelector(
1131
- `.standard-lab-token-row:has([data-token="${token}"])`,
1132
- );
1133
- if (row)
1134
- row.classList.toggle("is-modified", this.modifiedTokens.has(token));
1135
- if (
1136
- token.includes("font-") &&
1137
- !token.includes("font-size") &&
1138
- !token.includes("font-ratio")
1139
- ) {
1140
- this.loadGoogleFont(value);
1141
- }
1142
- }
1143
-
1144
- resetToken(token) {
1145
- const defaultValue = this.defaults[token];
1146
- if (defaultValue) {
1147
- document.documentElement.style.setProperty(token, defaultValue);
1148
- this.modifiedTokens.delete(token);
1149
- const input = this.panel?.querySelector(`[data-token="${token}"]`);
1150
- if (input) input.value = defaultValue;
1151
- this.updateBadge();
1152
- this.refreshTokenList();
1153
- window.toast?.info(`Reset ${token}`);
1154
- }
1155
- }
1156
-
1157
- resetAll() {
1158
- if (!confirm("Reset all tokens to defaults?")) return;
1159
- document.documentElement.removeAttribute("style");
1160
- this.modifiedTokens.clear();
1161
- this.googleFontsLoaded.clear();
1162
- this.updateBadge();
1163
- if (this.state === "full") {
1164
- this.refreshTokenList();
1165
- }
1166
- window.toast?.info("All tokens reset");
1167
- }
1168
-
1169
- copyCSS() {
1170
- const allTokens = [
1171
- ...this.coreTokens.typography,
1172
- ...this.coreTokens.layout,
1173
- ...this.coreTokens.colors,
1174
- ...this.coreTokens.advanced,
1175
- ];
1176
- const css = `:root {\n${allTokens.map((token) => ` ${token}: ${this.getPropertyValue(token)};`).join("\n")}\n}`;
1177
- navigator.clipboard
1178
- .writeText(css)
1179
- .then(() => window.toast?.success("CSS copied to clipboard!"))
1180
- .catch(() => {
1181
- const textarea = document.createElement("textarea");
1182
- textarea.value = css;
1183
- textarea.style.position = "fixed";
1184
- textarea.style.opacity = "0";
1185
- document.body.appendChild(textarea);
1186
- textarea.select();
1187
- document.execCommand("copy");
1188
- document.body.removeChild(textarea);
1189
- window.toast?.success("CSS copied to clipboard!");
1190
- });
1191
- }
1192
-
1193
- getPropertyValue(token) {
1194
- const value = getComputedStyle(document.documentElement)
1195
- .getPropertyValue(token)
1196
- .trim();
1197
- return (
1198
- value || document.documentElement.style.getPropertyValue(token).trim()
1199
- );
1200
- }
1201
-
1202
- isColorValue(value) {
1203
- return value
1204
- ? value.startsWith("#") ||
1205
- value.startsWith("rgb") ||
1206
- value.startsWith("hsl") ||
1207
- value.startsWith("oklch") ||
1208
- value.includes("color-mix")
1209
- : false;
1210
- }
1211
-
1212
- extractHexColor(value) {
1213
- if (!value) return "#000000";
1214
- const hexMatch = value.match(/#[0-9a-f]{6}/i);
1215
- if (hexMatch) return hexMatch[0];
1216
- const rgbMatch = value.match(/rgb\((\d+),\s*(\d+),\s*(\d+)\)/);
1217
- if (rgbMatch) {
1218
- const [, r, g, b] = rgbMatch;
1219
- return (
1220
- "#" +
1221
- [r, g, b]
1222
- .map((x) => parseInt(x).toString(16).padStart(2, "0"))
1223
- .join("")
1224
- );
1225
- }
1226
- const colorMap = {
1227
- white: "#ffffff",
1228
- black: "#000000",
1229
- red: "#ff0000",
1230
- green: "#00ff00",
1231
- blue: "#0000ff",
1232
- yellow: "#ffff00",
1233
- orange: "#ffa500",
1234
- purple: "#800080",
1235
- pink: "#ffc0cb",
1236
- cyan: "#00ffff",
1237
- };
1238
- return colorMap[value.toLowerCase()] || "#000000";
1239
- }
1240
-
1241
- loadGoogleFont(fontName) {
1242
- const cleanName = fontName.split(",")[0].trim().replace(/['"]/g, "");
1243
- const systemFonts = [
1244
- "system-ui",
1245
- "sans-serif",
1246
- "serif",
1247
- "monospace",
1248
- "-apple-system",
1249
- "BlinkMacSystemFont",
1250
- ];
1251
- if (
1252
- systemFonts.includes(cleanName) ||
1253
- this.googleFontsLoaded.has(cleanName)
1254
- )
1255
- return;
1256
- const fontUrl = `https://fonts.googleapis.com/css2?family=${cleanName.replace(/ /g, "+")}:wght@300;400;500;600;700;900&display=swap`;
1257
- if (
1258
- !document.querySelector(`link[href*="${cleanName.replace(/ /g, "+")}"]`)
1259
- ) {
1260
- const link = document.createElement("link");
1261
- link.rel = "stylesheet";
1262
- link.href = fontUrl;
1263
- document.head.appendChild(link);
1264
- this.googleFontsLoaded.add(cleanName);
1265
- }
1266
- }
1267
-
1268
- showNotification(message, type = "info") {
1269
- const notification = document.createElement("div");
1270
- notification.className = `standard-lab-notification standard-lab-notification-${type}`;
1271
- notification.textContent = message;
1272
- document.body.appendChild(notification);
1273
- requestAnimationFrame(() => {
1274
- notification.style.opacity = "1";
1275
- notification.style.transform = "translateY(0)";
1276
- });
1277
- setTimeout(() => {
1278
- notification.style.opacity = "0";
1279
- notification.style.transform = "translateY(-10px)";
1280
- setTimeout(() => notification.remove(), 200);
1281
- }, 2500);
1282
- }
1283
-
1284
- initKeyboardShortcuts() {
1285
- document.addEventListener("keydown", (e) => {
1286
- const isMac = navigator.platform.indexOf("Mac") > -1;
1287
- if (
1288
- e.shiftKey &&
1289
- (isMac ? e.metaKey : e.ctrlKey) &&
1290
- e.key.toLowerCase() === "s"
1291
- ) {
1292
- e.preventDefault();
1293
- this.state === "dormant" ? this.openQuick() : this.close();
1294
- }
1295
- if (
1296
- e.shiftKey &&
1297
- (isMac ? e.metaKey : e.ctrlKey) &&
1298
- e.key.toLowerCase() === "d"
1299
- ) {
1300
- e.preventDefault();
1301
-
1302
- document.documentElement.classList.toggle("stnd-debug");
1303
- const isActive =
1304
- document.documentElement.classList.contains("stnd-debug");
1305
-
1306
- // UPDATED: Save debug mode to sessionStorage
1307
- this.saveDebugMode(isActive);
1308
-
1309
- if (isActive) {
1310
- this.injectGridDebugOverlays();
1311
- } else {
1312
- this.removeGridDebugOverlays();
1313
- }
1314
-
1315
- window.toast?.info(`Debug mode ${isActive ? "enabled" : "disabled"}`);
1316
- }
1317
- if (e.key === "Escape" && this.state !== "dormant") {
1318
- this.close();
1319
- }
1320
- });
1321
- }
1322
-
1323
- initModificationTracking() {
1324
- const allTokens = [
1325
- ...this.coreTokens.typography,
1326
- ...this.coreTokens.layout,
1327
- ...this.coreTokens.colors,
1328
- ...this.coreTokens.advanced,
1329
- ];
1330
- allTokens.forEach((token) => {
1331
- const currentValue = this.getPropertyValue(token);
1332
- const defaultValue = this.defaults[token];
1333
- if (currentValue && defaultValue && currentValue !== defaultValue) {
1334
- this.modifiedTokens.add(token);
1335
- }
1336
- });
1337
- this.updateBadge();
1338
- }
1339
-
1340
- /**
1341
- * Inject grid debug overlays into all .prose containers
1342
- */
1343
- injectGridDebugOverlays() {
1344
- const proseContainers = document.querySelectorAll(".prose");
1345
-
1346
- proseContainers.forEach((prose) => {
1347
- // Check if overlay already exists
1348
- if (prose.querySelector(".grid-debug-overlay")) {
1349
- return;
1350
- }
1351
-
1352
- const overlay = document.createElement("div");
1353
- overlay.className = "grid-debug-overlay";
1354
- overlay.setAttribute("aria-hidden", "true");
1355
-
1356
- // Add 7 spans for grid columns
1357
- for (let i = 0; i < 7; i++) {
1358
- overlay.appendChild(document.createElement("span"));
1359
- }
1360
-
1361
- // Insert as first child
1362
- prose.insertBefore(overlay, prose.firstChild);
1363
- });
1364
- }
1365
-
1366
- /**
1367
- * Remove grid debug overlays from all .prose containers
1368
- */
1369
- removeGridDebugOverlays() {
1370
- document
1371
- .querySelectorAll(".grid-debug-overlay")
1372
- .forEach((overlay) => overlay.remove());
1373
- }
1374
-
1375
- injectStyles() {
1376
- if (document.getElementById("standard-lab-styles")) return;
1377
- const styles = document.createElement("style");
1378
- styles.id = "standard-lab-styles";
1379
- styles.textContent = `
1380
- .standard-lab-badge { font-family: var(--stnd-lab-font-family); position: fixed; width: var(--space-d2); height: var(--space-d2); border-radius: 50%; background: var(--color-surface); border: var(--border); box-shadow: var(--shadow); backdrop-filter: var(--filter-blur); cursor: pointer; z-index: var(--z-toast); line-height:1.5; display: flex; align-items: baseline; justify-content: center; color: var(--color-foreground); transition: all 0.2s ease; padding: 0; margin:0; }
1381
- .standard-lab-badge:hover { box-shadow: var(--shadow); transform: scale(1.05); color: var(--color-dark-accent); }
1382
- .standard-lab-badge:active { transform: scale(0.95); }
1383
- .standard-lab-badge[data-modified]::after { content: attr(data-modified); position: absolute; top: -4px; right: -4px; width: 20px; height: 20px; border-radius: 50%; background: var(--color-accent); color: var(--color-background); font-size: var(--scale-d3); display: flex; align-items: center; justify-content: center; font-weight: 600; border: var(--border); box-shadow:var(--shadow); }
1384
- .standard-lab-panel { position: fixed; background: var(--stnd-lab-bg); color: var(--stnd-lab-text); font-size: var(--stnd-lab-text-size); border: var(--stnd-lab-border); border-radius: 6px; box-shadow: var(--stnd-lab-shadow); backdrop-filter: var(--stnd-lab-blur); z-index: var(--z-toast); opacity: 0; transform: translateY(10px); transition: opacity 0.2s ease, transform 0.2s ease; display: flex; flex-direction: column; }
1385
- .standard-lab-panel * { font-family: var(--stnd-lab-font-family) !important; font-size: inherit; line-height:1.5; margin:0; }
1386
- .standard-lab-panel :is(label, input) { margin:0; }
1387
- .standard-lab-panel-quick { max-width: 300px; max-height: 80vh; overflow-y: auto; }
1388
- .standard-lab-panel-full { max-width: 360px; max-height: 600px; }
1389
- .standard-lab-panel-header { padding: 6px; border-bottom: var(--stnd-lab-border); display: flex; align-items: center; justify-content: space-between; flex-shrink: 0; }
1390
- .standard-lab-panel-header h2 { margin: 0; font-size: calc(var(--stnd-lab-text-size) * 1.167); font-weight: 600; letter-spacing: -0.02em; }
1391
- .standard-lab-viewport-info { font-size: calc(var(--stnd-lab-text-size) * 0.917); font-weight: 400; color: var(--stnd-lab-text); opacity: 0.7; margin-top: 2px; font-family: var(--font-monospace); }
1392
- .standard-lab-header-actions { display: flex; gap: 6px; }
1393
- .standard-lab-section { margin:0;padding: 4px; border-bottom: var(--stnd-lab-border); }
1394
- .standard-lab-section:last-of-type { border-bottom: none; }
1395
- .standard-lab-label { display: block; font-size: calc(var(--stnd-lab-text-size) * 0.917); font-weight: 600; color: var(--stnd-lab-text); opacity: 0.7; text-transform: uppercase; letter-spacing: 0.05em; margin-bottom: 6px; }
1396
- .standard-lab-toggle { display: flex; align-items: center; gap: 6px; cursor: pointer; user-select: none; }
1397
- .standard-lab-toggle input[type="checkbox"] { width: 16px; height: 16px; cursor: pointer; margin: 0; }
1398
- .standard-lab-toggle span { flex: 1; font-size: calc(var(--stnd-lab-text-size) * 1.167); }
1399
- .standard-lab-toggle kbd { font-size: calc(var(--stnd-lab-text-size) * 1); font-family: var(--font-monospace); color: var(--stnd-lab-text); opacity: 0.6; padding: 2px 6px; background: rgba(0,0,0,0.1); border-radius: 6px; }
1400
- .standard-lab-select { width: 100%; padding: 6px 3px; margin:0 ; background: var(--stnd-lab-bg); border: var(--stnd-lab-border); border-radius: 6px; font-size: calc(var(--stnd-lab-text-size) * 1.167); color: var(--stnd-lab-text); cursor: pointer; }
1401
- .standard-lab-actions { padding: 6px; border-top: var(--stnd-lab-border); display: flex; flex-direction: column; gap: 6px; flex-shrink: 0; }
1402
- .standard-lab-btn { font-size: calc(var(--stnd-lab-text-size) * 1.167); width: 100%; padding: 6px 3px; background: var(--stnd-lab-accent); color: var(--stnd-lab-bg); border: var(--stnd-lab-border); border-radius: 6px; font-weight: 600; cursor: pointer; transition: opacity 0.2s ease; }
1403
- .standard-lab-btn:hover { opacity: 0.9; }
1404
- .standard-lab-btn:active { opacity: 0.8; }
1405
- .standard-lab-btn-secondary { background: rgba(255,255,255,0.1); color: var(--stnd-lab-text); border: var(--stnd-lab-border); }
1406
- .standard-lab-btn-icon { font-size: calc(var(--stnd-lab-text-size) * 1.667); width: 24px; height: 24px; padding: 0; background: transparent; border: none; color: var(--stnd-lab-text); line-height: 1; cursor: pointer; border-radius: 6px; transition: all 0.2s ease; display: inline-flex; align-items: center; justify-content: center; opacity: 0.7; }
1407
- .standard-lab-btn-icon:hover { background: rgba(255,255,255,0.1); color: var(--stnd-lab-text); opacity: 1; }
1408
- .standard-lab-tabs { display: flex; border-bottom: var(--stnd-lab-border); flex-shrink: 0; }
1409
- .standard-lab-tab { flex: 1; padding: 6px; background: transparent; border: none; border-bottom: 2px solid transparent; font-size: calc(var(--stnd-lab-text-size) * 1.333); font-weight: 500; color: var(--stnd-lab-text); opacity: 0.6; cursor: pointer; transition: all 0.2s ease; }
1410
- .standard-lab-tab:hover { color: var(--stnd-lab-text); opacity: 1; background: rgba(255,255,255,0.05); }
1411
- .standard-lab-tab[aria-selected="true"] { color: var(--stnd-lab-accent); opacity: 1; border-bottom-color: var(--stnd-lab-accent); font-weight: 600; }
1412
- .standard-lab-filter { padding: 6px 6px; background: rgba(255,255,255,0.05); border-bottom: var(--stnd-lab-border); flex-shrink: 0; }
1413
- .standard-lab-content { flex: 1; overflow-y: auto; padding: 6px; }
1414
- .standard-lab-content::-webkit-scrollbar { width: 6px; }
1415
- .standard-lab-content::-webkit-scrollbar-track { background: transparent; }
1416
- .standard-lab-content::-webkit-scrollbar-thumb { background: rgba(255,255,255,0.2); border-radius: 6px; }
1417
- .standard-lab-content::-webkit-scrollbar-thumb:hover { background: rgba(255,255,255,0.3); }
1418
- .standard-lab-token-list { display: flex; flex-direction: column; gap: 3px; }
1419
- .standard-lab-token-row { display: grid; grid-template-columns: 1fr auto; gap: 6px; align-items: start; padding: 6px; border-radius: 6px; transition: background 0.2s ease; }
1420
- .standard-lab-token-row:hover { background: rgba(255,255,255,0.05); }
1421
- .standard-lab-token-row.is-modified { background: rgba(var(--stnd-lab-accent-rgb, 255), 0.05); }
1422
- .standard-lab-text-input { padding: 0; background: transparent; border: 1px solid transparent; border-radius: 6px; font-size: var(--stnd-lab-text-size); font-family: var(--font-monospace); color: var(--stnd-lab-text); min-width: 0; transition: all 0.2s ease; }
1423
- .standard-lab-text-input:focus { outline: none; background: rgba(255,255,255,0.05); border-color: var(--stnd-lab-accent); }
1424
- .standard-lab-color-container { width: 100%; }
1425
- .standard-lab-color-label { display: block; font-size: calc(var(--stnd-lab-text-size) * 0.917); font-family: var(--font-monospace); color: var(--stnd-lab-text); opacity: 0.7; margin-bottom: 6px; }
1426
- .standard-lab-color-controls { display: flex; gap: 6px; align-items: center; }
1427
- .standard-lab-color-input { width: 40px; height: 40px; border: var(--stnd-lab-border); border-radius: 6px; cursor: pointer; padding: 4px; background: rgba(255,255,255,0.05); flex-shrink: 0; }
1428
- .standard-lab-color-input::-webkit-color-swatch-wrapper { padding: 0; }
1429
- .standard-lab-color-input::-webkit-color-swatch { border: none; border-radius: 6px; }
1430
- .standard-lab-color-value { flex: 1; min-width: 0; padding: 0; font-family: var(--font-monospace); font-size: var(--stnd-lab-text-size); }
1431
- .standard-lab-text-container { width: 100%; }
1432
- .standard-lab-text-label { display: block; font-size: calc(var(--stnd-lab-text-size) * 0.917); font-family: var(--font-monospace); color: var(--stnd-lab-text); opacity: 0.7; margin-bottom: 6px; }
1433
- .standard-lab-slider-container { width: 100%; }
1434
- .standard-lab-slider-label { display: block; font-size: calc(var(--stnd-lab-text-size) * 0.917); font-family: var(--font-monospace); color: var(--stnd-lab-text); opacity: 0.7; margin-bottom: 6px; }
1435
- .standard-lab-slider-controls { display: flex; gap: 6px; align-items: center; }
1436
- .standard-lab-slider-input { flex: 1; height: 6px; -webkit-appearance: none; appearance: none; background: rgba(255,255,255,0.1); border-radius: 6px; outline: none; cursor: pointer; }
1437
- .standard-lab-slider-input::-webkit-slider-thumb { -webkit-appearance: none; appearance: none; width: 16px; height: 16px; background: var(--stnd-lab-accent); border-radius: 50%; cursor: grab; transition: transform 0.1s ease; }
1438
- .standard-lab-slider-input::-webkit-slider-thumb:active { cursor: grabbing; transform: scale(1.2); }
1439
- .standard-lab-slider-input::-moz-range-thumb { width: 16px; height: 16px; background: var(--stnd-lab-accent); border-radius: 50%; border: none; cursor: grab; transition: transform 0.1s ease; }
1440
- .standard-lab-slider-input::-moz-range-thumb:active { cursor: grabbing; transform: scale(1.2); }
1441
- .standard-lab-slider-value { width: 80px; flex-shrink: 0; text-align: center; padding: 6px 8px; font-size: var(--stnd-lab-text-size); }
1442
- .standard-lab-btn-reset { opacity: 0; transition: opacity 0.2s ease; }
1443
- .standard-lab-token-row:hover .standard-lab-btn-reset:not(:disabled) { opacity: 1; }
1444
- .standard-lab-btn-reset:disabled { opacity: 0; cursor: not-allowed; }
1445
- .standard-lab-seed-row { display: flex; align-items: center; gap: 6px; margin-bottom: 4px; }
1446
- .standard-lab-seed-row:last-child { margin-bottom: 0; }
1447
- .standard-lab-color-swatch { width: 28px; height: 28px; border: var(--stnd-lab-border); border-radius: 4px; cursor: pointer; padding: 2px; background: rgba(255,255,255,0.05); flex-shrink: 0; }
1448
- .standard-lab-color-swatch::-webkit-color-swatch-wrapper { padding: 0; }
1449
- .standard-lab-color-swatch::-webkit-color-swatch { border: none; border-radius: 2px; }
1450
- .standard-lab-seed-input { flex: 1; min-width: 0; padding: 2px 4px; background: transparent; border: 1px solid transparent; border-radius: 4px; font-family: var(--stnd-lab-font-family); font-size: var(--stnd-lab-text-size); color: var(--stnd-lab-text); transition: border-color 0.2s ease; }
1451
- .standard-lab-seed-input:focus { outline: none; background: rgba(255,255,255,0.05); border-color: var(--stnd-lab-accent); }
1452
- .standard-lab-seed-tag { font-size: calc(var(--stnd-lab-text-size) * 0.833); text-transform: uppercase; letter-spacing: 0.05em; opacity: 0.5; flex-shrink: 0; width: 20px; text-align: right; }
1453
- .standard-lab-font-row { display: flex; align-items: center; gap: 6px; margin-bottom: 4px; }
1454
- .standard-lab-font-row:last-child { margin-bottom: 0; }
1455
- .standard-lab-font-tag { font-size: calc(var(--stnd-lab-text-size) * 0.833); text-transform: uppercase; letter-spacing: 0.05em; opacity: 0.5; flex-shrink: 0; width: 32px; }
1456
- .standard-lab-quick-slider { display: flex; align-items: center; gap: 6px; margin-bottom: 4px; }
1457
- .standard-lab-quick-slider:last-child { margin-bottom: 0; }
1458
- .standard-lab-quick-slider .standard-lab-slider-input { flex: 1; }
1459
- .standard-lab-slider-readout { font-family: var(--stnd-lab-font-family); font-size: var(--stnd-lab-text-size); opacity: 0.8; flex-shrink: 0; width: 48px; text-align: right; }
1460
- .standard-lab-empty { padding: 6px; text-align: center; color: var(--stnd-lab-text); opacity: 0.6; font-size: calc(var(--stnd-lab-text-size) * 1.167); }
1461
- .standard-lab-notification { position: fixed; top: 6px; left: 50%; transform: translateX(-50%) translateY(-10px); background: var(--stnd-lab-bg); border: var(--stnd-lab-border); border-radius: 6px; padding: 8px 16px; box-shadow: var(--stnd-lab-shadow); z-index: calc(var(--z-toast) + 1); font-size: calc(var(--stnd-lab-text-size) * 1.167); opacity: 0; transition: opacity 0.2s ease, transform 0.2s ease; pointer-events: none; }
1462
- .standard-lab-notification-success { background: var(--stnd-lab-accent); color: var(--stnd-lab-bg); border-color: var(--stnd-lab-accent); }
1463
- .standard-lab-notification-info { background: var(--stnd-lab-accent); color: var(--stnd-lab-bg); border-color: var(--stnd-lab-accent); }
1464
- @media (prefers-color-scheme: dark) { .standard-lab-panel { box-shadow: var(--stnd-lab-shadow); } }
1465
- @media (max-width: 768px) { .standard-lab-panel-quick, .standard-lab-panel-full { width: calc(100vw - 32px); max-width: 360px; } }
1466
- @media print { .standard-lab-badge, .standard-lab-panel, .standard-lab-notification { display: none !important; } }
1467
- `;
1468
- document.head.appendChild(styles);
1469
- }
1470
- }
1471
-
1472
- // ================================================================
1473
- // AUTO-INITIALIZATION AND ASTRO CLIENTROUTER INTEGRATION
1474
- // ================================================================
1475
-
1476
- if (typeof window !== "undefined") {
1477
- const setupStandardLab = () => {
1478
- // Ensure body exists before initializing
1479
- if (!document.body) {
1480
- setTimeout(setupStandardLab, 10);
1481
- return;
1482
- }
1483
-
1484
- if (!window.StandardLab) {
1485
- window.StandardLab = new StandardLab();
1486
- console.log("[StandardLab] ✓ Initialized");
1487
- }
1488
-
1489
- // Handle HTMX swaps - Setup here to ensure document.body exists
1490
- if (document.body) {
1491
- document.body.addEventListener("htmx:afterSettle", () => {
1492
- console.log("[StandardLab] ⚡ HTMX swap detected");
1493
- if (window.StandardLab) {
1494
- window.StandardLab.refresh();
1495
- }
1496
- });
1497
- }
1498
- };
1499
-
1500
- if (document.readyState === "loading") {
1501
- document.addEventListener("DOMContentLoaded", setupStandardLab);
1502
- } else {
1503
- setupStandardLab();
1504
- }
1505
-
1506
- // Handle Astro ClientRouter page swaps - MUST check root container
1507
- document.addEventListener("astro:after-swap", () => {
1508
- console.log("[StandardLab] 🔄 Page swap detected");
1509
-
1510
- if (!window.StandardLab) {
1511
- console.log("[StandardLab] ⚠️ Instance not found, reinitializing");
1512
- window.StandardLab = new StandardLab();
1513
- return;
1514
- }
1515
-
1516
- // Check if badge exists in the new DOM
1517
- const badge = document.getElementById("standard-lab-badge-container");
1518
-
1519
- // If badge is gone, recreate it
1520
- if (!badge) {
1521
- console.log("[StandardLab] 🔨 Recreating badge");
1522
- window.StandardLab.badge = null;
1523
- window.StandardLab.createBadge();
1524
- }
1525
-
1526
- // If panel was open, we might want to close it or re-open it.
1527
- // For now, let's ensure state is consistent.
1528
- // If the panel element is missing from DOM but state says it's open, close it to reset.
1529
- const panel = document.getElementById("standard-lab-panel-container");
1530
- if (window.StandardLab.state !== "dormant" && !panel) {
1531
- window.StandardLab.state = "dormant";
1532
- window.StandardLab.panel = null;
1533
- }
1534
-
1535
- // Always restore state from storage
1536
- window.StandardLab.restoreTheme();
1537
- window.StandardLab.restoreDebugMode();
1538
-
1539
- // Re-inject debug overlays if needed
1540
- if (document.documentElement.classList.contains("stnd-debug")) {
1541
- window.StandardLab.injectGridDebugOverlays();
1542
- }
1543
- });
1544
- }
1545
- })();