@stnd/lab 1.4.2 → 1.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +11 -0
- package/Lab.svelte +653 -167
- 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,50 @@
|
|
|
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 = parseFloat(readVar("--font-size", target)) || 1;
|
|
363
|
+
lineHeight = parseFloat(readVar("--line-height", target)) || 1.414;
|
|
364
|
+
ratio = parseFloat(readVar("--optical-ratio", target)) || 1.414;
|
|
365
|
+
|
|
366
|
+
const rawGap = readVar("--page-padding", target);
|
|
321
367
|
const pxSpace = resolvePx("var(--space)");
|
|
322
368
|
|
|
323
369
|
// Calculate body gap multiplier
|
|
@@ -329,13 +375,16 @@
|
|
|
329
375
|
rhythmGap = pxSpace ? pxRhythm / pxSpace : 0.5;
|
|
330
376
|
|
|
331
377
|
// Calculate radius multiplier
|
|
332
|
-
const rawRadius = readVar("--radius");
|
|
378
|
+
const rawRadius = readVar("--radius", target);
|
|
333
379
|
const pxRadius = resolvePx(rawRadius);
|
|
334
380
|
radius = pxSpace ? pxRadius / pxSpace : 0;
|
|
335
381
|
|
|
336
382
|
debug = document.documentElement.classList.contains("stnd-debug");
|
|
337
383
|
eink = document.documentElement.classList.contains("standard-eink");
|
|
338
|
-
theme =
|
|
384
|
+
theme =
|
|
385
|
+
document.documentElement.getAttribute("data-theme") ||
|
|
386
|
+
document.body?.getAttribute("data-theme") ||
|
|
387
|
+
"";
|
|
339
388
|
viewportW = window.innerWidth;
|
|
340
389
|
}
|
|
341
390
|
|
|
@@ -681,21 +730,35 @@
|
|
|
681
730
|
|
|
682
731
|
function onGlobalClick(e) {
|
|
683
732
|
if (view !== "open" || pinned) return;
|
|
684
|
-
if (sidebarEl &&
|
|
733
|
+
if (sidebarEl && e.composedPath().includes(sidebarEl)) return;
|
|
685
734
|
if (
|
|
686
|
-
e.target.closest(".lab-handler") ||
|
|
687
|
-
e.target.closest(".lab-handler-top")
|
|
735
|
+
e.target.closest?.(".lab-handler") ||
|
|
736
|
+
e.target.closest?.(".lab-handler-top")
|
|
688
737
|
)
|
|
689
738
|
return;
|
|
690
739
|
view = "dormant";
|
|
691
740
|
}
|
|
692
741
|
|
|
742
|
+
const domObserver = new MutationObserver(() => {
|
|
743
|
+
requestAnimationFrame(syncFromDOM);
|
|
744
|
+
});
|
|
745
|
+
domObserver.observe(document.documentElement, {
|
|
746
|
+
attributes: true,
|
|
747
|
+
attributeFilter: [
|
|
748
|
+
"data-theme",
|
|
749
|
+
"data-color-mode",
|
|
750
|
+
"class",
|
|
751
|
+
"data-lab-scheme",
|
|
752
|
+
],
|
|
753
|
+
});
|
|
754
|
+
|
|
693
755
|
document.addEventListener("keydown", onKey);
|
|
694
756
|
document.addEventListener("click", onGlobalClick);
|
|
695
757
|
window.addEventListener("resize", onResize);
|
|
696
758
|
window.addEventListener("scroll", onScroll, { passive: true });
|
|
697
759
|
|
|
698
760
|
return () => {
|
|
761
|
+
domObserver.disconnect();
|
|
699
762
|
document.removeEventListener("keydown", onKey);
|
|
700
763
|
document.removeEventListener("click", onGlobalClick);
|
|
701
764
|
document.removeEventListener("astro:after-swap", restoreLabState);
|
|
@@ -741,6 +804,37 @@
|
|
|
741
804
|
}
|
|
742
805
|
});
|
|
743
806
|
|
|
807
|
+
// Render 2D color area on canvas whenever active picker, hue, or picker open state changes
|
|
808
|
+
$effect(() => {
|
|
809
|
+
if (!activePicker || !pickerCanvasEl) return;
|
|
810
|
+
const canvas = pickerCanvasEl;
|
|
811
|
+
const ctx = canvas.getContext("2d");
|
|
812
|
+
if (!ctx) return;
|
|
813
|
+
|
|
814
|
+
const width = canvas.width;
|
|
815
|
+
const height = canvas.height;
|
|
816
|
+
const imgData = ctx.createImageData(width, height);
|
|
817
|
+
const data = imgData.data;
|
|
818
|
+
|
|
819
|
+
const cur = activePicker === "bg" ? bgOklch : fgOklch;
|
|
820
|
+
const h = cur.h;
|
|
821
|
+
|
|
822
|
+
// Render 2D grid: X = Chroma (0 to MAX_CHROMA), Y = Lightness (1 at top to 0 at bottom)
|
|
823
|
+
for (let y = 0; y < height; y++) {
|
|
824
|
+
const L = 1 - y / (height - 1);
|
|
825
|
+
for (let x = 0; x < width; x++) {
|
|
826
|
+
const C = (x / (width - 1)) * MAX_CHROMA;
|
|
827
|
+
const rgb = oklchToRgb(L, C, h);
|
|
828
|
+
const idx = (y * width + x) * 4;
|
|
829
|
+
data[idx] = rgb.r;
|
|
830
|
+
data[idx + 1] = rgb.g;
|
|
831
|
+
data[idx + 2] = rgb.b;
|
|
832
|
+
data[idx + 3] = rgb.inGamut ? 255 : 180;
|
|
833
|
+
}
|
|
834
|
+
}
|
|
835
|
+
ctx.putImageData(imgData, 0, 0);
|
|
836
|
+
});
|
|
837
|
+
|
|
744
838
|
// =========================================================================
|
|
745
839
|
// HANDLERS — Theme
|
|
746
840
|
// =========================================================================
|
|
@@ -786,50 +880,115 @@
|
|
|
786
880
|
}
|
|
787
881
|
|
|
788
882
|
// =========================================================================
|
|
789
|
-
// HANDLERS —
|
|
883
|
+
// HANDLERS — Color Picker
|
|
790
884
|
// =========================================================================
|
|
791
|
-
function
|
|
792
|
-
|
|
885
|
+
function toggleColorPicker(role) {
|
|
886
|
+
activePicker = activePicker === role ? null : role;
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
function updateColorFromOklch(role, newOklch) {
|
|
890
|
+
const L = clamp(newOklch.L, 0, 1);
|
|
891
|
+
const C = clamp(newOklch.C, 0, MAX_CHROMA);
|
|
892
|
+
const h = ((newOklch.h % 360) + 360) % 360;
|
|
893
|
+
|
|
894
|
+
const rgb = oklchToRgb(L, C, h);
|
|
895
|
+
const hex = rgbToHex(rgb);
|
|
896
|
+
const oklchStr = formatOklch(L, C, h);
|
|
897
|
+
const formatted = pickerFormat === "hex" ? hex : oklchStr;
|
|
898
|
+
|
|
793
899
|
const token = role === "bg" ? bgToken : fgToken;
|
|
900
|
+
|
|
794
901
|
if (role === "bg") {
|
|
795
|
-
|
|
902
|
+
bgOklch = { L, C, h };
|
|
903
|
+
seedBg = formatted;
|
|
796
904
|
seedBgHex = hex;
|
|
797
905
|
} else {
|
|
798
|
-
|
|
906
|
+
fgOklch = { L, C, h };
|
|
907
|
+
seedFg = formatted;
|
|
799
908
|
seedFgHex = hex;
|
|
800
909
|
}
|
|
801
|
-
|
|
910
|
+
|
|
911
|
+
writeVar(token, formatted);
|
|
802
912
|
}
|
|
803
913
|
|
|
804
914
|
function onColorText(role, e) {
|
|
805
915
|
const val = e.target.value;
|
|
806
916
|
const token = role === "bg" ? bgToken : fgToken;
|
|
917
|
+
const target = getThemeTargetElement();
|
|
918
|
+
const parsed = parseCssColor(val, target);
|
|
919
|
+
|
|
807
920
|
if (role === "bg") {
|
|
808
921
|
seedBg = val;
|
|
809
|
-
seedBgHex =
|
|
922
|
+
seedBgHex = parsed.hex;
|
|
923
|
+
bgOklch = { L: parsed.L, C: parsed.C, h: parsed.h };
|
|
810
924
|
} else {
|
|
811
925
|
seedFg = val;
|
|
812
|
-
seedFgHex =
|
|
926
|
+
seedFgHex = parsed.hex;
|
|
927
|
+
fgOklch = { L: parsed.L, C: parsed.C, h: parsed.h };
|
|
813
928
|
}
|
|
814
929
|
writeVar(token, val);
|
|
815
930
|
}
|
|
816
931
|
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
932
|
+
function onSliderInput(role, channel, e) {
|
|
933
|
+
const val = parseFloat(e.target.value);
|
|
934
|
+
const cur = role === "bg" ? { ...bgOklch } : { ...fgOklch };
|
|
935
|
+
cur[channel] = val;
|
|
936
|
+
updateColorFromOklch(role, cur);
|
|
937
|
+
}
|
|
938
|
+
|
|
939
|
+
function handlePlanePointer(e) {
|
|
940
|
+
if (!pickerCanvasEl || !activePicker) return;
|
|
941
|
+
const rect = pickerCanvasEl.getBoundingClientRect();
|
|
942
|
+
const x = clamp(e.clientX - rect.left, 0, rect.width);
|
|
943
|
+
const y = clamp(e.clientY - rect.top, 0, rect.height);
|
|
944
|
+
|
|
945
|
+
const C = (x / rect.width) * MAX_CHROMA;
|
|
946
|
+
const L = 1 - y / rect.height;
|
|
947
|
+
|
|
948
|
+
const cur = activePicker === "bg" ? bgOklch : fgOklch;
|
|
949
|
+
updateColorFromOklch(activePicker, { L, C, h: cur.h });
|
|
950
|
+
}
|
|
951
|
+
|
|
952
|
+
function onPlanePointerDown(e) {
|
|
953
|
+
isDraggingPlane = true;
|
|
954
|
+
e.currentTarget.setPointerCapture(e.pointerId);
|
|
955
|
+
handlePlanePointer(e);
|
|
956
|
+
}
|
|
957
|
+
|
|
958
|
+
function onPlanePointerMove(e) {
|
|
959
|
+
if (!isDraggingPlane) return;
|
|
960
|
+
handlePlanePointer(e);
|
|
961
|
+
}
|
|
962
|
+
|
|
963
|
+
function onPlanePointerUp(e) {
|
|
964
|
+
if (isDraggingPlane) {
|
|
965
|
+
isDraggingPlane = false;
|
|
966
|
+
try {
|
|
967
|
+
e.currentTarget.releasePointerCapture(e.pointerId);
|
|
968
|
+
} catch {
|
|
969
|
+
/* noop */
|
|
970
|
+
}
|
|
971
|
+
}
|
|
972
|
+
}
|
|
973
|
+
|
|
974
|
+
async function pickWithEyeDropper(role) {
|
|
975
|
+
if (!hasEyeDropper) return;
|
|
976
|
+
try {
|
|
977
|
+
const eyeDropper = new window.EyeDropper();
|
|
978
|
+
const result = await eyeDropper.open();
|
|
979
|
+
if (result?.sRGBHex) {
|
|
980
|
+
const parsed = parseCssColor(result.sRGBHex);
|
|
981
|
+
updateColorFromOklch(role, parsed);
|
|
982
|
+
}
|
|
983
|
+
} catch {
|
|
984
|
+
/* User dismissed eyedropper */
|
|
985
|
+
}
|
|
986
|
+
}
|
|
987
|
+
|
|
988
|
+
function applyPreset(role, val) {
|
|
989
|
+
const target = getThemeTargetElement();
|
|
990
|
+
const parsed = parseCssColor(val, target);
|
|
991
|
+
updateColorFromOklch(role, parsed);
|
|
833
992
|
}
|
|
834
993
|
|
|
835
994
|
// =========================================================================
|
|
@@ -1093,49 +1252,207 @@
|
|
|
1093
1252
|
{#if sectionsOpen.colors}
|
|
1094
1253
|
<div class="lab-section-body" transition:slide>
|
|
1095
1254
|
<div class="lab-field">
|
|
1096
|
-
<label class="lab-field-label" for="lab-bg-text">Background</label
|
|
1097
|
-
>
|
|
1255
|
+
<label class="lab-field-label" for="lab-bg-text">Background</label>
|
|
1098
1256
|
<div class="lab-color-row">
|
|
1099
|
-
<
|
|
1100
|
-
type="
|
|
1257
|
+
<button
|
|
1258
|
+
type="button"
|
|
1101
1259
|
class="lab-swatch"
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1260
|
+
class:active={activePicker === "bg"}
|
|
1261
|
+
style="background-color: {seedBgHex};"
|
|
1262
|
+
title="Toggle OKLCH background picker"
|
|
1263
|
+
onclick={() => toggleColorPicker("bg")}
|
|
1264
|
+
aria-label="Pick background color"
|
|
1265
|
+
></button>
|
|
1106
1266
|
<input
|
|
1107
1267
|
id="lab-bg-text"
|
|
1108
1268
|
type="text"
|
|
1109
1269
|
class="lab-text-input"
|
|
1110
1270
|
value={seedBg}
|
|
1111
1271
|
spellcheck="false"
|
|
1112
|
-
placeholder="e.g.
|
|
1272
|
+
placeholder="e.g. oklch(0.23 0.02 67)"
|
|
1113
1273
|
oninput={(e) => onColorText("bg", e)}
|
|
1114
1274
|
/>
|
|
1115
1275
|
</div>
|
|
1116
1276
|
</div>
|
|
1277
|
+
|
|
1117
1278
|
<div class="lab-field">
|
|
1118
|
-
<label class="lab-field-label" for="lab-fg-text">Foreground</label
|
|
1119
|
-
>
|
|
1279
|
+
<label class="lab-field-label" for="lab-fg-text">Foreground</label>
|
|
1120
1280
|
<div class="lab-color-row">
|
|
1121
|
-
<
|
|
1122
|
-
type="
|
|
1281
|
+
<button
|
|
1282
|
+
type="button"
|
|
1123
1283
|
class="lab-swatch"
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1284
|
+
class:active={activePicker === "fg"}
|
|
1285
|
+
style="background-color: {seedFgHex};"
|
|
1286
|
+
title="Toggle OKLCH foreground picker"
|
|
1287
|
+
onclick={() => toggleColorPicker("fg")}
|
|
1288
|
+
aria-label="Pick foreground color"
|
|
1289
|
+
></button>
|
|
1128
1290
|
<input
|
|
1129
1291
|
id="lab-fg-text"
|
|
1130
1292
|
type="text"
|
|
1131
1293
|
class="lab-text-input"
|
|
1132
1294
|
value={seedFg}
|
|
1133
1295
|
spellcheck="false"
|
|
1134
|
-
placeholder="e.g. #
|
|
1296
|
+
placeholder="e.g. #262626"
|
|
1135
1297
|
oninput={(e) => onColorText("fg", e)}
|
|
1136
1298
|
/>
|
|
1137
1299
|
</div>
|
|
1138
1300
|
</div>
|
|
1301
|
+
|
|
1302
|
+
<!-- Custom OKLCH Color Picker Panel -->
|
|
1303
|
+
{#if activePicker}
|
|
1304
|
+
{@const curOklch = activePicker === "bg" ? bgOklch : fgOklch}
|
|
1305
|
+
{@const curHex = activePicker === "bg" ? seedBgHex : seedFgHex}
|
|
1306
|
+
{@const presets = activePicker === "bg" ? BG_PRESETS : FG_PRESETS}
|
|
1307
|
+
<div class="lab-picker-panel" transition:slide>
|
|
1308
|
+
<header class="lab-picker-header">
|
|
1309
|
+
<div class="lab-picker-title">
|
|
1310
|
+
<span class="lab-picker-role"
|
|
1311
|
+
>{activePicker === "bg" ? "Background" : "Foreground"}</span
|
|
1312
|
+
>
|
|
1313
|
+
<span class="lab-picker-hex">{curHex}</span>
|
|
1314
|
+
</div>
|
|
1315
|
+
<div class="lab-picker-actions">
|
|
1316
|
+
<button
|
|
1317
|
+
type="button"
|
|
1318
|
+
class="smaller ghost"
|
|
1319
|
+
title="Toggle output format (OKLCH or Hex)"
|
|
1320
|
+
onclick={() => {
|
|
1321
|
+
pickerFormat =
|
|
1322
|
+
pickerFormat === "oklch" ? "hex" : "oklch";
|
|
1323
|
+
updateColorFromOklch(activePicker, curOklch);
|
|
1324
|
+
}}
|
|
1325
|
+
>
|
|
1326
|
+
{pickerFormat.toUpperCase()}
|
|
1327
|
+
</button>
|
|
1328
|
+
{#if hasEyeDropper}
|
|
1329
|
+
<button
|
|
1330
|
+
type="button"
|
|
1331
|
+
class="smaller icon ghost"
|
|
1332
|
+
title="Pick color from screen"
|
|
1333
|
+
onclick={() => pickWithEyeDropper(activePicker)}
|
|
1334
|
+
>
|
|
1335
|
+
<svg
|
|
1336
|
+
width="1em"
|
|
1337
|
+
height="1em"
|
|
1338
|
+
viewBox="0 0 24 24"
|
|
1339
|
+
fill="none"
|
|
1340
|
+
stroke="currentColor"
|
|
1341
|
+
stroke-width="2"
|
|
1342
|
+
stroke-linecap="round"
|
|
1343
|
+
stroke-linejoin="round"
|
|
1344
|
+
>
|
|
1345
|
+
<path
|
|
1346
|
+
d="m14 7 3 3m-8.5 8.5L4 20l1.5-4.5L14 7l3 3L8.5 18.5Z"
|
|
1347
|
+
></path>
|
|
1348
|
+
</svg>
|
|
1349
|
+
</button>
|
|
1350
|
+
{/if}
|
|
1351
|
+
<button
|
|
1352
|
+
type="button"
|
|
1353
|
+
class="smaller icon ghost"
|
|
1354
|
+
title="Close color picker"
|
|
1355
|
+
onclick={() => (activePicker = null)}
|
|
1356
|
+
>
|
|
1357
|
+
✕
|
|
1358
|
+
</button>
|
|
1359
|
+
</div>
|
|
1360
|
+
</header>
|
|
1361
|
+
|
|
1362
|
+
<!-- 2D Lightness x Chroma Color Plane -->
|
|
1363
|
+
<div
|
|
1364
|
+
class="lab-plane-wrapper"
|
|
1365
|
+
role="slider"
|
|
1366
|
+
tabindex="0"
|
|
1367
|
+
aria-label="Lightness and Chroma 2D plane"
|
|
1368
|
+
aria-valuemin="0"
|
|
1369
|
+
aria-valuemax="100"
|
|
1370
|
+
aria-valuenow={Math.round(curOklch.L * 100)}
|
|
1371
|
+
onpointerdown={onPlanePointerDown}
|
|
1372
|
+
onpointermove={onPlanePointerMove}
|
|
1373
|
+
onpointerup={onPlanePointerUp}
|
|
1374
|
+
>
|
|
1375
|
+
<canvas
|
|
1376
|
+
bind:this={pickerCanvasEl}
|
|
1377
|
+
width="240"
|
|
1378
|
+
height="110"
|
|
1379
|
+
class="lab-plane-canvas"
|
|
1380
|
+
></canvas>
|
|
1381
|
+
<div
|
|
1382
|
+
class="lab-plane-thumb"
|
|
1383
|
+
style="left: {(curOklch.C / MAX_CHROMA) *
|
|
1384
|
+
100}%; top: {(1 - curOklch.L) *
|
|
1385
|
+
100}%; background-color: {curHex};"
|
|
1386
|
+
></div>
|
|
1387
|
+
</div>
|
|
1388
|
+
|
|
1389
|
+
<!-- Perceptual OKLCH Sliders -->
|
|
1390
|
+
<div class="lab-picker-sliders">
|
|
1391
|
+
<!-- Lightness Slider -->
|
|
1392
|
+
<div class="lab-picker-slider-row">
|
|
1393
|
+
<span class="lab-slider-label">L</span>
|
|
1394
|
+
<input
|
|
1395
|
+
type="range"
|
|
1396
|
+
class="lab-range lab-range-l"
|
|
1397
|
+
min="0"
|
|
1398
|
+
max="1"
|
|
1399
|
+
step="0.005"
|
|
1400
|
+
value={curOklch.L}
|
|
1401
|
+
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}));"
|
|
1402
|
+
oninput={(e) => onSliderInput(activePicker, "L", e)}
|
|
1403
|
+
/>
|
|
1404
|
+
<span class="lab-slider-val"
|
|
1405
|
+
>{Math.round(curOklch.L * 100)}%</span
|
|
1406
|
+
>
|
|
1407
|
+
</div>
|
|
1408
|
+
|
|
1409
|
+
<!-- Chroma Slider -->
|
|
1410
|
+
<div class="lab-picker-slider-row">
|
|
1411
|
+
<span class="lab-slider-label">C</span>
|
|
1412
|
+
<input
|
|
1413
|
+
type="range"
|
|
1414
|
+
class="lab-range lab-range-c"
|
|
1415
|
+
min="0"
|
|
1416
|
+
max={MAX_CHROMA}
|
|
1417
|
+
step="0.002"
|
|
1418
|
+
value={curOklch.C}
|
|
1419
|
+
style="--slider-track: linear-gradient(to right, oklch({curOklch.L} 0 {curOklch.h}), oklch({curOklch.L} {MAX_CHROMA *
|
|
1420
|
+
0.5} {curOklch.h}), oklch({curOklch.L} {MAX_CHROMA} {curOklch.h}));"
|
|
1421
|
+
oninput={(e) => onSliderInput(activePicker, "C", e)}
|
|
1422
|
+
/>
|
|
1423
|
+
<span class="lab-slider-val">{curOklch.C.toFixed(3)}</span>
|
|
1424
|
+
</div>
|
|
1425
|
+
|
|
1426
|
+
<!-- Hue Slider -->
|
|
1427
|
+
<div class="lab-picker-slider-row">
|
|
1428
|
+
<span class="lab-slider-label">H</span>
|
|
1429
|
+
<input
|
|
1430
|
+
type="range"
|
|
1431
|
+
class="lab-range lab-range-h"
|
|
1432
|
+
min="0"
|
|
1433
|
+
max="360"
|
|
1434
|
+
step="1"
|
|
1435
|
+
value={curOklch.h}
|
|
1436
|
+
oninput={(e) => onSliderInput(activePicker, "h", e)}
|
|
1437
|
+
/>
|
|
1438
|
+
<span class="lab-slider-val">{Math.round(curOklch.h)}°</span>
|
|
1439
|
+
</div>
|
|
1440
|
+
</div>
|
|
1441
|
+
|
|
1442
|
+
<!-- Palette Presets -->
|
|
1443
|
+
<div class="lab-picker-presets">
|
|
1444
|
+
{#each presets as p}
|
|
1445
|
+
<button
|
|
1446
|
+
type="button"
|
|
1447
|
+
class="lab-preset-dot"
|
|
1448
|
+
title={p.label}
|
|
1449
|
+
style="background-color: {p.value};"
|
|
1450
|
+
onclick={() => applyPreset(activePicker, p.value)}
|
|
1451
|
+
></button>
|
|
1452
|
+
{/each}
|
|
1453
|
+
</div>
|
|
1454
|
+
</div>
|
|
1455
|
+
{/if}
|
|
1139
1456
|
</div>
|
|
1140
1457
|
{/if}
|
|
1141
1458
|
</section>
|
|
@@ -1168,13 +1485,13 @@
|
|
|
1168
1485
|
emptyText="No fonts found."
|
|
1169
1486
|
allowCustom={true}
|
|
1170
1487
|
preview={true}
|
|
1171
|
-
previewDebounce={
|
|
1488
|
+
previewDebounce={60}
|
|
1172
1489
|
loadFont={(name) => loadGoogleFont(name)}
|
|
1173
1490
|
class="lab-font-combobox"
|
|
1174
1491
|
aria-controls={inputId}
|
|
1175
|
-
onpreview={(e) =>
|
|
1492
|
+
onpreview={(e) =>
|
|
1493
|
+
previewFont(token, e.detail?.value, currentValue)}
|
|
1176
1494
|
onchange={(e) => {
|
|
1177
|
-
// Combobox dispatches change with detail.value (base font name)
|
|
1178
1495
|
selectFont(token, e.detail?.value);
|
|
1179
1496
|
}}
|
|
1180
1497
|
/>
|
|
@@ -1546,14 +1863,16 @@
|
|
|
1546
1863
|
}
|
|
1547
1864
|
}
|
|
1548
1865
|
|
|
1549
|
-
/* Reset children to Lab font */
|
|
1866
|
+
/* Reset children to Lab font, allowing explicit font-family previews to win */
|
|
1550
1867
|
.lab-sidebar * {
|
|
1551
|
-
font-family: var(--font-monospace) !important;
|
|
1552
1868
|
box-sizing: border-box;
|
|
1553
1869
|
/* Disable opacity transitions on child elements to prevent fading when opening the lab.
|
|
1554
1870
|
Other property transitions for children remain allowed outside of opacity. */
|
|
1555
1871
|
transition: opacity 0s !important;
|
|
1556
1872
|
}
|
|
1873
|
+
.lab-sidebar *:not([style*="font-family"]) {
|
|
1874
|
+
font-family: var(--font-monospace);
|
|
1875
|
+
}
|
|
1557
1876
|
|
|
1558
1877
|
/* ═══════════ HEADER ═══════════ */
|
|
1559
1878
|
.lab-header {
|
|
@@ -1683,34 +2002,201 @@
|
|
|
1683
2002
|
margin: 0 !important;
|
|
1684
2003
|
}
|
|
1685
2004
|
|
|
1686
|
-
/* ═══════════ COLOR ROW ═══════════ */
|
|
2005
|
+
/* ═══════════ COLOR ROW & SWATCH ═══════════ */
|
|
1687
2006
|
.lab-color-row {
|
|
1688
2007
|
display: flex;
|
|
1689
2008
|
align-items: center;
|
|
1690
2009
|
gap: var(--trim);
|
|
1691
2010
|
}
|
|
1692
2011
|
.lab-swatch {
|
|
2012
|
+
all: unset;
|
|
1693
2013
|
width: var(--space);
|
|
1694
2014
|
height: var(--space);
|
|
1695
2015
|
border: var(--border);
|
|
1696
2016
|
border-radius: var(--radius);
|
|
1697
2017
|
cursor: pointer;
|
|
1698
|
-
|
|
1699
|
-
background: var(--color-surface);
|
|
2018
|
+
box-shadow: var(--shadow);
|
|
1700
2019
|
flex-shrink: 0;
|
|
2020
|
+
box-sizing: border-box;
|
|
2021
|
+
transition: box-shadow var(--transition-fast), transform var(--transition-fast);
|
|
1701
2022
|
}
|
|
1702
|
-
.lab-swatch
|
|
1703
|
-
|
|
2023
|
+
.lab-swatch:hover {
|
|
2024
|
+
transform: scale(1.05);
|
|
1704
2025
|
}
|
|
1705
|
-
.lab-swatch
|
|
1706
|
-
|
|
1707
|
-
border-radius: var(--radius);
|
|
2026
|
+
.lab-swatch.active {
|
|
2027
|
+
box-shadow: 0 0 0 2px var(--color-accent);
|
|
1708
2028
|
}
|
|
1709
2029
|
.lab-color-row .lab-text-input {
|
|
1710
2030
|
flex: 1;
|
|
1711
2031
|
min-width: 0;
|
|
1712
2032
|
}
|
|
1713
2033
|
|
|
2034
|
+
/* ═══════════ OKLCH COLOR PICKER ═══════════ */
|
|
2035
|
+
.lab-picker-panel {
|
|
2036
|
+
margin-top: var(--space-d2);
|
|
2037
|
+
margin-bottom: var(--space-d2);
|
|
2038
|
+
padding: var(--space-d3);
|
|
2039
|
+
background: var(--color-surface-sunken);
|
|
2040
|
+
border: var(--border);
|
|
2041
|
+
border-radius: var(--radius);
|
|
2042
|
+
box-shadow: var(--shadow-inset);
|
|
2043
|
+
display: flex;
|
|
2044
|
+
flex-direction: column;
|
|
2045
|
+
gap: var(--space-d3);
|
|
2046
|
+
}
|
|
2047
|
+
|
|
2048
|
+
.lab-picker-header {
|
|
2049
|
+
display: flex;
|
|
2050
|
+
align-items: center;
|
|
2051
|
+
justify-content: space-between;
|
|
2052
|
+
gap: var(--space-d4);
|
|
2053
|
+
}
|
|
2054
|
+
|
|
2055
|
+
.lab-picker-title {
|
|
2056
|
+
display: flex;
|
|
2057
|
+
align-items: baseline;
|
|
2058
|
+
gap: var(--space-d4);
|
|
2059
|
+
}
|
|
2060
|
+
|
|
2061
|
+
.lab-picker-role {
|
|
2062
|
+
font-size: var(--scale-d4);
|
|
2063
|
+
font-weight: 600;
|
|
2064
|
+
text-transform: uppercase;
|
|
2065
|
+
color: var(--color-muted);
|
|
2066
|
+
}
|
|
2067
|
+
|
|
2068
|
+
.lab-picker-hex {
|
|
2069
|
+
font-size: var(--scale-d4);
|
|
2070
|
+
color: var(--color-foreground);
|
|
2071
|
+
font-family: var(--font-monospace);
|
|
2072
|
+
}
|
|
2073
|
+
|
|
2074
|
+
.lab-picker-actions {
|
|
2075
|
+
display: flex;
|
|
2076
|
+
align-items: center;
|
|
2077
|
+
gap: var(--space-d6);
|
|
2078
|
+
}
|
|
2079
|
+
|
|
2080
|
+
.lab-plane-wrapper {
|
|
2081
|
+
position: relative;
|
|
2082
|
+
width: 100%;
|
|
2083
|
+
height: 110px;
|
|
2084
|
+
border-radius: var(--radius-sm);
|
|
2085
|
+
overflow: hidden;
|
|
2086
|
+
cursor: crosshair;
|
|
2087
|
+
user-select: none;
|
|
2088
|
+
touch-action: none;
|
|
2089
|
+
box-shadow: var(--shadow-inset);
|
|
2090
|
+
border: var(--border);
|
|
2091
|
+
}
|
|
2092
|
+
|
|
2093
|
+
.lab-plane-canvas {
|
|
2094
|
+
width: 100%;
|
|
2095
|
+
height: 100%;
|
|
2096
|
+
display: block;
|
|
2097
|
+
}
|
|
2098
|
+
|
|
2099
|
+
.lab-plane-thumb {
|
|
2100
|
+
position: absolute;
|
|
2101
|
+
width: var(--space-d2);
|
|
2102
|
+
height: var(--space-d2);
|
|
2103
|
+
border-radius: 50%;
|
|
2104
|
+
border: 2px solid white;
|
|
2105
|
+
box-shadow: 0 0 2px black;
|
|
2106
|
+
transform: translate(-50%, -50%);
|
|
2107
|
+
pointer-events: none;
|
|
2108
|
+
}
|
|
2109
|
+
|
|
2110
|
+
.lab-picker-sliders {
|
|
2111
|
+
display: flex;
|
|
2112
|
+
flex-direction: column;
|
|
2113
|
+
gap: var(--space-d4);
|
|
2114
|
+
}
|
|
2115
|
+
|
|
2116
|
+
.lab-picker-slider-row {
|
|
2117
|
+
display: grid;
|
|
2118
|
+
grid-template-columns: var(--space-d2) 1fr var(--space-2);
|
|
2119
|
+
align-items: center;
|
|
2120
|
+
gap: var(--space-d4);
|
|
2121
|
+
}
|
|
2122
|
+
|
|
2123
|
+
.lab-slider-label {
|
|
2124
|
+
font-size: var(--scale-d4);
|
|
2125
|
+
font-weight: 600;
|
|
2126
|
+
color: var(--color-muted);
|
|
2127
|
+
text-align: center;
|
|
2128
|
+
}
|
|
2129
|
+
|
|
2130
|
+
.lab-slider-val {
|
|
2131
|
+
font-size: var(--scale-d4);
|
|
2132
|
+
color: var(--color-foreground);
|
|
2133
|
+
text-align: right;
|
|
2134
|
+
font-family: var(--font-monospace);
|
|
2135
|
+
}
|
|
2136
|
+
|
|
2137
|
+
.lab-range-l,
|
|
2138
|
+
.lab-range-c,
|
|
2139
|
+
.lab-range-h {
|
|
2140
|
+
appearance: none;
|
|
2141
|
+
-webkit-appearance: none;
|
|
2142
|
+
width: 100%;
|
|
2143
|
+
height: var(--space-d4);
|
|
2144
|
+
border-radius: var(--radius-sm);
|
|
2145
|
+
outline: none;
|
|
2146
|
+
background: var(--slider-track, var(--color-surface));
|
|
2147
|
+
box-shadow: var(--shadow-inset);
|
|
2148
|
+
cursor: pointer;
|
|
2149
|
+
}
|
|
2150
|
+
|
|
2151
|
+
.lab-range-h {
|
|
2152
|
+
background: linear-gradient(
|
|
2153
|
+
to right,
|
|
2154
|
+
oklch(0.7 0.18 0),
|
|
2155
|
+
oklch(0.7 0.18 60),
|
|
2156
|
+
oklch(0.7 0.18 120),
|
|
2157
|
+
oklch(0.7 0.18 180),
|
|
2158
|
+
oklch(0.7 0.18 240),
|
|
2159
|
+
oklch(0.7 0.18 300),
|
|
2160
|
+
oklch(0.7 0.18 360)
|
|
2161
|
+
);
|
|
2162
|
+
}
|
|
2163
|
+
|
|
2164
|
+
.lab-range::-webkit-slider-thumb {
|
|
2165
|
+
appearance: none;
|
|
2166
|
+
-webkit-appearance: none;
|
|
2167
|
+
width: var(--space-d2);
|
|
2168
|
+
height: var(--space-d2);
|
|
2169
|
+
border-radius: 50%;
|
|
2170
|
+
background: var(--color-surface);
|
|
2171
|
+
border: 2px solid var(--color-foreground);
|
|
2172
|
+
box-shadow: var(--shadow);
|
|
2173
|
+
cursor: pointer;
|
|
2174
|
+
}
|
|
2175
|
+
|
|
2176
|
+
.lab-picker-presets {
|
|
2177
|
+
display: flex;
|
|
2178
|
+
flex-wrap: wrap;
|
|
2179
|
+
gap: var(--space-d4);
|
|
2180
|
+
padding-top: var(--space-d6);
|
|
2181
|
+
border-top: var(--border);
|
|
2182
|
+
}
|
|
2183
|
+
|
|
2184
|
+
.lab-preset-dot {
|
|
2185
|
+
all: unset;
|
|
2186
|
+
width: var(--space-d2);
|
|
2187
|
+
height: var(--space-d2);
|
|
2188
|
+
border-radius: var(--radius-sm);
|
|
2189
|
+
border: var(--border);
|
|
2190
|
+
cursor: pointer;
|
|
2191
|
+
box-shadow: var(--shadow);
|
|
2192
|
+
transition: transform var(--transition-fast);
|
|
2193
|
+
box-sizing: border-box;
|
|
2194
|
+
}
|
|
2195
|
+
|
|
2196
|
+
.lab-preset-dot:hover {
|
|
2197
|
+
transform: scale(1.15);
|
|
2198
|
+
}
|
|
2199
|
+
|
|
1714
2200
|
/* ═══════════ SELECT ═══════════ */
|
|
1715
2201
|
.lab-select {
|
|
1716
2202
|
font-size: var(--scale-d4) !important;
|