@stnd/lab 1.4.2 → 1.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +17 -0
- package/Lab.svelte +673 -170
- package/color.js +365 -0
- package/color.test.js +82 -0
- package/package.json +5 -2
package/Lab.svelte
CHANGED
|
@@ -15,6 +15,13 @@
|
|
|
15
15
|
import { modules as loadedModules } from "virtual:stnd/modules-metadata";
|
|
16
16
|
import { confirm } from "@stnd/ui/dialog.js";
|
|
17
17
|
import Combobox from "@stnd/ui/Combobox.svelte";
|
|
18
|
+
import {
|
|
19
|
+
clamp,
|
|
20
|
+
oklchToRgb,
|
|
21
|
+
rgbToHex,
|
|
22
|
+
formatOklch,
|
|
23
|
+
parseCssColor,
|
|
24
|
+
} from "./color.js";
|
|
18
25
|
|
|
19
26
|
// Sidebar transition parameters (defaults)
|
|
20
27
|
let sidebarDuration = $state(2000); // milliseconds
|
|
@@ -40,6 +47,26 @@
|
|
|
40
47
|
),
|
|
41
48
|
].sort((a, b) => a.localeCompare(b));
|
|
42
49
|
|
|
50
|
+
const MAX_CHROMA = 0.32;
|
|
51
|
+
const BG_PRESETS = [
|
|
52
|
+
{ label: "Pure White", value: "#ffffff" },
|
|
53
|
+
{ label: "Warm Paper", value: "#f7f3ee" },
|
|
54
|
+
{ label: "Soft Cream", value: "oklch(0.955 0.018 85)" },
|
|
55
|
+
{ label: "Sage Mist", value: "oklch(0.95 0.016 150)" },
|
|
56
|
+
{ label: "Warm Charcoal", value: "#231e1a" },
|
|
57
|
+
{ label: "Deep Slate", value: "oklch(0.19 0.015 255)" },
|
|
58
|
+
{ label: "Carbon", value: "#141413" },
|
|
59
|
+
];
|
|
60
|
+
const FG_PRESETS = [
|
|
61
|
+
{ label: "Warm Ink", value: "#262626" },
|
|
62
|
+
{ label: "Espresso", value: "#4a341c" },
|
|
63
|
+
{ label: "Forest Ink", value: "oklch(0.392 0.0488 67.4)" },
|
|
64
|
+
{ label: "Navy Ink", value: "oklch(0.28 0.045 255)" },
|
|
65
|
+
{ label: "Warm Parchment", value: "#e6cfb3" },
|
|
66
|
+
{ label: "Soft Ivory", value: "oklch(0.88 0.018 85)" },
|
|
67
|
+
{ label: "Pure White", value: "#ffffff" },
|
|
68
|
+
];
|
|
69
|
+
|
|
43
70
|
// =========================================================================
|
|
44
71
|
// STATE
|
|
45
72
|
// =========================================================================
|
|
@@ -61,54 +88,41 @@
|
|
|
61
88
|
let scrollTimer;
|
|
62
89
|
let sidebarEl = $state();
|
|
63
90
|
|
|
64
|
-
//
|
|
65
|
-
let
|
|
66
|
-
let
|
|
67
|
-
let
|
|
68
|
-
|
|
69
|
-
let
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
);
|
|
74
|
-
|
|
75
|
-
function openFontDropdown(token) {
|
|
76
|
-
fontDropdown = token;
|
|
77
|
-
fontSearch = "";
|
|
78
|
-
fontHighlight = -1;
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
function closeFontDropdown() {
|
|
82
|
-
fontDropdown = null;
|
|
83
|
-
fontSearch = "";
|
|
84
|
-
fontHighlight = -1;
|
|
85
|
-
}
|
|
91
|
+
// Custom OKLCH color picker state
|
|
92
|
+
let activePicker = $state(null); // null | "bg" | "fg"
|
|
93
|
+
let pickerFormat = $state("oklch"); // "oklch" | "hex"
|
|
94
|
+
let bgOklch = $state({ L: 1, C: 0, h: 0 });
|
|
95
|
+
let fgOklch = $state({ L: 0.2, C: 0, h: 0 });
|
|
96
|
+
let pickerCanvasEl = $state();
|
|
97
|
+
let isDraggingPlane = $state(false);
|
|
98
|
+
const hasEyeDropper =
|
|
99
|
+
typeof window !== "undefined" && "EyeDropper" in window;
|
|
86
100
|
|
|
87
101
|
function selectFont(token, name) {
|
|
88
102
|
if (!name) return;
|
|
89
|
-
const
|
|
90
|
-
|
|
103
|
+
const clean = name.trim().replace(/^"|"$/g, "");
|
|
104
|
+
const val = clean.includes(" ") ? `"${clean}"` : clean;
|
|
91
105
|
writeVar(token, val);
|
|
92
|
-
loadGoogleFont(
|
|
106
|
+
loadGoogleFont(clean);
|
|
93
107
|
if (token === "--font-text") fontText = val;
|
|
94
108
|
else if (token === "--font-header") fontHeader = val;
|
|
95
109
|
else if (token === "--font-interface") fontInterface = val;
|
|
96
110
|
else if (token === "--font-monospace") fontMono = val;
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
if (
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function previewFont(token, previewName, committedValue) {
|
|
114
|
+
if (typeof document === "undefined") return;
|
|
115
|
+
if (previewName) {
|
|
116
|
+
const clean = previewName.trim().replace(/^"|"$/g, "");
|
|
117
|
+
const val = clean.includes(" ") ? `"${clean}"` : clean;
|
|
118
|
+
loadGoogleFont(clean);
|
|
119
|
+
document.documentElement.style.setProperty(token, val);
|
|
120
|
+
} else {
|
|
121
|
+
if (modified.has(token) && committedValue) {
|
|
122
|
+
document.documentElement.style.setProperty(token, committedValue);
|
|
123
|
+
} else {
|
|
124
|
+
document.documentElement.style.removeProperty(token);
|
|
125
|
+
}
|
|
112
126
|
}
|
|
113
127
|
}
|
|
114
128
|
|
|
@@ -118,12 +132,13 @@
|
|
|
118
132
|
}
|
|
119
133
|
|
|
120
134
|
function requestCloseLab() {
|
|
121
|
-
if (pinned) return;
|
|
122
|
-
// Keep open if user is interacting with inputs inside
|
|
135
|
+
if (pinned || activePicker || isDraggingPlane) return;
|
|
136
|
+
// Keep open if user is interacting with inputs or popovers inside
|
|
123
137
|
if (
|
|
124
138
|
sidebarEl &&
|
|
125
139
|
typeof document !== "undefined" &&
|
|
126
|
-
sidebarEl.contains(document.activeElement)
|
|
140
|
+
(sidebarEl.contains(document.activeElement) ||
|
|
141
|
+
sidebarEl.querySelector(".std-combobox-popover"))
|
|
127
142
|
) {
|
|
128
143
|
return;
|
|
129
144
|
}
|
|
@@ -131,7 +146,7 @@
|
|
|
131
146
|
clearTimeout(closeTimer);
|
|
132
147
|
closeTimer = setTimeout(() => {
|
|
133
148
|
view = "dormant";
|
|
134
|
-
}, 100);
|
|
149
|
+
}, 100);
|
|
135
150
|
}
|
|
136
151
|
|
|
137
152
|
function cancelCloseLab() {
|
|
@@ -153,13 +168,36 @@
|
|
|
153
168
|
links: false,
|
|
154
169
|
});
|
|
155
170
|
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
typeof
|
|
159
|
-
|
|
171
|
+
/** Detect the active scheme of the current page */
|
|
172
|
+
function detectPageScheme() {
|
|
173
|
+
if (typeof document === "undefined") return "light";
|
|
174
|
+
const root = document.documentElement;
|
|
175
|
+
const body = document.body;
|
|
176
|
+
const explicitMode =
|
|
177
|
+
root.getAttribute("data-color-mode") ||
|
|
178
|
+
body?.getAttribute("data-color-mode");
|
|
179
|
+
if (explicitMode === "dark" || explicitMode === "light") {
|
|
180
|
+
return explicitMode;
|
|
181
|
+
}
|
|
182
|
+
if (
|
|
183
|
+
root.classList.contains("theme-dark") ||
|
|
184
|
+
body?.classList.contains("theme-dark")
|
|
185
|
+
) {
|
|
186
|
+
return "dark";
|
|
187
|
+
}
|
|
188
|
+
if (
|
|
189
|
+
root.classList.contains("theme-light") ||
|
|
190
|
+
body?.classList.contains("theme-light")
|
|
191
|
+
) {
|
|
192
|
+
return "light";
|
|
193
|
+
}
|
|
194
|
+
return window.matchMedia?.("(prefers-color-scheme: dark)").matches
|
|
160
195
|
? "dark"
|
|
161
|
-
: "light"
|
|
162
|
-
|
|
196
|
+
: "light";
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// Color scheme detection
|
|
200
|
+
let systemScheme = $state(detectPageScheme());
|
|
163
201
|
let userScheme = $state("auto");
|
|
164
202
|
let scheme = $derived(userScheme === "auto" ? systemScheme : userScheme);
|
|
165
203
|
|
|
@@ -176,6 +214,7 @@
|
|
|
176
214
|
} else {
|
|
177
215
|
document.documentElement.setAttribute("data-lab-scheme", userScheme);
|
|
178
216
|
}
|
|
217
|
+
requestAnimationFrame(syncFromDOM);
|
|
179
218
|
}
|
|
180
219
|
});
|
|
181
220
|
let bgToken = $derived(
|
|
@@ -219,23 +258,35 @@
|
|
|
219
258
|
// HELPERS
|
|
220
259
|
// =========================================================================
|
|
221
260
|
|
|
261
|
+
/** Find the element carrying the page's active theme tokens */
|
|
262
|
+
function getThemeTargetElement() {
|
|
263
|
+
if (typeof document === "undefined") return null;
|
|
264
|
+
if (document.documentElement.hasAttribute("data-theme")) {
|
|
265
|
+
return document.documentElement;
|
|
266
|
+
}
|
|
267
|
+
if (document.body?.hasAttribute("data-theme")) {
|
|
268
|
+
return document.body;
|
|
269
|
+
}
|
|
270
|
+
return document.documentElement;
|
|
271
|
+
}
|
|
272
|
+
|
|
222
273
|
/** Read a CSS custom property, resolving one level of var() */
|
|
223
|
-
function readVar(token) {
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
274
|
+
function readVar(token, el) {
|
|
275
|
+
if (typeof document === "undefined") return "";
|
|
276
|
+
const target = el || getThemeTargetElement() || document.documentElement;
|
|
277
|
+
const cs = getComputedStyle(target);
|
|
278
|
+
const raw = cs.getPropertyValue(token).trim();
|
|
227
279
|
if (!raw) return "";
|
|
228
280
|
const m = raw.match(/^var\(--([\w-]+)\)$/);
|
|
229
|
-
return m
|
|
230
|
-
? getComputedStyle(document.documentElement)
|
|
231
|
-
.getPropertyValue(`--${m[1]}`)
|
|
232
|
-
.trim()
|
|
233
|
-
: raw;
|
|
281
|
+
return m ? cs.getPropertyValue(`--${m[1]}`).trim() : raw;
|
|
234
282
|
}
|
|
235
283
|
|
|
236
284
|
/** Write an inline override and mark as modified */
|
|
237
285
|
function writeVar(token, value) {
|
|
238
286
|
document.documentElement.style.setProperty(token, value);
|
|
287
|
+
if (document.body?.hasAttribute("data-theme")) {
|
|
288
|
+
document.body.style.setProperty(token, value);
|
|
289
|
+
}
|
|
239
290
|
modified = new Set([...modified, token]);
|
|
240
291
|
|
|
241
292
|
// Persist to session
|
|
@@ -250,36 +301,6 @@
|
|
|
250
301
|
}
|
|
251
302
|
}
|
|
252
303
|
|
|
253
|
-
/** Convert any CSS color to #hex for <input type="color"> */
|
|
254
|
-
function toHex(css) {
|
|
255
|
-
if (!css) return "#000000";
|
|
256
|
-
if (/^#[0-9a-f]{6}$/i.test(css.trim())) return css.trim();
|
|
257
|
-
if (css.trim().toLowerCase() === "white") return "#ffffff";
|
|
258
|
-
if (css.trim().toLowerCase() === "black") return "#000000";
|
|
259
|
-
|
|
260
|
-
// Use a temp element to resolve any CSS color format
|
|
261
|
-
try {
|
|
262
|
-
const el = document.createElement("div");
|
|
263
|
-
el.style.color = css;
|
|
264
|
-
document.body.appendChild(el);
|
|
265
|
-
const resolved = getComputedStyle(el).color;
|
|
266
|
-
el.remove();
|
|
267
|
-
const m = resolved.match(/(\d+)/g);
|
|
268
|
-
if (m && m.length >= 3) {
|
|
269
|
-
return (
|
|
270
|
-
"#" +
|
|
271
|
-
m
|
|
272
|
-
.slice(0, 3)
|
|
273
|
-
.map((n) => parseInt(n).toString(16).padStart(2, "0"))
|
|
274
|
-
.join("")
|
|
275
|
-
);
|
|
276
|
-
}
|
|
277
|
-
} catch {
|
|
278
|
-
/* noop */
|
|
279
|
-
}
|
|
280
|
-
return "#000000";
|
|
281
|
-
}
|
|
282
|
-
|
|
283
304
|
/** Resolve a CSS length (var, calc, unit) to pixels */
|
|
284
305
|
function resolvePx(value) {
|
|
285
306
|
if (typeof document === "undefined" || !value) return 0;
|
|
@@ -299,25 +320,53 @@
|
|
|
299
320
|
|
|
300
321
|
/** Read all token values from the current DOM cascade */
|
|
301
322
|
function syncFromDOM() {
|
|
302
|
-
|
|
303
|
-
systemScheme =
|
|
304
|
-
|
|
305
|
-
|
|
323
|
+
if (typeof document === "undefined") return;
|
|
324
|
+
systemScheme = detectPageScheme();
|
|
325
|
+
const target = getThemeTargetElement();
|
|
326
|
+
|
|
327
|
+
// Read and resolve the active scheme's background and foreground seeds
|
|
328
|
+
const rawBg = readVar(bgToken, target);
|
|
329
|
+
const rawFg = readVar(fgToken, target);
|
|
330
|
+
|
|
331
|
+
const parsedBg = parseCssColor(
|
|
332
|
+
rawBg && !/\b(from|light-dark|var)\b/i.test(rawBg)
|
|
333
|
+
? rawBg
|
|
334
|
+
: `var(${bgToken}, var(--color-background))`,
|
|
335
|
+
target,
|
|
336
|
+
);
|
|
337
|
+
const parsedFg = parseCssColor(
|
|
338
|
+
rawFg && !/\b(from|light-dark|var)\b/i.test(rawFg)
|
|
339
|
+
? rawFg
|
|
340
|
+
: `var(${fgToken}, var(--color-foreground))`,
|
|
341
|
+
target,
|
|
342
|
+
);
|
|
306
343
|
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
344
|
+
seedBg =
|
|
345
|
+
rawBg && !/\b(from|light-dark|var|calc|color-mix)\b/i.test(rawBg)
|
|
346
|
+
? rawBg
|
|
347
|
+
: parsedBg.css;
|
|
348
|
+
seedFg =
|
|
349
|
+
rawFg && !/\b(from|light-dark|var|calc|color-mix)\b/i.test(rawFg)
|
|
350
|
+
? rawFg
|
|
351
|
+
: parsedFg.css;
|
|
352
|
+
|
|
353
|
+
seedBgHex = parsedBg.hex;
|
|
354
|
+
seedFgHex = parsedFg.hex;
|
|
355
|
+
bgOklch = { L: parsedBg.L, C: parsedBg.C, h: parsedBg.h };
|
|
356
|
+
fgOklch = { L: parsedFg.L, C: parsedFg.C, h: parsedFg.h };
|
|
357
|
+
|
|
358
|
+
fontText = readVar("--font-text", target);
|
|
359
|
+
fontHeader = readVar("--font-header", target);
|
|
360
|
+
fontInterface = readVar("--font-interface", target);
|
|
361
|
+
fontMono = readVar("--font-monospace", target);
|
|
362
|
+
fontSize =
|
|
363
|
+
parseFloat(readVar("--font-text-size", target)) ||
|
|
364
|
+
parseFloat(readVar("--font-size", target)) ||
|
|
365
|
+
1;
|
|
366
|
+
lineHeight = parseFloat(readVar("--line-height", target)) || 1.414;
|
|
367
|
+
ratio = parseFloat(readVar("--optical-ratio", target)) || 1.414;
|
|
368
|
+
|
|
369
|
+
const rawGap = readVar("--page-padding", target);
|
|
321
370
|
const pxSpace = resolvePx("var(--space)");
|
|
322
371
|
|
|
323
372
|
// Calculate body gap multiplier
|
|
@@ -329,13 +378,16 @@
|
|
|
329
378
|
rhythmGap = pxSpace ? pxRhythm / pxSpace : 0.5;
|
|
330
379
|
|
|
331
380
|
// Calculate radius multiplier
|
|
332
|
-
const rawRadius = readVar("--radius");
|
|
381
|
+
const rawRadius = readVar("--radius", target);
|
|
333
382
|
const pxRadius = resolvePx(rawRadius);
|
|
334
383
|
radius = pxSpace ? pxRadius / pxSpace : 0;
|
|
335
384
|
|
|
336
385
|
debug = document.documentElement.classList.contains("stnd-debug");
|
|
337
386
|
eink = document.documentElement.classList.contains("standard-eink");
|
|
338
|
-
theme =
|
|
387
|
+
theme =
|
|
388
|
+
document.documentElement.getAttribute("data-theme") ||
|
|
389
|
+
document.body?.getAttribute("data-theme") ||
|
|
390
|
+
"";
|
|
339
391
|
viewportW = window.innerWidth;
|
|
340
392
|
}
|
|
341
393
|
|
|
@@ -350,7 +402,7 @@
|
|
|
350
402
|
"--font-header",
|
|
351
403
|
"--font-interface",
|
|
352
404
|
"--font-monospace",
|
|
353
|
-
"--font-size",
|
|
405
|
+
"--font-text-size",
|
|
354
406
|
"--line-height",
|
|
355
407
|
"--optical-ratio",
|
|
356
408
|
"--page-padding",
|
|
@@ -410,7 +462,7 @@
|
|
|
410
462
|
"--font-header",
|
|
411
463
|
"--font-interface",
|
|
412
464
|
"--font-monospace",
|
|
413
|
-
"--font-size",
|
|
465
|
+
"--font-text-size",
|
|
414
466
|
"--line-height",
|
|
415
467
|
"--optical-ratio",
|
|
416
468
|
"--page-padding",
|
|
@@ -588,6 +640,15 @@
|
|
|
588
640
|
const savedOverrides = JSON.parse(
|
|
589
641
|
sessionStorage.getItem("stnd-overrides") || "{}",
|
|
590
642
|
);
|
|
643
|
+
if (savedOverrides["--font-size"]) {
|
|
644
|
+
delete savedOverrides["--font-size"];
|
|
645
|
+
document.documentElement.style.removeProperty("--font-size");
|
|
646
|
+
try {
|
|
647
|
+
sessionStorage.setItem("stnd-overrides", JSON.stringify(savedOverrides));
|
|
648
|
+
} catch {
|
|
649
|
+
/* noop */
|
|
650
|
+
}
|
|
651
|
+
}
|
|
591
652
|
if (Object.keys(savedOverrides).length > 0) {
|
|
592
653
|
for (const [token, value] of Object.entries(savedOverrides)) {
|
|
593
654
|
document.documentElement.style.setProperty(token, value);
|
|
@@ -681,21 +742,35 @@
|
|
|
681
742
|
|
|
682
743
|
function onGlobalClick(e) {
|
|
683
744
|
if (view !== "open" || pinned) return;
|
|
684
|
-
if (sidebarEl &&
|
|
745
|
+
if (sidebarEl && e.composedPath().includes(sidebarEl)) return;
|
|
685
746
|
if (
|
|
686
|
-
e.target.closest(".lab-handler") ||
|
|
687
|
-
e.target.closest(".lab-handler-top")
|
|
747
|
+
e.target.closest?.(".lab-handler") ||
|
|
748
|
+
e.target.closest?.(".lab-handler-top")
|
|
688
749
|
)
|
|
689
750
|
return;
|
|
690
751
|
view = "dormant";
|
|
691
752
|
}
|
|
692
753
|
|
|
754
|
+
const domObserver = new MutationObserver(() => {
|
|
755
|
+
requestAnimationFrame(syncFromDOM);
|
|
756
|
+
});
|
|
757
|
+
domObserver.observe(document.documentElement, {
|
|
758
|
+
attributes: true,
|
|
759
|
+
attributeFilter: [
|
|
760
|
+
"data-theme",
|
|
761
|
+
"data-color-mode",
|
|
762
|
+
"class",
|
|
763
|
+
"data-lab-scheme",
|
|
764
|
+
],
|
|
765
|
+
});
|
|
766
|
+
|
|
693
767
|
document.addEventListener("keydown", onKey);
|
|
694
768
|
document.addEventListener("click", onGlobalClick);
|
|
695
769
|
window.addEventListener("resize", onResize);
|
|
696
770
|
window.addEventListener("scroll", onScroll, { passive: true });
|
|
697
771
|
|
|
698
772
|
return () => {
|
|
773
|
+
domObserver.disconnect();
|
|
699
774
|
document.removeEventListener("keydown", onKey);
|
|
700
775
|
document.removeEventListener("click", onGlobalClick);
|
|
701
776
|
document.removeEventListener("astro:after-swap", restoreLabState);
|
|
@@ -741,6 +816,37 @@
|
|
|
741
816
|
}
|
|
742
817
|
});
|
|
743
818
|
|
|
819
|
+
// Render 2D color area on canvas whenever active picker, hue, or picker open state changes
|
|
820
|
+
$effect(() => {
|
|
821
|
+
if (!activePicker || !pickerCanvasEl) return;
|
|
822
|
+
const canvas = pickerCanvasEl;
|
|
823
|
+
const ctx = canvas.getContext("2d");
|
|
824
|
+
if (!ctx) return;
|
|
825
|
+
|
|
826
|
+
const width = canvas.width;
|
|
827
|
+
const height = canvas.height;
|
|
828
|
+
const imgData = ctx.createImageData(width, height);
|
|
829
|
+
const data = imgData.data;
|
|
830
|
+
|
|
831
|
+
const cur = activePicker === "bg" ? bgOklch : fgOklch;
|
|
832
|
+
const h = cur.h;
|
|
833
|
+
|
|
834
|
+
// Render 2D grid: X = Chroma (0 to MAX_CHROMA), Y = Lightness (1 at top to 0 at bottom)
|
|
835
|
+
for (let y = 0; y < height; y++) {
|
|
836
|
+
const L = 1 - y / (height - 1);
|
|
837
|
+
for (let x = 0; x < width; x++) {
|
|
838
|
+
const C = (x / (width - 1)) * MAX_CHROMA;
|
|
839
|
+
const rgb = oklchToRgb(L, C, h);
|
|
840
|
+
const idx = (y * width + x) * 4;
|
|
841
|
+
data[idx] = rgb.r;
|
|
842
|
+
data[idx + 1] = rgb.g;
|
|
843
|
+
data[idx + 2] = rgb.b;
|
|
844
|
+
data[idx + 3] = rgb.inGamut ? 255 : 180;
|
|
845
|
+
}
|
|
846
|
+
}
|
|
847
|
+
ctx.putImageData(imgData, 0, 0);
|
|
848
|
+
});
|
|
849
|
+
|
|
744
850
|
// =========================================================================
|
|
745
851
|
// HANDLERS — Theme
|
|
746
852
|
// =========================================================================
|
|
@@ -758,6 +864,7 @@
|
|
|
758
864
|
"--font-header",
|
|
759
865
|
"--font-interface",
|
|
760
866
|
"--font-monospace",
|
|
867
|
+
"--font-text-size",
|
|
761
868
|
"--font-size",
|
|
762
869
|
"--line-height",
|
|
763
870
|
"--optical-ratio",
|
|
@@ -786,50 +893,115 @@
|
|
|
786
893
|
}
|
|
787
894
|
|
|
788
895
|
// =========================================================================
|
|
789
|
-
// HANDLERS —
|
|
896
|
+
// HANDLERS — Color Picker
|
|
790
897
|
// =========================================================================
|
|
791
|
-
function
|
|
792
|
-
|
|
898
|
+
function toggleColorPicker(role) {
|
|
899
|
+
activePicker = activePicker === role ? null : role;
|
|
900
|
+
}
|
|
901
|
+
|
|
902
|
+
function updateColorFromOklch(role, newOklch) {
|
|
903
|
+
const L = clamp(newOklch.L, 0, 1);
|
|
904
|
+
const C = clamp(newOklch.C, 0, MAX_CHROMA);
|
|
905
|
+
const h = ((newOklch.h % 360) + 360) % 360;
|
|
906
|
+
|
|
907
|
+
const rgb = oklchToRgb(L, C, h);
|
|
908
|
+
const hex = rgbToHex(rgb);
|
|
909
|
+
const oklchStr = formatOklch(L, C, h);
|
|
910
|
+
const formatted = pickerFormat === "hex" ? hex : oklchStr;
|
|
911
|
+
|
|
793
912
|
const token = role === "bg" ? bgToken : fgToken;
|
|
913
|
+
|
|
794
914
|
if (role === "bg") {
|
|
795
|
-
|
|
915
|
+
bgOklch = { L, C, h };
|
|
916
|
+
seedBg = formatted;
|
|
796
917
|
seedBgHex = hex;
|
|
797
918
|
} else {
|
|
798
|
-
|
|
919
|
+
fgOklch = { L, C, h };
|
|
920
|
+
seedFg = formatted;
|
|
799
921
|
seedFgHex = hex;
|
|
800
922
|
}
|
|
801
|
-
|
|
923
|
+
|
|
924
|
+
writeVar(token, formatted);
|
|
802
925
|
}
|
|
803
926
|
|
|
804
927
|
function onColorText(role, e) {
|
|
805
928
|
const val = e.target.value;
|
|
806
929
|
const token = role === "bg" ? bgToken : fgToken;
|
|
930
|
+
const target = getThemeTargetElement();
|
|
931
|
+
const parsed = parseCssColor(val, target);
|
|
932
|
+
|
|
807
933
|
if (role === "bg") {
|
|
808
934
|
seedBg = val;
|
|
809
|
-
seedBgHex =
|
|
935
|
+
seedBgHex = parsed.hex;
|
|
936
|
+
bgOklch = { L: parsed.L, C: parsed.C, h: parsed.h };
|
|
810
937
|
} else {
|
|
811
938
|
seedFg = val;
|
|
812
|
-
seedFgHex =
|
|
939
|
+
seedFgHex = parsed.hex;
|
|
940
|
+
fgOklch = { L: parsed.L, C: parsed.C, h: parsed.h };
|
|
813
941
|
}
|
|
814
942
|
writeVar(token, val);
|
|
815
943
|
}
|
|
816
944
|
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
945
|
+
function onSliderInput(role, channel, e) {
|
|
946
|
+
const val = parseFloat(e.target.value);
|
|
947
|
+
const cur = role === "bg" ? { ...bgOklch } : { ...fgOklch };
|
|
948
|
+
cur[channel] = val;
|
|
949
|
+
updateColorFromOklch(role, cur);
|
|
950
|
+
}
|
|
951
|
+
|
|
952
|
+
function handlePlanePointer(e) {
|
|
953
|
+
if (!pickerCanvasEl || !activePicker) return;
|
|
954
|
+
const rect = pickerCanvasEl.getBoundingClientRect();
|
|
955
|
+
const x = clamp(e.clientX - rect.left, 0, rect.width);
|
|
956
|
+
const y = clamp(e.clientY - rect.top, 0, rect.height);
|
|
957
|
+
|
|
958
|
+
const C = (x / rect.width) * MAX_CHROMA;
|
|
959
|
+
const L = 1 - y / rect.height;
|
|
960
|
+
|
|
961
|
+
const cur = activePicker === "bg" ? bgOklch : fgOklch;
|
|
962
|
+
updateColorFromOklch(activePicker, { L, C, h: cur.h });
|
|
963
|
+
}
|
|
964
|
+
|
|
965
|
+
function onPlanePointerDown(e) {
|
|
966
|
+
isDraggingPlane = true;
|
|
967
|
+
e.currentTarget.setPointerCapture(e.pointerId);
|
|
968
|
+
handlePlanePointer(e);
|
|
969
|
+
}
|
|
970
|
+
|
|
971
|
+
function onPlanePointerMove(e) {
|
|
972
|
+
if (!isDraggingPlane) return;
|
|
973
|
+
handlePlanePointer(e);
|
|
974
|
+
}
|
|
975
|
+
|
|
976
|
+
function onPlanePointerUp(e) {
|
|
977
|
+
if (isDraggingPlane) {
|
|
978
|
+
isDraggingPlane = false;
|
|
979
|
+
try {
|
|
980
|
+
e.currentTarget.releasePointerCapture(e.pointerId);
|
|
981
|
+
} catch {
|
|
982
|
+
/* noop */
|
|
983
|
+
}
|
|
984
|
+
}
|
|
985
|
+
}
|
|
986
|
+
|
|
987
|
+
async function pickWithEyeDropper(role) {
|
|
988
|
+
if (!hasEyeDropper) return;
|
|
989
|
+
try {
|
|
990
|
+
const eyeDropper = new window.EyeDropper();
|
|
991
|
+
const result = await eyeDropper.open();
|
|
992
|
+
if (result?.sRGBHex) {
|
|
993
|
+
const parsed = parseCssColor(result.sRGBHex);
|
|
994
|
+
updateColorFromOklch(role, parsed);
|
|
995
|
+
}
|
|
996
|
+
} catch {
|
|
997
|
+
/* User dismissed eyedropper */
|
|
998
|
+
}
|
|
999
|
+
}
|
|
1000
|
+
|
|
1001
|
+
function applyPreset(role, val) {
|
|
1002
|
+
const target = getThemeTargetElement();
|
|
1003
|
+
const parsed = parseCssColor(val, target);
|
|
1004
|
+
updateColorFromOklch(role, parsed);
|
|
833
1005
|
}
|
|
834
1006
|
|
|
835
1007
|
// =========================================================================
|
|
@@ -837,7 +1009,11 @@
|
|
|
837
1009
|
// =========================================================================
|
|
838
1010
|
function onFontSizeInput(e) {
|
|
839
1011
|
fontSize = parseFloat(e.target.value);
|
|
840
|
-
|
|
1012
|
+
document.documentElement.style.removeProperty("--font-size");
|
|
1013
|
+
if (document.body?.hasAttribute("data-theme")) {
|
|
1014
|
+
document.body.style.removeProperty("--font-size");
|
|
1015
|
+
}
|
|
1016
|
+
writeVar("--font-text-size", `${fontSize}rem`);
|
|
841
1017
|
}
|
|
842
1018
|
|
|
843
1019
|
function onLineHeightInput(e) {
|
|
@@ -1093,49 +1269,207 @@
|
|
|
1093
1269
|
{#if sectionsOpen.colors}
|
|
1094
1270
|
<div class="lab-section-body" transition:slide>
|
|
1095
1271
|
<div class="lab-field">
|
|
1096
|
-
<label class="lab-field-label" for="lab-bg-text">Background</label
|
|
1097
|
-
>
|
|
1272
|
+
<label class="lab-field-label" for="lab-bg-text">Background</label>
|
|
1098
1273
|
<div class="lab-color-row">
|
|
1099
|
-
<
|
|
1100
|
-
type="
|
|
1274
|
+
<button
|
|
1275
|
+
type="button"
|
|
1101
1276
|
class="lab-swatch"
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1277
|
+
class:active={activePicker === "bg"}
|
|
1278
|
+
style="background-color: {seedBgHex};"
|
|
1279
|
+
title="Toggle OKLCH background picker"
|
|
1280
|
+
onclick={() => toggleColorPicker("bg")}
|
|
1281
|
+
aria-label="Pick background color"
|
|
1282
|
+
></button>
|
|
1106
1283
|
<input
|
|
1107
1284
|
id="lab-bg-text"
|
|
1108
1285
|
type="text"
|
|
1109
1286
|
class="lab-text-input"
|
|
1110
1287
|
value={seedBg}
|
|
1111
1288
|
spellcheck="false"
|
|
1112
|
-
placeholder="e.g.
|
|
1289
|
+
placeholder="e.g. oklch(0.23 0.02 67)"
|
|
1113
1290
|
oninput={(e) => onColorText("bg", e)}
|
|
1114
1291
|
/>
|
|
1115
1292
|
</div>
|
|
1116
1293
|
</div>
|
|
1294
|
+
|
|
1117
1295
|
<div class="lab-field">
|
|
1118
|
-
<label class="lab-field-label" for="lab-fg-text">Foreground</label
|
|
1119
|
-
>
|
|
1296
|
+
<label class="lab-field-label" for="lab-fg-text">Foreground</label>
|
|
1120
1297
|
<div class="lab-color-row">
|
|
1121
|
-
<
|
|
1122
|
-
type="
|
|
1298
|
+
<button
|
|
1299
|
+
type="button"
|
|
1123
1300
|
class="lab-swatch"
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1301
|
+
class:active={activePicker === "fg"}
|
|
1302
|
+
style="background-color: {seedFgHex};"
|
|
1303
|
+
title="Toggle OKLCH foreground picker"
|
|
1304
|
+
onclick={() => toggleColorPicker("fg")}
|
|
1305
|
+
aria-label="Pick foreground color"
|
|
1306
|
+
></button>
|
|
1128
1307
|
<input
|
|
1129
1308
|
id="lab-fg-text"
|
|
1130
1309
|
type="text"
|
|
1131
1310
|
class="lab-text-input"
|
|
1132
1311
|
value={seedFg}
|
|
1133
1312
|
spellcheck="false"
|
|
1134
|
-
placeholder="e.g. #
|
|
1313
|
+
placeholder="e.g. #262626"
|
|
1135
1314
|
oninput={(e) => onColorText("fg", e)}
|
|
1136
1315
|
/>
|
|
1137
1316
|
</div>
|
|
1138
1317
|
</div>
|
|
1318
|
+
|
|
1319
|
+
<!-- Custom OKLCH Color Picker Panel -->
|
|
1320
|
+
{#if activePicker}
|
|
1321
|
+
{@const curOklch = activePicker === "bg" ? bgOklch : fgOklch}
|
|
1322
|
+
{@const curHex = activePicker === "bg" ? seedBgHex : seedFgHex}
|
|
1323
|
+
{@const presets = activePicker === "bg" ? BG_PRESETS : FG_PRESETS}
|
|
1324
|
+
<div class="lab-picker-panel" transition:slide>
|
|
1325
|
+
<header class="lab-picker-header">
|
|
1326
|
+
<div class="lab-picker-title">
|
|
1327
|
+
<span class="lab-picker-role"
|
|
1328
|
+
>{activePicker === "bg" ? "Background" : "Foreground"}</span
|
|
1329
|
+
>
|
|
1330
|
+
<span class="lab-picker-hex">{curHex}</span>
|
|
1331
|
+
</div>
|
|
1332
|
+
<div class="lab-picker-actions">
|
|
1333
|
+
<button
|
|
1334
|
+
type="button"
|
|
1335
|
+
class="smaller ghost"
|
|
1336
|
+
title="Toggle output format (OKLCH or Hex)"
|
|
1337
|
+
onclick={() => {
|
|
1338
|
+
pickerFormat =
|
|
1339
|
+
pickerFormat === "oklch" ? "hex" : "oklch";
|
|
1340
|
+
updateColorFromOklch(activePicker, curOklch);
|
|
1341
|
+
}}
|
|
1342
|
+
>
|
|
1343
|
+
{pickerFormat.toUpperCase()}
|
|
1344
|
+
</button>
|
|
1345
|
+
{#if hasEyeDropper}
|
|
1346
|
+
<button
|
|
1347
|
+
type="button"
|
|
1348
|
+
class="smaller icon ghost"
|
|
1349
|
+
title="Pick color from screen"
|
|
1350
|
+
onclick={() => pickWithEyeDropper(activePicker)}
|
|
1351
|
+
>
|
|
1352
|
+
<svg
|
|
1353
|
+
width="1em"
|
|
1354
|
+
height="1em"
|
|
1355
|
+
viewBox="0 0 24 24"
|
|
1356
|
+
fill="none"
|
|
1357
|
+
stroke="currentColor"
|
|
1358
|
+
stroke-width="2"
|
|
1359
|
+
stroke-linecap="round"
|
|
1360
|
+
stroke-linejoin="round"
|
|
1361
|
+
>
|
|
1362
|
+
<path
|
|
1363
|
+
d="m14 7 3 3m-8.5 8.5L4 20l1.5-4.5L14 7l3 3L8.5 18.5Z"
|
|
1364
|
+
></path>
|
|
1365
|
+
</svg>
|
|
1366
|
+
</button>
|
|
1367
|
+
{/if}
|
|
1368
|
+
<button
|
|
1369
|
+
type="button"
|
|
1370
|
+
class="smaller icon ghost"
|
|
1371
|
+
title="Close color picker"
|
|
1372
|
+
onclick={() => (activePicker = null)}
|
|
1373
|
+
>
|
|
1374
|
+
✕
|
|
1375
|
+
</button>
|
|
1376
|
+
</div>
|
|
1377
|
+
</header>
|
|
1378
|
+
|
|
1379
|
+
<!-- 2D Lightness x Chroma Color Plane -->
|
|
1380
|
+
<div
|
|
1381
|
+
class="lab-plane-wrapper"
|
|
1382
|
+
role="slider"
|
|
1383
|
+
tabindex="0"
|
|
1384
|
+
aria-label="Lightness and Chroma 2D plane"
|
|
1385
|
+
aria-valuemin="0"
|
|
1386
|
+
aria-valuemax="100"
|
|
1387
|
+
aria-valuenow={Math.round(curOklch.L * 100)}
|
|
1388
|
+
onpointerdown={onPlanePointerDown}
|
|
1389
|
+
onpointermove={onPlanePointerMove}
|
|
1390
|
+
onpointerup={onPlanePointerUp}
|
|
1391
|
+
>
|
|
1392
|
+
<canvas
|
|
1393
|
+
bind:this={pickerCanvasEl}
|
|
1394
|
+
width="240"
|
|
1395
|
+
height="110"
|
|
1396
|
+
class="lab-plane-canvas"
|
|
1397
|
+
></canvas>
|
|
1398
|
+
<div
|
|
1399
|
+
class="lab-plane-thumb"
|
|
1400
|
+
style="left: {(curOklch.C / MAX_CHROMA) *
|
|
1401
|
+
100}%; top: {(1 - curOklch.L) *
|
|
1402
|
+
100}%; background-color: {curHex};"
|
|
1403
|
+
></div>
|
|
1404
|
+
</div>
|
|
1405
|
+
|
|
1406
|
+
<!-- Perceptual OKLCH Sliders -->
|
|
1407
|
+
<div class="lab-picker-sliders">
|
|
1408
|
+
<!-- Lightness Slider -->
|
|
1409
|
+
<div class="lab-picker-slider-row">
|
|
1410
|
+
<span class="lab-slider-label">L</span>
|
|
1411
|
+
<input
|
|
1412
|
+
type="range"
|
|
1413
|
+
class="lab-range lab-range-l"
|
|
1414
|
+
min="0"
|
|
1415
|
+
max="1"
|
|
1416
|
+
step="0.005"
|
|
1417
|
+
value={curOklch.L}
|
|
1418
|
+
style="--slider-track: linear-gradient(to right, oklch(0 {curOklch.C} {curOklch.h}), oklch(0.5 {curOklch.C} {curOklch.h}), oklch(1 {curOklch.C} {curOklch.h}));"
|
|
1419
|
+
oninput={(e) => onSliderInput(activePicker, "L", e)}
|
|
1420
|
+
/>
|
|
1421
|
+
<span class="lab-slider-val"
|
|
1422
|
+
>{Math.round(curOklch.L * 100)}%</span
|
|
1423
|
+
>
|
|
1424
|
+
</div>
|
|
1425
|
+
|
|
1426
|
+
<!-- Chroma Slider -->
|
|
1427
|
+
<div class="lab-picker-slider-row">
|
|
1428
|
+
<span class="lab-slider-label">C</span>
|
|
1429
|
+
<input
|
|
1430
|
+
type="range"
|
|
1431
|
+
class="lab-range lab-range-c"
|
|
1432
|
+
min="0"
|
|
1433
|
+
max={MAX_CHROMA}
|
|
1434
|
+
step="0.002"
|
|
1435
|
+
value={curOklch.C}
|
|
1436
|
+
style="--slider-track: linear-gradient(to right, oklch({curOklch.L} 0 {curOklch.h}), oklch({curOklch.L} {MAX_CHROMA *
|
|
1437
|
+
0.5} {curOklch.h}), oklch({curOklch.L} {MAX_CHROMA} {curOklch.h}));"
|
|
1438
|
+
oninput={(e) => onSliderInput(activePicker, "C", e)}
|
|
1439
|
+
/>
|
|
1440
|
+
<span class="lab-slider-val">{curOklch.C.toFixed(3)}</span>
|
|
1441
|
+
</div>
|
|
1442
|
+
|
|
1443
|
+
<!-- Hue Slider -->
|
|
1444
|
+
<div class="lab-picker-slider-row">
|
|
1445
|
+
<span class="lab-slider-label">H</span>
|
|
1446
|
+
<input
|
|
1447
|
+
type="range"
|
|
1448
|
+
class="lab-range lab-range-h"
|
|
1449
|
+
min="0"
|
|
1450
|
+
max="360"
|
|
1451
|
+
step="1"
|
|
1452
|
+
value={curOklch.h}
|
|
1453
|
+
oninput={(e) => onSliderInput(activePicker, "h", e)}
|
|
1454
|
+
/>
|
|
1455
|
+
<span class="lab-slider-val">{Math.round(curOklch.h)}°</span>
|
|
1456
|
+
</div>
|
|
1457
|
+
</div>
|
|
1458
|
+
|
|
1459
|
+
<!-- Palette Presets -->
|
|
1460
|
+
<div class="lab-picker-presets">
|
|
1461
|
+
{#each presets as p}
|
|
1462
|
+
<button
|
|
1463
|
+
type="button"
|
|
1464
|
+
class="lab-preset-dot"
|
|
1465
|
+
title={p.label}
|
|
1466
|
+
style="background-color: {p.value};"
|
|
1467
|
+
onclick={() => applyPreset(activePicker, p.value)}
|
|
1468
|
+
></button>
|
|
1469
|
+
{/each}
|
|
1470
|
+
</div>
|
|
1471
|
+
</div>
|
|
1472
|
+
{/if}
|
|
1139
1473
|
</div>
|
|
1140
1474
|
{/if}
|
|
1141
1475
|
</section>
|
|
@@ -1168,13 +1502,13 @@
|
|
|
1168
1502
|
emptyText="No fonts found."
|
|
1169
1503
|
allowCustom={true}
|
|
1170
1504
|
preview={true}
|
|
1171
|
-
previewDebounce={
|
|
1505
|
+
previewDebounce={60}
|
|
1172
1506
|
loadFont={(name) => loadGoogleFont(name)}
|
|
1173
1507
|
class="lab-font-combobox"
|
|
1174
1508
|
aria-controls={inputId}
|
|
1175
|
-
onpreview={(e) =>
|
|
1509
|
+
onpreview={(e) =>
|
|
1510
|
+
previewFont(token, e.detail?.value, currentValue)}
|
|
1176
1511
|
onchange={(e) => {
|
|
1177
|
-
// Combobox dispatches change with detail.value (base font name)
|
|
1178
1512
|
selectFont(token, e.detail?.value);
|
|
1179
1513
|
}}
|
|
1180
1514
|
/>
|
|
@@ -1546,14 +1880,16 @@
|
|
|
1546
1880
|
}
|
|
1547
1881
|
}
|
|
1548
1882
|
|
|
1549
|
-
/* Reset children to Lab font */
|
|
1883
|
+
/* Reset children to Lab font, allowing explicit font-family previews to win */
|
|
1550
1884
|
.lab-sidebar * {
|
|
1551
|
-
font-family: var(--font-monospace) !important;
|
|
1552
1885
|
box-sizing: border-box;
|
|
1553
1886
|
/* Disable opacity transitions on child elements to prevent fading when opening the lab.
|
|
1554
1887
|
Other property transitions for children remain allowed outside of opacity. */
|
|
1555
1888
|
transition: opacity 0s !important;
|
|
1556
1889
|
}
|
|
1890
|
+
.lab-sidebar *:not([style*="font-family"]) {
|
|
1891
|
+
font-family: var(--font-monospace);
|
|
1892
|
+
}
|
|
1557
1893
|
|
|
1558
1894
|
/* ═══════════ HEADER ═══════════ */
|
|
1559
1895
|
.lab-header {
|
|
@@ -1683,34 +2019,201 @@
|
|
|
1683
2019
|
margin: 0 !important;
|
|
1684
2020
|
}
|
|
1685
2021
|
|
|
1686
|
-
/* ═══════════ COLOR ROW ═══════════ */
|
|
2022
|
+
/* ═══════════ COLOR ROW & SWATCH ═══════════ */
|
|
1687
2023
|
.lab-color-row {
|
|
1688
2024
|
display: flex;
|
|
1689
2025
|
align-items: center;
|
|
1690
2026
|
gap: var(--trim);
|
|
1691
2027
|
}
|
|
1692
2028
|
.lab-swatch {
|
|
2029
|
+
all: unset;
|
|
1693
2030
|
width: var(--space);
|
|
1694
2031
|
height: var(--space);
|
|
1695
2032
|
border: var(--border);
|
|
1696
2033
|
border-radius: var(--radius);
|
|
1697
2034
|
cursor: pointer;
|
|
1698
|
-
|
|
1699
|
-
background: var(--color-surface);
|
|
2035
|
+
box-shadow: var(--shadow);
|
|
1700
2036
|
flex-shrink: 0;
|
|
2037
|
+
box-sizing: border-box;
|
|
2038
|
+
transition: box-shadow var(--transition-fast), transform var(--transition-fast);
|
|
1701
2039
|
}
|
|
1702
|
-
.lab-swatch
|
|
1703
|
-
|
|
2040
|
+
.lab-swatch:hover {
|
|
2041
|
+
transform: scale(1.05);
|
|
1704
2042
|
}
|
|
1705
|
-
.lab-swatch
|
|
1706
|
-
|
|
1707
|
-
border-radius: var(--radius);
|
|
2043
|
+
.lab-swatch.active {
|
|
2044
|
+
box-shadow: 0 0 0 2px var(--color-accent);
|
|
1708
2045
|
}
|
|
1709
2046
|
.lab-color-row .lab-text-input {
|
|
1710
2047
|
flex: 1;
|
|
1711
2048
|
min-width: 0;
|
|
1712
2049
|
}
|
|
1713
2050
|
|
|
2051
|
+
/* ═══════════ OKLCH COLOR PICKER ═══════════ */
|
|
2052
|
+
.lab-picker-panel {
|
|
2053
|
+
margin-top: var(--space-d2);
|
|
2054
|
+
margin-bottom: var(--space-d2);
|
|
2055
|
+
padding: var(--space-d3);
|
|
2056
|
+
background: var(--color-surface-sunken);
|
|
2057
|
+
border: var(--border);
|
|
2058
|
+
border-radius: var(--radius);
|
|
2059
|
+
box-shadow: var(--shadow-inset);
|
|
2060
|
+
display: flex;
|
|
2061
|
+
flex-direction: column;
|
|
2062
|
+
gap: var(--space-d3);
|
|
2063
|
+
}
|
|
2064
|
+
|
|
2065
|
+
.lab-picker-header {
|
|
2066
|
+
display: flex;
|
|
2067
|
+
align-items: center;
|
|
2068
|
+
justify-content: space-between;
|
|
2069
|
+
gap: var(--space-d4);
|
|
2070
|
+
}
|
|
2071
|
+
|
|
2072
|
+
.lab-picker-title {
|
|
2073
|
+
display: flex;
|
|
2074
|
+
align-items: baseline;
|
|
2075
|
+
gap: var(--space-d4);
|
|
2076
|
+
}
|
|
2077
|
+
|
|
2078
|
+
.lab-picker-role {
|
|
2079
|
+
font-size: var(--scale-d4);
|
|
2080
|
+
font-weight: 600;
|
|
2081
|
+
text-transform: uppercase;
|
|
2082
|
+
color: var(--color-muted);
|
|
2083
|
+
}
|
|
2084
|
+
|
|
2085
|
+
.lab-picker-hex {
|
|
2086
|
+
font-size: var(--scale-d4);
|
|
2087
|
+
color: var(--color-foreground);
|
|
2088
|
+
font-family: var(--font-monospace);
|
|
2089
|
+
}
|
|
2090
|
+
|
|
2091
|
+
.lab-picker-actions {
|
|
2092
|
+
display: flex;
|
|
2093
|
+
align-items: center;
|
|
2094
|
+
gap: var(--space-d6);
|
|
2095
|
+
}
|
|
2096
|
+
|
|
2097
|
+
.lab-plane-wrapper {
|
|
2098
|
+
position: relative;
|
|
2099
|
+
width: 100%;
|
|
2100
|
+
height: 110px;
|
|
2101
|
+
border-radius: var(--radius-sm);
|
|
2102
|
+
overflow: hidden;
|
|
2103
|
+
cursor: crosshair;
|
|
2104
|
+
user-select: none;
|
|
2105
|
+
touch-action: none;
|
|
2106
|
+
box-shadow: var(--shadow-inset);
|
|
2107
|
+
border: var(--border);
|
|
2108
|
+
}
|
|
2109
|
+
|
|
2110
|
+
.lab-plane-canvas {
|
|
2111
|
+
width: 100%;
|
|
2112
|
+
height: 100%;
|
|
2113
|
+
display: block;
|
|
2114
|
+
}
|
|
2115
|
+
|
|
2116
|
+
.lab-plane-thumb {
|
|
2117
|
+
position: absolute;
|
|
2118
|
+
width: var(--space-d2);
|
|
2119
|
+
height: var(--space-d2);
|
|
2120
|
+
border-radius: 50%;
|
|
2121
|
+
border: 2px solid white;
|
|
2122
|
+
box-shadow: 0 0 2px black;
|
|
2123
|
+
transform: translate(-50%, -50%);
|
|
2124
|
+
pointer-events: none;
|
|
2125
|
+
}
|
|
2126
|
+
|
|
2127
|
+
.lab-picker-sliders {
|
|
2128
|
+
display: flex;
|
|
2129
|
+
flex-direction: column;
|
|
2130
|
+
gap: var(--space-d4);
|
|
2131
|
+
}
|
|
2132
|
+
|
|
2133
|
+
.lab-picker-slider-row {
|
|
2134
|
+
display: grid;
|
|
2135
|
+
grid-template-columns: var(--space-d2) 1fr var(--space-2);
|
|
2136
|
+
align-items: center;
|
|
2137
|
+
gap: var(--space-d4);
|
|
2138
|
+
}
|
|
2139
|
+
|
|
2140
|
+
.lab-slider-label {
|
|
2141
|
+
font-size: var(--scale-d4);
|
|
2142
|
+
font-weight: 600;
|
|
2143
|
+
color: var(--color-muted);
|
|
2144
|
+
text-align: center;
|
|
2145
|
+
}
|
|
2146
|
+
|
|
2147
|
+
.lab-slider-val {
|
|
2148
|
+
font-size: var(--scale-d4);
|
|
2149
|
+
color: var(--color-foreground);
|
|
2150
|
+
text-align: right;
|
|
2151
|
+
font-family: var(--font-monospace);
|
|
2152
|
+
}
|
|
2153
|
+
|
|
2154
|
+
.lab-range-l,
|
|
2155
|
+
.lab-range-c,
|
|
2156
|
+
.lab-range-h {
|
|
2157
|
+
appearance: none;
|
|
2158
|
+
-webkit-appearance: none;
|
|
2159
|
+
width: 100%;
|
|
2160
|
+
height: var(--space-d4);
|
|
2161
|
+
border-radius: var(--radius-sm);
|
|
2162
|
+
outline: none;
|
|
2163
|
+
background: var(--slider-track, var(--color-surface));
|
|
2164
|
+
box-shadow: var(--shadow-inset);
|
|
2165
|
+
cursor: pointer;
|
|
2166
|
+
}
|
|
2167
|
+
|
|
2168
|
+
.lab-range-h {
|
|
2169
|
+
background: linear-gradient(
|
|
2170
|
+
to right,
|
|
2171
|
+
oklch(0.7 0.18 0),
|
|
2172
|
+
oklch(0.7 0.18 60),
|
|
2173
|
+
oklch(0.7 0.18 120),
|
|
2174
|
+
oklch(0.7 0.18 180),
|
|
2175
|
+
oklch(0.7 0.18 240),
|
|
2176
|
+
oklch(0.7 0.18 300),
|
|
2177
|
+
oklch(0.7 0.18 360)
|
|
2178
|
+
);
|
|
2179
|
+
}
|
|
2180
|
+
|
|
2181
|
+
.lab-range::-webkit-slider-thumb {
|
|
2182
|
+
appearance: none;
|
|
2183
|
+
-webkit-appearance: none;
|
|
2184
|
+
width: var(--space-d2);
|
|
2185
|
+
height: var(--space-d2);
|
|
2186
|
+
border-radius: 50%;
|
|
2187
|
+
background: var(--color-surface);
|
|
2188
|
+
border: 2px solid var(--color-foreground);
|
|
2189
|
+
box-shadow: var(--shadow);
|
|
2190
|
+
cursor: pointer;
|
|
2191
|
+
}
|
|
2192
|
+
|
|
2193
|
+
.lab-picker-presets {
|
|
2194
|
+
display: flex;
|
|
2195
|
+
flex-wrap: wrap;
|
|
2196
|
+
gap: var(--space-d4);
|
|
2197
|
+
padding-top: var(--space-d6);
|
|
2198
|
+
border-top: var(--border);
|
|
2199
|
+
}
|
|
2200
|
+
|
|
2201
|
+
.lab-preset-dot {
|
|
2202
|
+
all: unset;
|
|
2203
|
+
width: var(--space-d2);
|
|
2204
|
+
height: var(--space-d2);
|
|
2205
|
+
border-radius: var(--radius-sm);
|
|
2206
|
+
border: var(--border);
|
|
2207
|
+
cursor: pointer;
|
|
2208
|
+
box-shadow: var(--shadow);
|
|
2209
|
+
transition: transform var(--transition-fast);
|
|
2210
|
+
box-sizing: border-box;
|
|
2211
|
+
}
|
|
2212
|
+
|
|
2213
|
+
.lab-preset-dot:hover {
|
|
2214
|
+
transform: scale(1.15);
|
|
2215
|
+
}
|
|
2216
|
+
|
|
1714
2217
|
/* ═══════════ SELECT ═══════════ */
|
|
1715
2218
|
.lab-select {
|
|
1716
2219
|
font-size: var(--scale-d4) !important;
|