@stnd/lab 0.2.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.
Files changed (3) hide show
  1. package/CHANGELOG.md +7 -0
  2. package/lab.js +1328 -0
  3. package/package.json +12 -0
package/CHANGELOG.md ADDED
@@ -0,0 +1,7 @@
1
+ # @stnd/lab
2
+
3
+ ## 0.2.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 6d8888a: First release
package/lab.js ADDED
@@ -0,0 +1,1328 @@
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
+ "--bold-weight": { 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": { min: 0, max: 3, step: 0.05, unit: "rem" },
45
+ "--grid-gap": { min: 0, max: 3, step: 0.05, unit: "rem" },
46
+ };
47
+
48
+ this.coreTokens = {
49
+ typography: [
50
+ "--font-text",
51
+ "--font-header",
52
+ "--font-monospace",
53
+ "--font-interface",
54
+ "--font-size",
55
+ "--optical-ratio",
56
+ "--line-height",
57
+ "--font-weight",
58
+ "--bold-weight",
59
+ "--font-letter-spacing",
60
+ "--font-header-letter-spacing",
61
+ "--font-header-weight",
62
+ ],
63
+ layout: [
64
+ "--line-width",
65
+ "--line-width-xs",
66
+ "--line-width-s",
67
+ "--line-width-m",
68
+ "--line-width-l",
69
+ "--line-width-xl",
70
+ "--base",
71
+ "--base-gap",
72
+ "--base-block-gap",
73
+ "--body-padding",
74
+ "--gap",
75
+ "--grid-gap",
76
+ ],
77
+ colors: this.getColorTokens(),
78
+ advanced: [
79
+ "--ratio-golden",
80
+ "--ratio-silver",
81
+ "--ratio-wholestep",
82
+ "--ratio-halfstep",
83
+ "--radius",
84
+ "--blur",
85
+ "--transition",
86
+ "--line-width",
87
+ "--shadow",
88
+ "--border",
89
+ ],
90
+ };
91
+
92
+ // NEW: CSS Variable configuration for StandardLab styling
93
+ this.cssVars = {
94
+ "--standard-lab-bg":
95
+ "color-mix( in srgb, var(--color-dark-background) 90%, var(--color-dark-foreground))",
96
+ "--standard-lab-text":
97
+ "color-mix( in srgb, var(--color-dark-background) 10%, var(--color-dark-foreground))",
98
+ "--standard-lab-text-size": "12px",
99
+ "--standard-lab-opacity": "1",
100
+ "--standard-lab-accent": "var(--color-accent)",
101
+ "--standard-lab-border": "var(--border)",
102
+ "--standard-lab-shadow": "var(--shadow)",
103
+ "--standard-lab-blur": "var(--blur)",
104
+ "--standard-lab-font-family": "var(--font-monospace)",
105
+ };
106
+
107
+ this.defaults = {};
108
+ this.log = (window.standard && window.standard.log) || console;
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., "--standard-lab-bg")
123
+ * @param {string} value - CSS value
124
+ *
125
+ * @example
126
+ * window.StandardLab.setCSSVar("--standard-lab-bg", "#1a1a1a");
127
+ * window.StandardLab.setCSSVar("--standard-lab-text", "#fff");
128
+ * window.StandardLab.setCSSVar("--standard-lab-text-size", "14px");
129
+ * window.StandardLab.setCSSVar("--standard-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("standard-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("standard-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("standard-debug-mode");
194
+
195
+ if (savedDebugMode === "true") {
196
+ document.documentElement.classList.add("standard-debug");
197
+ } else if (savedDebugMode === "false") {
198
+ document.documentElement.classList.remove("standard-debug");
199
+ }
200
+ }
201
+
202
+ // NEW: Save debug mode to sessionStorage
203
+ saveDebugMode(isEnabled) {
204
+ sessionStorage.setItem("standard-debug-mode", isEnabled.toString());
205
+ }
206
+
207
+ // NEW: Restore panel state from sessionStorage
208
+ restorePanelState() {
209
+ const savedPanelState = sessionStorage.getItem("standard-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("standard-panel-state");
222
+ } else {
223
+ sessionStorage.setItem("standard-panel-state", state);
224
+ }
225
+ }
226
+
227
+ /**
228
+ * Add a custom theme to the theme selector
229
+ * @param {string} value - Theme attribute value (e.g., "custom_dark")
230
+ * @param {string} label - Display label (e.g., "Custom Dark")
231
+ *
232
+ * @example
233
+ * window.StandardLab.addTheme("my-theme", "My Theme");
234
+ */
235
+ addTheme(value, label) {
236
+ // Check if theme already exists
237
+ const exists = this.customThemes.some((theme) => theme.value === value);
238
+ if (!exists) {
239
+ this.customThemes.push({ value, label });
240
+
241
+ // Refresh panel if it's open
242
+ if (this.state === "quick") {
243
+ this.createQuickPanel();
244
+ }
245
+ }
246
+ }
247
+
248
+ storeDefaults() {
249
+ const allTokens = [
250
+ ...this.coreTokens.typography,
251
+ ...this.coreTokens.layout,
252
+ ...this.coreTokens.colors,
253
+ ...this.coreTokens.advanced,
254
+ ];
255
+ allTokens.forEach((token) => {
256
+ const value = getComputedStyle(document.documentElement)
257
+ .getPropertyValue(token)
258
+ .trim();
259
+ if (value) {
260
+ this.defaults[token] = value;
261
+ }
262
+ });
263
+ }
264
+
265
+ getColorTokens() {
266
+ const scheme = this.colorScheme === "light" ? "light" : "dark";
267
+ return [
268
+ `--color-${scheme}-background`,
269
+ `--color-${scheme}-foreground`,
270
+ `--color-${scheme}-accent`,
271
+ `--color-${scheme}-red`,
272
+ `--color-${scheme}-orange`,
273
+ `--color-${scheme}-yellow`,
274
+ `--color-${scheme}-green`,
275
+ `--color-${scheme}-cyan`,
276
+ `--color-${scheme}-blue`,
277
+ `--color-${scheme}-purple`,
278
+ `--color-${scheme}-pink`,
279
+ ];
280
+ }
281
+
282
+ detectColorScheme() {
283
+ if (
284
+ window.matchMedia &&
285
+ window.matchMedia("(prefers-color-scheme: dark)").matches
286
+ ) {
287
+ return "dark";
288
+ }
289
+ return "light";
290
+ }
291
+
292
+ loadPosition() {
293
+ const saved = localStorage.getItem("standard-lab-position");
294
+ if (saved) {
295
+ return JSON.parse(saved);
296
+ }
297
+ return { bottom: 16, right: 16 };
298
+ }
299
+
300
+ savePosition() {
301
+ localStorage.setItem(
302
+ "standard-lab-position",
303
+ JSON.stringify(this.position),
304
+ );
305
+ }
306
+
307
+ createBadge() {
308
+ if (this.badge) return;
309
+ this.badge = document.createElement("button");
310
+ this.badge.id = "standard-lab-badge-container";
311
+ this.badge.className = "standard-lab-badge";
312
+ this.badge.setAttribute("aria-label", "Open Standard Inspector");
313
+ this.badge.setAttribute("title", "Standard Inspector (Shift+Cmd+S)");
314
+ this.badge.setAttribute("style", "color:var(--color-subtle);");
315
+ this.badge.style.bottom = `${this.position.bottom}px`;
316
+ this.badge.style.right = `${this.position.right}px`;
317
+ this.badge.innerHTML = ``;
318
+ this.badge.addEventListener("click", () => this.openQuick());
319
+ this.makeDraggable(this.badge);
320
+
321
+ // Append to body
322
+ document.body.appendChild(this.badge);
323
+
324
+ this.updateBadge();
325
+ this.injectStyles();
326
+ }
327
+
328
+ updateBadge() {
329
+ if (!this.badge) return;
330
+ const count = this.modifiedTokens.size;
331
+ if (count > 0) {
332
+ this.badge.setAttribute("data-modified", count);
333
+ } else {
334
+ this.badge.removeAttribute("data-modified");
335
+ }
336
+ }
337
+
338
+ makeDraggable(element) {
339
+ let isDragging = false,
340
+ startX,
341
+ startY,
342
+ initialBottom,
343
+ initialRight;
344
+ const onMouseDown = (e) => {
345
+ if (
346
+ e.target !== element &&
347
+ !element.classList.contains("standard-lab-panel-header")
348
+ )
349
+ return;
350
+ isDragging = true;
351
+ startX = e.clientX;
352
+ startY = e.clientY;
353
+ const rect = element.getBoundingClientRect();
354
+ initialBottom = window.innerHeight - rect.bottom;
355
+ initialRight = window.innerWidth - rect.right;
356
+ document.addEventListener("mousemove", onMouseMove);
357
+ document.addEventListener("mouseup", onMouseUp);
358
+ element.style.cursor = "grabbing";
359
+ e.preventDefault();
360
+ };
361
+ const onMouseMove = (e) => {
362
+ if (!isDragging) return;
363
+ const deltaX = startX - e.clientX;
364
+ const deltaY = e.clientY - startY;
365
+ this.position.right = Math.max(0, initialRight + deltaX);
366
+ this.position.bottom = Math.max(0, initialBottom - deltaY);
367
+ element.style.right = `${this.position.right}px`;
368
+ element.style.bottom = `${this.position.bottom}px`;
369
+ };
370
+ const onMouseUp = () => {
371
+ isDragging = false;
372
+ element.style.cursor = "";
373
+ document.removeEventListener("mousemove", onMouseMove);
374
+ document.removeEventListener("mouseup", onMouseUp);
375
+ this.savePosition();
376
+ };
377
+ element.addEventListener("mousedown", onMouseDown);
378
+ }
379
+
380
+ openQuick() {
381
+ if (this.state === "quick") {
382
+ this.close();
383
+ return;
384
+ }
385
+ this.state = "quick";
386
+ this.savePanelState("quick");
387
+ this.createQuickPanel();
388
+ }
389
+
390
+ openFull() {
391
+ this.state = "full";
392
+ this.savePanelState("full");
393
+ this.createFullPanel();
394
+ }
395
+
396
+ close() {
397
+ if (this.panel) {
398
+ this.panel.style.opacity = "0";
399
+ this.panel.style.transform = "translateY(10px)";
400
+ setTimeout(() => {
401
+ if (this.panel) {
402
+ this.panel.remove();
403
+ this.panel = null;
404
+ }
405
+ }, 200);
406
+ }
407
+ this.state = "dormant";
408
+ this.savePanelState("dormant");
409
+ }
410
+
411
+ /**
412
+ * Public method to refresh the panel's UI.
413
+ * Ideal for calling after HTMX swaps change the page state (e.g., theme).
414
+ */
415
+ refresh() {
416
+ if (this.log && this.log.debug) {
417
+ this.log.debug("Refreshing StandardLab");
418
+ }
419
+ this.storeDefaults();
420
+
421
+ this.badge = null;
422
+ this.init();
423
+
424
+ // Re-inject overlays if debug is active
425
+ if (document.documentElement.classList.contains("standard-debug")) {
426
+ this.removeGridDebugOverlays();
427
+ this.injectGridDebugOverlays();
428
+ }
429
+
430
+ // Do nothing if the panel is closed
431
+ if (this.state === "dormant") {
432
+ return;
433
+ }
434
+
435
+ // Re-run the appropriate panel creation method to rebuild the UI
436
+ if (this.state === "quick") {
437
+ this.createQuickPanel();
438
+ } else if (this.state === "full") {
439
+ const showModifiedOnly =
440
+ this.panel?.querySelector('[data-filter="modified"]')?.checked ||
441
+ false;
442
+ this.createFullPanel();
443
+ this.switchTab(this.currentTab);
444
+ const modifiedFilter = this.panel?.querySelector(
445
+ '[data-filter="modified"]',
446
+ );
447
+ if (modifiedFilter) {
448
+ modifiedFilter.checked = showModifiedOnly;
449
+ this.refreshTokenList(showModifiedOnly);
450
+ }
451
+ }
452
+ }
453
+
454
+ createQuickPanel() {
455
+ if (this.panel) this.panel.remove();
456
+ this.panel = document.createElement("div");
457
+ this.panel.id = "standard-lab-panel-container";
458
+ this.panel.className = "standard-lab-panel standard-lab-panel-quick";
459
+ this.panel.style.bottom = `calc(${this.position.bottom}px + var(--space-2))`;
460
+ this.panel.style.right = `${this.position.right}px`;
461
+ const debugActive =
462
+ document.documentElement.classList.contains("standard-debug");
463
+ const modifiedCount = this.modifiedTokens.size;
464
+ this.panel.innerHTML = `
465
+ <header class="standard-lab-panel-header">
466
+ <div style="display:inline-block;">
467
+ <h2 style="color: var(--color-dark-foreground);display:inline-block;">Inspector v${this.version}</h2>
468
+ <div style="display:inline-block;" class="standard-lab-viewport-info">${window.innerWidth}px</div>
469
+ </div>
470
+ <button class="standard-lab-btn-icon" data-action="close" title="Close (Esc)">×</button>
471
+ </header>
472
+ <section class="standard-lab-section">
473
+ <label class="standard-lab-toggle">
474
+ <input type="checkbox" ${debugActive ? "checked" : ""} data-toggle="debug">
475
+ <span>Debug Mode</span>
476
+ <kbd>⇧⌘D</kbd>
477
+ </label>
478
+ </section>
479
+ <section class="standard-lab-section">
480
+ <label class="standard-lab-label">Theme</label>
481
+ <select class="standard-lab-select" data-action="theme">
482
+ ${this.getThemeOptions()}
483
+ </select>
484
+ </section>
485
+ <div class="standard-lab-actions">
486
+ <button class="standard-lab-btn" data-action="edit-tokens">
487
+ ${modifiedCount > 0 ? `Edit Tokens (${modifiedCount})` : "Edit Tokens"}
488
+ </button>
489
+ ${modifiedCount > 0 ? `<button class="standard-lab-btn standard-lab-btn-secondary" data-action="reset">Reset</button>` : ""}
490
+ <button class="standard-lab-btn standard-lab-btn-secondary" data-action="copy">Copy CSS</button>
491
+ </div>
492
+ `;
493
+ this.attachQuickPanelListeners();
494
+ document.body.appendChild(this.panel);
495
+ requestAnimationFrame(() => {
496
+ this.panel.style.opacity = "1";
497
+ this.panel.style.transform = "translateY(0)";
498
+ });
499
+ }
500
+
501
+ getThemeOptions() {
502
+ const defaultThemes = [
503
+ { value: "", label: "Empty" },
504
+ { value: "default", label: "Default" },
505
+
506
+ { value: "system", label: "System" },
507
+ { value: "claude", label: "Claude" },
508
+ { value: "bauhaus", label: "Bauhaus" },
509
+ { value: "venetian", label: "Venetian" },
510
+ { value: "newspaper", label: "Newspaper" },
511
+ { value: "didone", label: "Didone" },
512
+ { value: "transitional", label: "Transitional" },
513
+
514
+ { value: "english-oldstyle", label: "English Oldstyle" },
515
+
516
+ { value: "paper", label: "Paper" },
517
+ { value: "swiss", label: "Swiss" },
518
+ { value: "kernel", label: "Kernel" },
519
+ { value: "forest", label: "Forest" },
520
+ { value: "book", label: "Book" },
521
+ { value: "occult", label: "Occult" },
522
+ { value: "baroque", label: "Baroque" },
523
+ { value: "botanist", label: "Botanist" },
524
+ { value: "chalky", label: "Chalky" },
525
+ { value: "minimal", label: "Minimal" },
526
+ { value: "marauder", label: "Marauder" },
527
+ { value: "blueprint", label: "Blueprint" },
528
+ { value: "custom_1", label: "Custom 1" },
529
+ { value: "custom_2", label: "Custom 2" },
530
+ { value: "custom_3", label: "Custom 3" },
531
+ ];
532
+
533
+ // Merge default themes with custom themes
534
+ const allThemes = [...defaultThemes, ...this.customThemes];
535
+
536
+ const current = document.documentElement.getAttribute("data-theme") || "";
537
+ return allThemes
538
+ .map(
539
+ (theme) =>
540
+ `<option value="${theme.value}" ${current === theme.value ? "selected" : ""}>${theme.label}</option>`,
541
+ )
542
+ .join("");
543
+ }
544
+
545
+ attachQuickPanelListeners() {
546
+ this.panel
547
+ .querySelector('[data-action="close"]')
548
+ ?.addEventListener("click", () => this.close());
549
+ this.panel
550
+ .querySelector('[data-toggle="debug"]')
551
+ ?.addEventListener("change", (e) => {
552
+ const isEnabled = e.target.checked;
553
+ document.documentElement.classList.toggle(
554
+ "standard-debug",
555
+ isEnabled,
556
+ );
557
+
558
+ // UPDATED: Save debug mode to sessionStorage
559
+ this.saveDebugMode(isEnabled);
560
+
561
+ const currentTheme =
562
+ document.documentElement.getAttribute("data-theme");
563
+ const isBlueprintActive = currentTheme === "blueprint";
564
+
565
+ if (isEnabled || isBlueprintActive) {
566
+ this.injectGridDebugOverlays();
567
+ } else {
568
+ this.removeGridDebugOverlays();
569
+ }
570
+ window.toast?.info(
571
+ `Debug mode ${e.target.checked ? "enabled" : "disabled"}`,
572
+ );
573
+ });
574
+ this.panel
575
+ .querySelector('[data-action="theme"]')
576
+ ?.addEventListener("change", (e) => {
577
+ const theme = e.target.value;
578
+ if (theme) {
579
+ document.documentElement.setAttribute("data-theme", theme);
580
+ } else {
581
+ document.documentElement.removeAttribute("data-theme");
582
+ }
583
+ sessionStorage.setItem("standard-theme", theme);
584
+
585
+ // Handle blueprint exception for debug mode
586
+ this.handleBlueprintDebugException(theme);
587
+
588
+ window.toast?.info(`Theme: ${theme || "Default"}`);
589
+ });
590
+ this.panel
591
+ .querySelector('[data-action="edit-tokens"]')
592
+ ?.addEventListener("click", () => this.openFull());
593
+ this.panel
594
+ .querySelector('[data-action="reset"]')
595
+ ?.addEventListener("click", () => this.resetAll());
596
+ this.panel
597
+ .querySelector('[data-action="copy"]')
598
+ ?.addEventListener("click", () => this.copyCSS());
599
+ }
600
+ /**
601
+ * Handle the blueprint theme exception for debug grid overlay
602
+ * @param {string} theme - Current theme name
603
+ */
604
+ handleBlueprintDebugException(theme) {
605
+ const isBlueprint = theme === "blueprint";
606
+ const hasDebugClass =
607
+ document.documentElement.classList.contains("standard-debug");
608
+
609
+ if (isBlueprint && !hasDebugClass) {
610
+ // Blueprint theme activated - show grid overlays
611
+ this.injectGridDebugOverlays();
612
+ } else if (!isBlueprint && !hasDebugClass) {
613
+ // Non-blueprint theme and debug not active - remove overlays
614
+ this.removeGridDebugOverlays();
615
+ }
616
+ // If debug is explicitly active, let it control the overlays
617
+ }
618
+ createFullPanel() {
619
+ if (this.panel) this.panel.remove();
620
+ this.panel = document.createElement("div");
621
+ this.panel.id = "standard-lab-panel-container";
622
+ this.panel.className = "standard-lab-panel standard-lab-panel-full";
623
+ this.panel.style.bottom = `calc(${this.position.bottom}px + var(--space-2))`;
624
+ this.panel.style.right = `${this.position.right}px`;
625
+ const modifiedCount = this.modifiedTokens.size;
626
+ const showModifiedOnly = modifiedCount > 0;
627
+ this.panel.innerHTML = `
628
+ <header class="standard-lab-panel-header" style="cursor: grab;">
629
+ <h2 style="display:inline-block;">Token Editor</h2>
630
+ <div class="standard-lab-header-actions">
631
+ <button class="standard-lab-btn-icon" data-action="minimize" title="Minimize">−</button>
632
+ <button class="standard-lab-btn-icon" data-action="close" title="Close (Esc)">×</button>
633
+ </div>
634
+ </header>
635
+ <nav class="standard-lab-tabs">
636
+ <button class="standard-lab-tab" data-tab="typography" aria-selected="true">Typography</button>
637
+ <button class="standard-lab-tab" data-tab="layout">Layout</button>
638
+ <button class="standard-lab-tab" data-tab="colors">Colors</button>
639
+ <button class="standard-lab-tab" data-tab="advanced">Advanced</button>
640
+ </nav>
641
+ ${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>` : ""}
642
+ <div class="standard-lab-content">${this.renderTokenList("typography", showModifiedOnly)}</div>
643
+ <div class="standard-lab-actions">
644
+ <button class="standard-lab-btn standard-lab-btn-secondary" data-action="reset">Reset All</button>
645
+ <button class="standard-lab-btn" data-action="copy">Copy CSS</button>
646
+ </div>
647
+ `;
648
+ this.attachFullPanelListeners();
649
+ this.makeDraggable(
650
+ this.panel.querySelector(".standard-lab-panel-header"),
651
+ );
652
+ document.body.appendChild(this.panel);
653
+ requestAnimationFrame(() => {
654
+ this.panel.style.opacity = "1";
655
+ this.panel.style.transform = "translateY(0)";
656
+ });
657
+ }
658
+
659
+ renderTokenList(category, modifiedOnly = false) {
660
+ const tokens = this.coreTokens[category] || [];
661
+ let filteredTokens = modifiedOnly
662
+ ? tokens.filter((token) => this.modifiedTokens.has(token))
663
+ : tokens;
664
+ if (filteredTokens.length === 0) {
665
+ return `<div class="standard-lab-empty">No tokens to display</div>`;
666
+ }
667
+ return `<div class="standard-lab-token-list">${filteredTokens.map((token) => this.renderTokenRow(token)).join("")}</div>`;
668
+ }
669
+
670
+ /**
671
+ * Check if a value is numeric (with optional unit)
672
+ */
673
+ isNumericValue(value) {
674
+ if (!value) return false;
675
+ // Match patterns like: 1.5, 1.5rem, 16px, 1.618, 500
676
+ return /^-?\d+(\.\d+)?(rem|em|px|%|ch|vh|vw)?$/.test(value.trim());
677
+ }
678
+
679
+ /**
680
+ * Extract numeric portion from a CSS value
681
+ */
682
+ extractNumericValue(value) {
683
+ if (!value) return 0;
684
+ const match = value.match(/^(-?\d+(\.\d+)?)/);
685
+ return match ? parseFloat(match[1]) : 0;
686
+ }
687
+
688
+ /**
689
+ * Extract unit from a CSS value (rem, px, em, etc.)
690
+ */
691
+ extractUnit(value) {
692
+ if (!value) return "";
693
+ const match = value.match(/[a-z%]+$/i);
694
+ return match ? match[0] : "";
695
+ }
696
+
697
+ renderTokenRow(token) {
698
+ const value = this.getPropertyValue(token);
699
+ const isModified = this.modifiedTokens.has(token);
700
+ const isColor = token.includes("color") && this.isColorValue(value);
701
+ const sliderConfig = this.sliderConfig[token];
702
+
703
+ // Detect if this is a slider-enabled numeric token
704
+ const isSlider = sliderConfig && this.isNumericValue(value);
705
+
706
+ if (isSlider) {
707
+ const numericValue = this.extractNumericValue(value);
708
+ return `
709
+ <div class="standard-lab-token-row ${isModified ? "is-modified" : ""}">
710
+ <div class="standard-lab-slider-container">
711
+ <label class="standard-lab-slider-label">${token}</label>
712
+ <div class="standard-lab-slider-controls">
713
+ <input
714
+ type="range"
715
+ class="standard-lab-slider-input"
716
+ data-token="${token}"
717
+ value="${numericValue}"
718
+ min="${sliderConfig.min}"
719
+ max="${sliderConfig.max}"
720
+ step="${sliderConfig.step}"
721
+ title="${token}">
722
+ <input
723
+ type="text"
724
+ class="standard-lab-text-input standard-lab-slider-value"
725
+ data-token="${token}"
726
+ value="${value}"
727
+ spellcheck="false">
728
+ </div>
729
+ </div>
730
+ <button class="standard-lab-btn-icon standard-lab-btn-reset" data-action="reset-token" data-token="${token}" title="Reset" ${!isModified ? "disabled" : ""}>↺</button>
731
+ </div>
732
+ `;
733
+ }
734
+
735
+ if (isColor) {
736
+ return `
737
+ <div class="standard-lab-token-row ${isModified ? "is-modified" : ""}">
738
+ <div class="standard-lab-color-container">
739
+ <label class="standard-lab-color-label">${token}</label>
740
+ <div class="standard-lab-color-controls">
741
+ <input
742
+ type="color"
743
+ class="standard-lab-color-input"
744
+ data-token="${token}"
745
+ value="${this.extractHexColor(value)}"
746
+ title="Pick color">
747
+ <input
748
+ type="text"
749
+ class="standard-lab-text-input standard-lab-color-value"
750
+ data-token="${token}"
751
+ value="${value}"
752
+ placeholder="${token}"
753
+ spellcheck="false">
754
+ </div>
755
+ </div>
756
+ <button class="standard-lab-btn-icon standard-lab-btn-reset" data-action="reset-token" data-token="${token}" title="Reset" ${!isModified ? "disabled" : ""}>↺</button>
757
+ </div>
758
+ `;
759
+ }
760
+
761
+ // Default text input (for font names, etc.)
762
+ return `
763
+ <div class="standard-lab-token-row ${isModified ? "is-modified" : ""}">
764
+ <div class="standard-lab-text-container">
765
+ <label class="standard-lab-text-label">${token}</label>
766
+ <input type="text" class="standard-lab-text-input" data-token="${token}" value="${value}" placeholder="${token}" spellcheck="false">
767
+ </div>
768
+ <button class="standard-lab-btn-icon standard-lab-btn-reset" data-action="reset-token" data-token="${token}" title="Reset" ${!isModified ? "disabled" : ""}>↺</button>
769
+ </div>
770
+ `;
771
+ }
772
+
773
+ attachFullPanelListeners() {
774
+ this.panel
775
+ .querySelector('[data-action="close"]')
776
+ ?.addEventListener("click", () => this.close());
777
+ this.panel
778
+ .querySelector('[data-action="minimize"]')
779
+ ?.addEventListener("click", () => this.openQuick());
780
+ this.panel.querySelectorAll(".standard-lab-tab").forEach((tab) => {
781
+ tab.addEventListener("click", (e) =>
782
+ this.switchTab(e.target.dataset.tab),
783
+ );
784
+ });
785
+ this.panel
786
+ .querySelector('[data-filter="modified"]')
787
+ ?.addEventListener("change", (e) =>
788
+ this.refreshTokenList(e.target.checked),
789
+ );
790
+
791
+ // Slider input listeners
792
+ this.panel
793
+ .querySelectorAll(".standard-lab-slider-input")
794
+ .forEach((slider) => {
795
+ slider.addEventListener("input", (e) => {
796
+ const token = e.target.dataset.token;
797
+ const numericValue = e.target.value;
798
+ const config = this.sliderConfig[token];
799
+ const unit =
800
+ config.unit || this.extractUnit(this.getPropertyValue(token));
801
+ const fullValue = `${numericValue}${unit}`;
802
+
803
+ this.updateToken(token, fullValue);
804
+
805
+ // Update the text input to reflect slider value
806
+ const textInput = this.panel.querySelector(
807
+ `.standard-lab-slider-value[data-token="${token}"]`,
808
+ );
809
+ if (textInput) textInput.value = fullValue;
810
+ });
811
+ });
812
+
813
+ // Text input for sliders (allows manual override)
814
+ this.panel
815
+ .querySelectorAll(".standard-lab-slider-value")
816
+ .forEach((input) => {
817
+ input.addEventListener("input", (e) => {
818
+ const token = e.target.dataset.token;
819
+ const value = e.target.value;
820
+ const numericValue = this.extractNumericValue(value);
821
+
822
+ this.updateToken(token, value);
823
+
824
+ // Sync slider position
825
+ const slider = this.panel.querySelector(
826
+ `.standard-lab-slider-input[data-token="${token}"]`,
827
+ );
828
+ if (slider) slider.value = numericValue;
829
+ });
830
+ });
831
+
832
+ // Regular text inputs
833
+ this.panel
834
+ .querySelectorAll(
835
+ ".standard-lab-text-input:not(.standard-lab-slider-value):not(.standard-lab-color-value)",
836
+ )
837
+ .forEach((input) => {
838
+ input.addEventListener("input", (e) =>
839
+ this.updateToken(e.target.dataset.token, e.target.value),
840
+ );
841
+ });
842
+
843
+ // Color pickers
844
+ this.panel
845
+ .querySelectorAll(".standard-lab-color-input")
846
+ .forEach((input) => {
847
+ input.addEventListener("input", (e) => {
848
+ const token = e.target.dataset.token;
849
+ const color = e.target.value;
850
+ this.updateToken(token, color);
851
+ const textInput = this.panel.querySelector(
852
+ `.standard-lab-color-value[data-token="${token}"]`,
853
+ );
854
+ if (textInput) textInput.value = color;
855
+ });
856
+ });
857
+
858
+ // Color text inputs
859
+ this.panel
860
+ .querySelectorAll(".standard-lab-color-value")
861
+ .forEach((input) => {
862
+ input.addEventListener("input", (e) => {
863
+ const token = e.target.dataset.token;
864
+ const value = e.target.value;
865
+ this.updateToken(token, value);
866
+ const colorInput = this.panel.querySelector(
867
+ `.standard-lab-color-input[data-token="${token}"]`,
868
+ );
869
+ if (colorInput && this.isColorValue(value)) {
870
+ colorInput.value = this.extractHexColor(value);
871
+ }
872
+ });
873
+ });
874
+
875
+ // Reset buttons
876
+ this.panel
877
+ .querySelectorAll('[data-action="reset-token"]')
878
+ .forEach((btn) => {
879
+ btn.addEventListener("click", (e) =>
880
+ this.resetToken(e.target.dataset.token),
881
+ );
882
+ });
883
+ this.panel
884
+ .querySelector('[data-action="reset"]')
885
+ ?.addEventListener("click", () => this.resetAll());
886
+ this.panel
887
+ .querySelector('[data-action="copy"]')
888
+ ?.addEventListener("click", () => this.copyCSS());
889
+ }
890
+
891
+ switchTab(category) {
892
+ this.currentTab = category;
893
+ this.panel.querySelectorAll(".standard-lab-tab").forEach((tab) => {
894
+ tab.setAttribute("aria-selected", tab.dataset.tab === category);
895
+ });
896
+ const showModifiedOnly =
897
+ this.panel.querySelector('[data-filter="modified"]')?.checked || false;
898
+ this.refreshTokenList(showModifiedOnly);
899
+ }
900
+
901
+ refreshTokenList(modifiedOnly = false) {
902
+ const content = this.panel?.querySelector(".standard-lab-content");
903
+ if (!content) return;
904
+ content.innerHTML = this.renderTokenList(this.currentTab, modifiedOnly);
905
+ this.attachFullPanelListeners();
906
+ }
907
+
908
+ updateToken(token, value) {
909
+ document.documentElement.style.setProperty(token, value);
910
+ if (value !== this.defaults[token]) {
911
+ this.modifiedTokens.add(token);
912
+ } else {
913
+ this.modifiedTokens.delete(token);
914
+ }
915
+ this.updateBadge();
916
+ const resetBtn = this.panel?.querySelector(
917
+ `[data-action="reset-token"][data-token="${token}"]`,
918
+ );
919
+ if (resetBtn) resetBtn.disabled = !this.modifiedTokens.has(token);
920
+ const row = this.panel?.querySelector(
921
+ `.standard-lab-token-row:has([data-token="${token}"])`,
922
+ );
923
+ if (row)
924
+ row.classList.toggle("is-modified", this.modifiedTokens.has(token));
925
+ if (
926
+ token.includes("font-") &&
927
+ !token.includes("font-size") &&
928
+ !token.includes("font-ratio")
929
+ ) {
930
+ this.loadGoogleFont(value);
931
+ }
932
+ }
933
+
934
+ resetToken(token) {
935
+ const defaultValue = this.defaults[token];
936
+ if (defaultValue) {
937
+ document.documentElement.style.setProperty(token, defaultValue);
938
+ this.modifiedTokens.delete(token);
939
+ const input = this.panel?.querySelector(`[data-token="${token}"]`);
940
+ if (input) input.value = defaultValue;
941
+ this.updateBadge();
942
+ this.refreshTokenList();
943
+ window.toast?.info(`Reset ${token}`);
944
+ }
945
+ }
946
+
947
+ resetAll() {
948
+ if (!confirm("Reset all tokens to defaults?")) return;
949
+ document.documentElement.removeAttribute("style");
950
+ this.modifiedTokens.clear();
951
+ this.googleFontsLoaded.clear();
952
+ this.updateBadge();
953
+ if (this.state === "full") {
954
+ this.refreshTokenList();
955
+ }
956
+ window.toast?.info("All tokens reset");
957
+ }
958
+
959
+ copyCSS() {
960
+ const allTokens = [
961
+ ...this.coreTokens.typography,
962
+ ...this.coreTokens.layout,
963
+ ...this.coreTokens.colors,
964
+ ...this.coreTokens.advanced,
965
+ ];
966
+ const css = `:root {\n${allTokens.map((token) => ` ${token}: ${this.getPropertyValue(token)};`).join("\n")}\n}`;
967
+ navigator.clipboard
968
+ .writeText(css)
969
+ .then(() => window.toast?.success("CSS copied to clipboard!"))
970
+ .catch(() => {
971
+ const textarea = document.createElement("textarea");
972
+ textarea.value = css;
973
+ textarea.style.position = "fixed";
974
+ textarea.style.opacity = "0";
975
+ document.body.appendChild(textarea);
976
+ textarea.select();
977
+ document.execCommand("copy");
978
+ document.body.removeChild(textarea);
979
+ window.toast?.success("CSS copied to clipboard!");
980
+ });
981
+ }
982
+
983
+ getPropertyValue(token) {
984
+ const value = getComputedStyle(document.documentElement)
985
+ .getPropertyValue(token)
986
+ .trim();
987
+ return (
988
+ value || document.documentElement.style.getPropertyValue(token).trim()
989
+ );
990
+ }
991
+
992
+ isColorValue(value) {
993
+ return value
994
+ ? value.startsWith("#") ||
995
+ value.startsWith("rgb") ||
996
+ value.startsWith("hsl") ||
997
+ value.startsWith("oklch") ||
998
+ value.includes("color-mix")
999
+ : false;
1000
+ }
1001
+
1002
+ extractHexColor(value) {
1003
+ if (!value) return "#000000";
1004
+ const hexMatch = value.match(/#[0-9a-f]{6}/i);
1005
+ if (hexMatch) return hexMatch[0];
1006
+ const rgbMatch = value.match(/rgb\((\d+),\s*(\d+),\s*(\d+)\)/);
1007
+ if (rgbMatch) {
1008
+ const [, r, g, b] = rgbMatch;
1009
+ return (
1010
+ "#" +
1011
+ [r, g, b]
1012
+ .map((x) => parseInt(x).toString(16).padStart(2, "0"))
1013
+ .join("")
1014
+ );
1015
+ }
1016
+ const colorMap = {
1017
+ white: "#ffffff",
1018
+ black: "#000000",
1019
+ red: "#ff0000",
1020
+ green: "#00ff00",
1021
+ blue: "#0000ff",
1022
+ yellow: "#ffff00",
1023
+ orange: "#ffa500",
1024
+ purple: "#800080",
1025
+ pink: "#ffc0cb",
1026
+ cyan: "#00ffff",
1027
+ };
1028
+ return colorMap[value.toLowerCase()] || "#000000";
1029
+ }
1030
+
1031
+ loadGoogleFont(fontName) {
1032
+ const cleanName = fontName.split(",")[0].trim().replace(/['"]/g, "");
1033
+ const systemFonts = [
1034
+ "system-ui",
1035
+ "sans-serif",
1036
+ "serif",
1037
+ "monospace",
1038
+ "-apple-system",
1039
+ "BlinkMacSystemFont",
1040
+ ];
1041
+ if (
1042
+ systemFonts.includes(cleanName) ||
1043
+ this.googleFontsLoaded.has(cleanName)
1044
+ )
1045
+ return;
1046
+ const fontUrl = `https://fonts.googleapis.com/css2?family=${cleanName.replace(/ /g, "+")}:wght@300;400;500;600;700;900&display=swap`;
1047
+ if (
1048
+ !document.querySelector(`link[href*="${cleanName.replace(/ /g, "+")}"]`)
1049
+ ) {
1050
+ const link = document.createElement("link");
1051
+ link.rel = "stylesheet";
1052
+ link.href = fontUrl;
1053
+ document.head.appendChild(link);
1054
+ this.googleFontsLoaded.add(cleanName);
1055
+ }
1056
+ }
1057
+
1058
+ showNotification(message, type = "info") {
1059
+ const notification = document.createElement("div");
1060
+ notification.className = `standard-lab-notification standard-lab-notification-${type}`;
1061
+ notification.textContent = message;
1062
+ document.body.appendChild(notification);
1063
+ requestAnimationFrame(() => {
1064
+ notification.style.opacity = "1";
1065
+ notification.style.transform = "translateY(0)";
1066
+ });
1067
+ setTimeout(() => {
1068
+ notification.style.opacity = "0";
1069
+ notification.style.transform = "translateY(-10px)";
1070
+ setTimeout(() => notification.remove(), 200);
1071
+ }, 2500);
1072
+ }
1073
+
1074
+ initKeyboardShortcuts() {
1075
+ document.addEventListener("keydown", (e) => {
1076
+ const isMac = navigator.platform.indexOf("Mac") > -1;
1077
+ if (
1078
+ e.shiftKey &&
1079
+ (isMac ? e.metaKey : e.ctrlKey) &&
1080
+ e.key.toLowerCase() === "s"
1081
+ ) {
1082
+ e.preventDefault();
1083
+ this.state === "dormant" ? this.openQuick() : this.close();
1084
+ }
1085
+ if (
1086
+ e.shiftKey &&
1087
+ (isMac ? e.metaKey : e.ctrlKey) &&
1088
+ e.key.toLowerCase() === "d"
1089
+ ) {
1090
+ e.preventDefault();
1091
+
1092
+ document.documentElement.classList.toggle("standard-debug");
1093
+ const isActive =
1094
+ document.documentElement.classList.contains("standard-debug");
1095
+
1096
+ // UPDATED: Save debug mode to sessionStorage
1097
+ this.saveDebugMode(isActive);
1098
+
1099
+ if (isActive) {
1100
+ this.injectGridDebugOverlays();
1101
+ } else {
1102
+ this.removeGridDebugOverlays();
1103
+ }
1104
+
1105
+ window.toast?.info(`Debug mode ${isActive ? "enabled" : "disabled"}`);
1106
+ }
1107
+ if (e.key === "Escape" && this.state !== "dormant") {
1108
+ this.close();
1109
+ }
1110
+ });
1111
+ }
1112
+
1113
+ initModificationTracking() {
1114
+ const allTokens = [
1115
+ ...this.coreTokens.typography,
1116
+ ...this.coreTokens.layout,
1117
+ ...this.coreTokens.colors,
1118
+ ...this.coreTokens.advanced,
1119
+ ];
1120
+ allTokens.forEach((token) => {
1121
+ const currentValue = this.getPropertyValue(token);
1122
+ const defaultValue = this.defaults[token];
1123
+ if (currentValue && defaultValue && currentValue !== defaultValue) {
1124
+ this.modifiedTokens.add(token);
1125
+ }
1126
+ });
1127
+ this.updateBadge();
1128
+ }
1129
+
1130
+ /**
1131
+ * Inject grid debug overlays into all .prose containers
1132
+ */
1133
+ injectGridDebugOverlays() {
1134
+ const proseContainers = document.querySelectorAll(".prose");
1135
+
1136
+ proseContainers.forEach((prose) => {
1137
+ // Check if overlay already exists
1138
+ if (prose.querySelector(".grid-debug-overlay")) {
1139
+ return;
1140
+ }
1141
+
1142
+ const overlay = document.createElement("div");
1143
+ overlay.className = "grid-debug-overlay";
1144
+ overlay.setAttribute("aria-hidden", "true");
1145
+
1146
+ // Add 7 spans for grid columns
1147
+ for (let i = 0; i < 7; i++) {
1148
+ overlay.appendChild(document.createElement("span"));
1149
+ }
1150
+
1151
+ // Insert as first child
1152
+ prose.insertBefore(overlay, prose.firstChild);
1153
+ });
1154
+ }
1155
+
1156
+ /**
1157
+ * Remove grid debug overlays from all .prose containers
1158
+ */
1159
+ removeGridDebugOverlays() {
1160
+ document
1161
+ .querySelectorAll(".grid-debug-overlay")
1162
+ .forEach((overlay) => overlay.remove());
1163
+ }
1164
+
1165
+ injectStyles() {
1166
+ if (document.getElementById("standard-lab-styles")) return;
1167
+ const styles = document.createElement("style");
1168
+ styles.id = "standard-lab-styles";
1169
+ styles.textContent = `
1170
+ .standard-lab-badge { font-family: var(--standard-lab-font-family); position: fixed; width: var(--space-half); height: var(--space-half); border-radius: 50%; background: var(--color-surface); border: var(--border); box-shadow: var(--shadow); backdrop-filter: var(--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; }
1171
+ .standard-lab-badge:hover { box-shadow: var(--shadow); transform: scale(1.05); color: var(--color-dark-accent); }
1172
+ .standard-lab-badge:active { transform: scale(0.95); }
1173
+ .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); }
1174
+ .standard-lab-panel { position: fixed; background: var(--standard-lab-bg); color: var(--standard-lab-text); font-size: var(--standard-lab-text-size); border: var(--standard-lab-border); border-radius: 6px; box-shadow: var(--standard-lab-shadow); backdrop-filter: var(--standard-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; }
1175
+ .standard-lab-panel * { font-family: var(--standard-lab-font-family) !important; font-size: inherit; line-height:1.5; margin:0; }
1176
+ .standard-lab-panel :is(label, input) { margin:0; }
1177
+ .standard-lab-panel-quick { max-width: 280px; max-height: 400px; }
1178
+ .standard-lab-panel-full { max-width: 360px; max-height: 600px; }
1179
+ .standard-lab-panel-header { padding: 6px; border-bottom: var(--standard-lab-border); display: flex; align-items: center; justify-content: space-between; flex-shrink: 0; }
1180
+ .standard-lab-panel-header h2 { margin: 0; font-size: calc(var(--standard-lab-text-size) * 1.167); font-weight: 600; letter-spacing: -0.02em; }
1181
+ .standard-lab-viewport-info { font-size: calc(var(--standard-lab-text-size) * 0.917); font-weight: 400; color: var(--standard-lab-text); opacity: 0.7; margin-top: 2px; font-family: var(--font-monospace); }
1182
+ .standard-lab-header-actions { display: flex; gap: 6px; }
1183
+ .standard-lab-section { margin:0;padding: 4px; border-bottom: var(--standard-lab-border); }
1184
+ .standard-lab-section:last-of-type { border-bottom: none; }
1185
+ .standard-lab-label { display: block; font-size: calc(var(--standard-lab-text-size) * 0.917); font-weight: 600; color: var(--standard-lab-text); opacity: 0.7; text-transform: uppercase; letter-spacing: 0.05em; margin-bottom: 6px; }
1186
+ .standard-lab-toggle { display: flex; align-items: center; gap: 6px; cursor: pointer; user-select: none; }
1187
+ .standard-lab-toggle input[type="checkbox"] { width: 16px; height: 16px; cursor: pointer; margin: 0; }
1188
+ .standard-lab-toggle span { flex: 1; font-size: calc(var(--standard-lab-text-size) * 1.167); }
1189
+ .standard-lab-toggle kbd { font-size: calc(var(--standard-lab-text-size) * 1); font-family: var(--font-monospace); color: var(--standard-lab-text); opacity: 0.6; padding: 2px 6px; background: rgba(0,0,0,0.1); border-radius: 6px; }
1190
+ .standard-lab-select { width: 100%; padding: 6px 3px; margin:0 ; background: var(--standard-lab-bg); border: var(--standard-lab-border); border-radius: 6px; font-size: calc(var(--standard-lab-text-size) * 1.167); color: var(--standard-lab-text); cursor: pointer; }
1191
+ .standard-lab-actions { padding: 6px; border-top: var(--standard-lab-border); display: flex; flex-direction: column; gap: 6px; flex-shrink: 0; }
1192
+ .standard-lab-btn { font-size: calc(var(--standard-lab-text-size) * 1.167); width: 100%; padding: 6px 3px; background: var(--standard-lab-accent); color: var(--standard-lab-bg); border: var(--standard-lab-border); border-radius: 6px; font-weight: 600; cursor: pointer; transition: opacity 0.2s ease; }
1193
+ .standard-lab-btn:hover { opacity: 0.9; }
1194
+ .standard-lab-btn:active { opacity: 0.8; }
1195
+ .standard-lab-btn-secondary { background: rgba(255,255,255,0.1); color: var(--standard-lab-text); border: var(--standard-lab-border); }
1196
+ .standard-lab-btn-icon { font-size: calc(var(--standard-lab-text-size) * 1.667); width: 24px; height: 24px; padding: 0; background: transparent; border: none; color: var(--standard-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; }
1197
+ .standard-lab-btn-icon:hover { background: rgba(255,255,255,0.1); color: var(--standard-lab-text); opacity: 1; }
1198
+ .standard-lab-tabs { display: flex; border-bottom: var(--standard-lab-border); flex-shrink: 0; }
1199
+ .standard-lab-tab { flex: 1; padding: 6px; background: transparent; border: none; border-bottom: 2px solid transparent; font-size: calc(var(--standard-lab-text-size) * 1.333); font-weight: 500; color: var(--standard-lab-text); opacity: 0.6; cursor: pointer; transition: all 0.2s ease; }
1200
+ .standard-lab-tab:hover { color: var(--standard-lab-text); opacity: 1; background: rgba(255,255,255,0.05); }
1201
+ .standard-lab-tab[aria-selected="true"] { color: var(--standard-lab-accent); opacity: 1; border-bottom-color: var(--standard-lab-accent); font-weight: 600; }
1202
+ .standard-lab-filter { padding: 6px 6px; background: rgba(255,255,255,0.05); border-bottom: var(--standard-lab-border); flex-shrink: 0; }
1203
+ .standard-lab-content { flex: 1; overflow-y: auto; padding: 6px; }
1204
+ .standard-lab-content::-webkit-scrollbar { width: 6px; }
1205
+ .standard-lab-content::-webkit-scrollbar-track { background: transparent; }
1206
+ .standard-lab-content::-webkit-scrollbar-thumb { background: rgba(255,255,255,0.2); border-radius: 6px; }
1207
+ .standard-lab-content::-webkit-scrollbar-thumb:hover { background: rgba(255,255,255,0.3); }
1208
+ .standard-lab-token-list { display: flex; flex-direction: column; gap: 3px; }
1209
+ .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; }
1210
+ .standard-lab-token-row:hover { background: rgba(255,255,255,0.05); }
1211
+ .standard-lab-token-row.is-modified { background: rgba(var(--standard-lab-accent-rgb, 255), 0.05); }
1212
+ .standard-lab-text-input { padding: 0; background: transparent; border: 1px solid transparent; border-radius: 6px; font-size: var(--standard-lab-text-size); font-family: var(--font-monospace); color: var(--standard-lab-text); min-width: 0; transition: all 0.2s ease; }
1213
+ .standard-lab-text-input:focus { outline: none; background: rgba(255,255,255,0.05); border-color: var(--standard-lab-accent); }
1214
+ .standard-lab-color-container { width: 100%; }
1215
+ .standard-lab-color-label { display: block; font-size: calc(var(--standard-lab-text-size) * 0.917); font-family: var(--font-monospace); color: var(--standard-lab-text); opacity: 0.7; margin-bottom: 6px; }
1216
+ .standard-lab-color-controls { display: flex; gap: 6px; align-items: center; }
1217
+ .standard-lab-color-input { width: 40px; height: 40px; border: var(--standard-lab-border); border-radius: 6px; cursor: pointer; padding: 4px; background: rgba(255,255,255,0.05); flex-shrink: 0; }
1218
+ .standard-lab-color-input::-webkit-color-swatch-wrapper { padding: 0; }
1219
+ .standard-lab-color-input::-webkit-color-swatch { border: none; border-radius: 6px; }
1220
+ .standard-lab-color-value { flex: 1; min-width: 0; padding: 0; font-family: var(--font-monospace); font-size: var(--standard-lab-text-size); }
1221
+ .standard-lab-text-container { width: 100%; }
1222
+ .standard-lab-text-label { display: block; font-size: calc(var(--standard-lab-text-size) * 0.917); font-family: var(--font-monospace); color: var(--standard-lab-text); opacity: 0.7; margin-bottom: 6px; }
1223
+ .standard-lab-slider-container { width: 100%; }
1224
+ .standard-lab-slider-label { display: block; font-size: calc(var(--standard-lab-text-size) * 0.917); font-family: var(--font-monospace); color: var(--standard-lab-text); opacity: 0.7; margin-bottom: 6px; }
1225
+ .standard-lab-slider-controls { display: flex; gap: 6px; align-items: center; }
1226
+ .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; }
1227
+ .standard-lab-slider-input::-webkit-slider-thumb { -webkit-appearance: none; appearance: none; width: 16px; height: 16px; background: var(--standard-lab-accent); border-radius: 50%; cursor: grab; transition: transform 0.1s ease; }
1228
+ .standard-lab-slider-input::-webkit-slider-thumb:active { cursor: grabbing; transform: scale(1.2); }
1229
+ .standard-lab-slider-input::-moz-range-thumb { width: 16px; height: 16px; background: var(--standard-lab-accent); border-radius: 50%; border: none; cursor: grab; transition: transform 0.1s ease; }
1230
+ .standard-lab-slider-input::-moz-range-thumb:active { cursor: grabbing; transform: scale(1.2); }
1231
+ .standard-lab-slider-value { width: 80px; flex-shrink: 0; text-align: center; padding: 6px 8px; font-size: var(--standard-lab-text-size); }
1232
+ .standard-lab-btn-reset { opacity: 0; transition: opacity 0.2s ease; }
1233
+ .standard-lab-token-row:hover .standard-lab-btn-reset:not(:disabled) { opacity: 1; }
1234
+ .standard-lab-btn-reset:disabled { opacity: 0; cursor: not-allowed; }
1235
+ .standard-lab-empty { padding: 6px; text-align: center; color: var(--standard-lab-text); opacity: 0.6; font-size: calc(var(--standard-lab-text-size) * 1.167); }
1236
+ .standard-lab-notification { position: fixed; top: 6px; left: 50%; transform: translateX(-50%) translateY(-10px); background: var(--standard-lab-bg); border: var(--standard-lab-border); border-radius: 6px; padding: 8px 16px; box-shadow: var(--standard-lab-shadow); z-index: calc(var(--z-toast) + 1); font-size: calc(var(--standard-lab-text-size) * 1.167); opacity: 0; transition: opacity 0.2s ease, transform 0.2s ease; pointer-events: none; }
1237
+ .standard-lab-notification-success { background: var(--standard-lab-accent); color: var(--standard-lab-bg); border-color: var(--standard-lab-accent); }
1238
+ .standard-lab-notification-info { background: var(--standard-lab-accent); color: var(--standard-lab-bg); border-color: var(--standard-lab-accent); }
1239
+ @media (prefers-color-scheme: dark) { .standard-lab-panel { box-shadow: var(--standard-lab-shadow); } }
1240
+ @media (max-width: 768px) { .standard-lab-panel-quick, .standard-lab-panel-full { width: calc(100vw - 32px); max-width: 360px; } }
1241
+ @media print { .standard-lab-badge, .standard-lab-panel, .standard-lab-notification { display: none !important; } }
1242
+ `;
1243
+ document.head.appendChild(styles);
1244
+ }
1245
+ }
1246
+
1247
+ // ================================================================
1248
+ // AUTO-INITIALIZATION AND ASTRO CLIENTROUTER INTEGRATION
1249
+ // ================================================================
1250
+
1251
+ if (typeof window !== "undefined") {
1252
+ const setupStandardLab = () => {
1253
+ // Ensure body exists before initializing
1254
+ if (!document.body) {
1255
+ setTimeout(setupStandardLab, 10);
1256
+ return;
1257
+ }
1258
+
1259
+ if (!window.StandardLab) {
1260
+ window.StandardLab = new StandardLab();
1261
+ if (window.StandardLab.log) {
1262
+ window.StandardLab.log.info("Initialized");
1263
+ }
1264
+ }
1265
+
1266
+ // Handle HTMX swaps - Setup here to ensure document.body exists
1267
+ if (document.body) {
1268
+ document.body.addEventListener("htmx:afterSettle", () => {
1269
+ if (window.StandardLab) {
1270
+ if (window.StandardLab.log) {
1271
+ window.StandardLab.log.debug("HTMX swap detected");
1272
+ }
1273
+ window.StandardLab.refresh();
1274
+ }
1275
+ });
1276
+ }
1277
+ };
1278
+
1279
+ if (document.readyState === "loading") {
1280
+ document.addEventListener("DOMContentLoaded", setupStandardLab);
1281
+ } else {
1282
+ setupStandardLab();
1283
+ }
1284
+
1285
+ // Handle Astro ClientRouter page swaps - MUST check root container
1286
+ document.addEventListener("astro:after-swap", () => {
1287
+ const l = (window.StandardLab && window.StandardLab.log) || console;
1288
+ if (l.debug) l.debug("Page swap detected");
1289
+
1290
+ if (!window.StandardLab) {
1291
+ const l = (window.standard && window.standard.log) || console;
1292
+ if (l.warn) l.warn("Instance not found, reinitializing");
1293
+ window.StandardLab = new StandardLab();
1294
+ return;
1295
+ }
1296
+
1297
+ // Check if badge exists in the new DOM
1298
+ const badge = document.getElementById("standard-lab-badge-container");
1299
+
1300
+ // If badge is gone, recreate it
1301
+ if (!badge) {
1302
+ if (window.StandardLab.log) {
1303
+ window.StandardLab.log.debug("Recreating badge");
1304
+ }
1305
+ window.StandardLab.badge = null;
1306
+ window.StandardLab.createBadge();
1307
+ }
1308
+
1309
+ // If panel was open, we might want to close it or re-open it.
1310
+ // For now, let's ensure state is consistent.
1311
+ // If the panel element is missing from DOM but state says it's open, close it to reset.
1312
+ const panel = document.getElementById("standard-lab-panel-container");
1313
+ if (window.StandardLab.state !== "dormant" && !panel) {
1314
+ window.StandardLab.state = "dormant";
1315
+ window.StandardLab.panel = null;
1316
+ }
1317
+
1318
+ // Always restore state from storage
1319
+ window.StandardLab.restoreTheme();
1320
+ window.StandardLab.restoreDebugMode();
1321
+
1322
+ // Re-inject debug overlays if needed
1323
+ if (document.documentElement.classList.contains("standard-debug")) {
1324
+ window.StandardLab.injectGridDebugOverlays();
1325
+ }
1326
+ });
1327
+ }
1328
+ })();
package/package.json ADDED
@@ -0,0 +1,12 @@
1
+ {
2
+ "name": "@stnd/lab",
3
+ "version": "0.2.0",
4
+ "type": "module",
5
+ "main": "./lab.js",
6
+ "exports": {
7
+ ".": "./lab.js"
8
+ },
9
+ "publishConfig": {
10
+ "access": "public"
11
+ }
12
+ }