@protolabsai/ui 0.16.0 → 0.17.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/package.json +3 -1
- package/src/styles/theming.css +2 -1
- package/src/theming.tsx +59 -6
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@protolabsai/ui",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.17.0",
|
|
4
4
|
"publishConfig": {
|
|
5
5
|
"access": "public",
|
|
6
6
|
"registry": "https://registry.npmjs.org/"
|
|
@@ -35,6 +35,7 @@
|
|
|
35
35
|
"@radix-ui/react-popover": "^1.1.16",
|
|
36
36
|
"@types/react": "^19.0.0",
|
|
37
37
|
"@types/react-dom": "^19.0.0",
|
|
38
|
+
"culori": "^4.0.2",
|
|
38
39
|
"@protolabsai/design": "0.5.0"
|
|
39
40
|
},
|
|
40
41
|
"peerDependencies": {
|
|
@@ -44,6 +45,7 @@
|
|
|
44
45
|
"devDependencies": {
|
|
45
46
|
"@storybook/react": "^10.4.1",
|
|
46
47
|
"@storybook/react-vite": "^10.4.1",
|
|
48
|
+
"@types/culori": "^4.0.1",
|
|
47
49
|
"@vitejs/plugin-react": "^4.3.4",
|
|
48
50
|
"react": "^19.0.0",
|
|
49
51
|
"react-dom": "^19.0.0",
|
package/src/styles/theming.css
CHANGED
package/src/theming.tsx
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { useCallback, useEffect, useState } from "react";
|
|
2
|
+
import { converter, formatHex, formatRgb, parse } from "culori";
|
|
2
3
|
import tokens from "@protolabsai/design/tokens.json";
|
|
3
4
|
import { Button } from "./primitives";
|
|
4
5
|
import { cx } from "./internal";
|
|
@@ -54,7 +55,59 @@ const prettyLabel = (v: string) =>
|
|
|
54
55
|
.trim()
|
|
55
56
|
.replace(/\b\w/g, (c) => c.toUpperCase()) || v.replace(/^--pl-/, "");
|
|
56
57
|
|
|
57
|
-
|
|
58
|
+
// ── Color format handling — every color token is pickable; a pick is written
|
|
59
|
+
// back in the token's OWN format (hex / rgb(a) / hsl / oklch / oklab), alpha kept.
|
|
60
|
+
const toRgb = converter("rgb");
|
|
61
|
+
const toHsl = converter("hsl");
|
|
62
|
+
const toOklch = converter("oklch");
|
|
63
|
+
const toOklab = converter("oklab");
|
|
64
|
+
const round = (n: number, d: number) => Math.round(n * 10 ** d) / 10 ** d;
|
|
65
|
+
|
|
66
|
+
/** A token value culori can read as a color (vs a length like radius/border-width). */
|
|
67
|
+
const isColor = (val: string) => !!parse(val);
|
|
68
|
+
|
|
69
|
+
/** Hex for the native `<input type="color">` (drops alpha, sRGB-clamped — display only). */
|
|
70
|
+
function toSwatchHex(val: string): string {
|
|
71
|
+
const c = parse(val);
|
|
72
|
+
return c ? formatHex(c) : "#000000";
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function formatOf(val: string): "hex" | "rgb" | "hsl" | "oklch" | "oklab" | "other" {
|
|
76
|
+
const v = val.trim().toLowerCase();
|
|
77
|
+
if (v.startsWith("#")) return "hex";
|
|
78
|
+
if (v.startsWith("rgb")) return "rgb";
|
|
79
|
+
if (v.startsWith("hsl")) return "hsl";
|
|
80
|
+
if (v.startsWith("oklch")) return "oklch";
|
|
81
|
+
if (v.startsWith("oklab")) return "oklab";
|
|
82
|
+
return "other";
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** Reproject a freshly-picked hex into `original`'s color format, preserving its alpha. */
|
|
86
|
+
function reformat(pickedHex: string, original: string): string {
|
|
87
|
+
const picked = parse(pickedHex);
|
|
88
|
+
if (!picked) return pickedHex;
|
|
89
|
+
const alpha = parse(original)?.alpha;
|
|
90
|
+
const hasA = alpha != null && alpha < 1;
|
|
91
|
+
const aTail = hasA ? ` / ${alpha}` : "";
|
|
92
|
+
switch (formatOf(original)) {
|
|
93
|
+
case "rgb":
|
|
94
|
+
return formatRgb({ ...toRgb(picked), alpha: hasA ? alpha : undefined });
|
|
95
|
+
case "hsl": {
|
|
96
|
+
const h = toHsl(picked);
|
|
97
|
+
return `hsl(${round(h.h ?? 0, 1)} ${round((h.s ?? 0) * 100, 1)}% ${round((h.l ?? 0) * 100, 1)}%${aTail})`;
|
|
98
|
+
}
|
|
99
|
+
case "oklch": {
|
|
100
|
+
const o = toOklch(picked);
|
|
101
|
+
return `oklch(${round(o.l ?? 0, 4)} ${round(o.c ?? 0, 4)} ${round(o.h ?? 0, 2)}${aTail})`;
|
|
102
|
+
}
|
|
103
|
+
case "oklab": {
|
|
104
|
+
const o = toOklab(picked);
|
|
105
|
+
return `oklab(${round(o.l ?? 0, 4)} ${round(o.a ?? 0, 4)} ${round(o.b ?? 0, 4)}${aTail})`;
|
|
106
|
+
}
|
|
107
|
+
default:
|
|
108
|
+
return formatHex(picked);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
58
111
|
|
|
59
112
|
// ── Presets ─────────────────────────────────────────────────────────────────────
|
|
60
113
|
export type ThemePreset = {
|
|
@@ -294,20 +347,20 @@ export function ThemePanel({
|
|
|
294
347
|
<legend className="pl-theme-group__legend">{g.label}</legend>
|
|
295
348
|
{vars.map((v) => {
|
|
296
349
|
const val = valueOf(v);
|
|
297
|
-
const
|
|
350
|
+
const color = isColor(val);
|
|
298
351
|
const changed = v in overrides;
|
|
299
352
|
return (
|
|
300
353
|
<label key={v} className={cx("pl-theme-row", changed && "pl-theme-row--changed")}>
|
|
301
|
-
{
|
|
354
|
+
{color ? (
|
|
302
355
|
<input
|
|
303
356
|
type="color"
|
|
304
357
|
className="pl-theme-row__swatch"
|
|
305
|
-
value={val}
|
|
306
|
-
onInput={(e) => setVar(v, (e.target as HTMLInputElement).value)}
|
|
358
|
+
value={toSwatchHex(val)}
|
|
359
|
+
onInput={(e) => setVar(v, reformat((e.target as HTMLInputElement).value, val))}
|
|
307
360
|
aria-label={prettyLabel(v)}
|
|
308
361
|
/>
|
|
309
362
|
) : (
|
|
310
|
-
<span className="pl-theme-row__swatch pl-theme-row__swatch--
|
|
363
|
+
<span className="pl-theme-row__swatch pl-theme-row__swatch--none" aria-hidden />
|
|
311
364
|
)}
|
|
312
365
|
<span className="pl-theme-row__label" title={v}>
|
|
313
366
|
{prettyLabel(v)}
|