@arronqzy/ui 1.0.9 → 1.0.11
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/README.md +15 -18
- package/dist/styles.css +31 -19
- package/index.ts +1 -0
- package/package.json +4 -2
- package/src/lib/theme-palette.ts +228 -0
- package/src/styles.css +8 -7
- package/src/theme-provider.tsx +13 -1
- package/src/ui/resizable.tsx +3 -4
package/README.md
CHANGED
|
@@ -1,15 +1,12 @@
|
|
|
1
1
|
# @arronqzy/ui
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
React 编辑器和蓝图用的组件:Button、Dialog、Menu、Toast、可调面板等。基于 Radix 和 Tailwind,视觉和 `@arronqzy/tailwind` 的令牌一致。
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
这是编辑器内部 UI,不是给业务大屏用的组件库。大屏上的图表、表格、三维在 `react-view` 的物料里。
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
-
|
|
10
|
-
- **主题**:`ThemeProvider` 支持亮/暗色与 `class` 策略
|
|
11
|
-
- **层级管理**:`UI_Z_INDEX` / `MODAL_Z_INDEX` 统一浮层 z-index
|
|
12
|
-
- **图表**:封装 Recharts 常用图表(按需使用)
|
|
7
|
+
下图顶栏、侧栏、按钮都来自本包:
|
|
8
|
+
|
|
9
|
+

|
|
13
10
|
|
|
14
11
|
## 安装
|
|
15
12
|
|
|
@@ -21,7 +18,7 @@ pnpm add @arronqzy/ui
|
|
|
21
18
|
import "@arronqzy/ui/styles.css";
|
|
22
19
|
```
|
|
23
20
|
|
|
24
|
-
##
|
|
21
|
+
## 用法
|
|
25
22
|
|
|
26
23
|
```tsx
|
|
27
24
|
import { ThemeProvider, Button, toast } from "@arronqzy/ui";
|
|
@@ -36,27 +33,27 @@ function App() {
|
|
|
36
33
|
}
|
|
37
34
|
```
|
|
38
35
|
|
|
39
|
-
### 工具函数
|
|
40
|
-
|
|
41
36
|
```ts
|
|
42
|
-
import { cn } from "@arronqzy/ui";
|
|
43
|
-
import { UI_Z_INDEX } from "@arronqzy/ui";
|
|
37
|
+
import { cn, UI_Z_INDEX } from "@arronqzy/ui";
|
|
44
38
|
```
|
|
45
39
|
|
|
46
|
-
|
|
40
|
+
浮层 z-index 用 `UI_Z_INDEX` / `MODAL_Z_INDEX`,不要在蓝图或配置面板里另写一个魔法数字,否则菜单会被画布盖住。
|
|
41
|
+
|
|
42
|
+
## 导出
|
|
47
43
|
|
|
48
|
-
- `@arronqzy/ui` —
|
|
49
|
-
- `@arronqzy/ui/styles.css` —
|
|
44
|
+
- `@arronqzy/ui` — 组件、`cn`、`ThemeProvider`、`useToast`
|
|
45
|
+
- `@arronqzy/ui/styles.css` — 编好的 Tailwind
|
|
50
46
|
|
|
51
|
-
|
|
47
|
+
源码在 `src/ui/`,按 shadcn 的方式组合。加组件时放进这个包,不要在 `react-view` 里复制一份 Dialog。
|
|
52
48
|
|
|
53
49
|
## 构建样式
|
|
54
50
|
|
|
55
51
|
```bash
|
|
56
52
|
pnpm -C packages/ui build:css
|
|
53
|
+
pnpm -C packages/ui dev:css
|
|
57
54
|
```
|
|
58
55
|
|
|
59
|
-
|
|
56
|
+
`apps/web` 的 `predev` 会构建一次。改组件 class 时开 `dev:css`,否则演示页看不到样式变化。
|
|
60
57
|
|
|
61
58
|
## 许可证
|
|
62
59
|
|
package/dist/styles.css
CHANGED
|
@@ -566,13 +566,13 @@ video {
|
|
|
566
566
|
--secondary: 210 40% 96.1%;
|
|
567
567
|
--secondary-foreground: 222.2 47.4% 11.2%;
|
|
568
568
|
--muted: 210 40% 96.1%;
|
|
569
|
-
--muted-foreground: 215.4 16.
|
|
569
|
+
--muted-foreground: 215.4 16.7% 45.7%;
|
|
570
570
|
--accent: 210 40% 96.1%;
|
|
571
571
|
--accent-foreground: 222.2 47.4% 11.2%;
|
|
572
|
-
--destructive:
|
|
573
|
-
--destructive-foreground:
|
|
572
|
+
--destructive: 358.6 72.7% 53.5%;
|
|
573
|
+
--destructive-foreground: 0 0% 100%;
|
|
574
574
|
--border: 214.3 31.8% 91.4%;
|
|
575
|
-
--input: 214.3
|
|
575
|
+
--input: 214.3 15.9% 83%;
|
|
576
576
|
--ring: 222.2 84% 4.9%;
|
|
577
577
|
--radius: 0.5rem;
|
|
578
578
|
}
|
|
@@ -592,10 +592,10 @@ video {
|
|
|
592
592
|
--muted-foreground: 215 20.2% 65.1%;
|
|
593
593
|
--accent: 217.2 32.6% 17.5%;
|
|
594
594
|
--accent-foreground: 210 40% 98%;
|
|
595
|
-
--destructive: 0
|
|
596
|
-
--destructive-foreground:
|
|
595
|
+
--destructive: 0 90.6% 70.8%;
|
|
596
|
+
--destructive-foreground: 222.2 47.4% 11.2%;
|
|
597
597
|
--border: 217.2 32.6% 17.5%;
|
|
598
|
-
--input: 217.2
|
|
598
|
+
--input: 217.2 28.5% 20.4%;
|
|
599
599
|
--ring: 212.7 26.8% 83.9%;
|
|
600
600
|
}
|
|
601
601
|
|
|
@@ -636,6 +636,10 @@ body {
|
|
|
636
636
|
visibility: hidden;
|
|
637
637
|
}
|
|
638
638
|
|
|
639
|
+
.static {
|
|
640
|
+
position: static;
|
|
641
|
+
}
|
|
642
|
+
|
|
639
643
|
.fixed {
|
|
640
644
|
position: fixed;
|
|
641
645
|
}
|
|
@@ -1546,10 +1550,6 @@ body {
|
|
|
1546
1550
|
border-color: hsl(var(--border) / 0.5);
|
|
1547
1551
|
}
|
|
1548
1552
|
|
|
1549
|
-
.border-border\/60 {
|
|
1550
|
-
border-color: hsl(var(--border) / 0.6);
|
|
1551
|
-
}
|
|
1552
|
-
|
|
1553
1553
|
.border-border\/70 {
|
|
1554
1554
|
border-color: hsl(var(--border) / 0.7);
|
|
1555
1555
|
}
|
|
@@ -1594,8 +1594,8 @@ body {
|
|
|
1594
1594
|
background-color: hsl(var(--background));
|
|
1595
1595
|
}
|
|
1596
1596
|
|
|
1597
|
-
.bg-background\/
|
|
1598
|
-
background-color: hsl(var(--background) / 0.
|
|
1597
|
+
.bg-background\/80 {
|
|
1598
|
+
background-color: hsl(var(--background) / 0.8);
|
|
1599
1599
|
}
|
|
1600
1600
|
|
|
1601
1601
|
.bg-black\/80 {
|
|
@@ -1606,8 +1606,8 @@ body {
|
|
|
1606
1606
|
background-color: hsl(var(--border));
|
|
1607
1607
|
}
|
|
1608
1608
|
|
|
1609
|
-
.bg-border\/
|
|
1610
|
-
background-color: hsl(var(--border) / 0.
|
|
1609
|
+
.bg-border\/40 {
|
|
1610
|
+
background-color: hsl(var(--border) / 0.4);
|
|
1611
1611
|
}
|
|
1612
1612
|
|
|
1613
1613
|
.bg-card {
|
|
@@ -1918,10 +1918,6 @@ body {
|
|
|
1918
1918
|
color: hsl(var(--foreground) / 0.5);
|
|
1919
1919
|
}
|
|
1920
1920
|
|
|
1921
|
-
.text-foreground\/60 {
|
|
1922
|
-
color: hsl(var(--foreground) / 0.6);
|
|
1923
|
-
}
|
|
1924
|
-
|
|
1925
1921
|
.text-muted-foreground {
|
|
1926
1922
|
color: hsl(var(--muted-foreground));
|
|
1927
1923
|
}
|
|
@@ -1946,6 +1942,10 @@ body {
|
|
|
1946
1942
|
text-underline-offset: 4px;
|
|
1947
1943
|
}
|
|
1948
1944
|
|
|
1945
|
+
.accent-foreground {
|
|
1946
|
+
accent-color: hsl(var(--foreground));
|
|
1947
|
+
}
|
|
1948
|
+
|
|
1949
1949
|
.opacity-0 {
|
|
1950
1950
|
opacity: 0;
|
|
1951
1951
|
}
|
|
@@ -1999,6 +1999,12 @@ body {
|
|
|
1999
1999
|
outline-style: solid;
|
|
2000
2000
|
}
|
|
2001
2001
|
|
|
2002
|
+
.ring {
|
|
2003
|
+
--tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);
|
|
2004
|
+
--tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(3px + var(--tw-ring-offset-width)) var(--tw-ring-color);
|
|
2005
|
+
box-shadow: var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow, 0 0 #0000);
|
|
2006
|
+
}
|
|
2007
|
+
|
|
2002
2008
|
.ring-0 {
|
|
2003
2009
|
--tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);
|
|
2004
2010
|
--tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(0px + var(--tw-ring-offset-width)) var(--tw-ring-color);
|
|
@@ -2121,6 +2127,8 @@ body {
|
|
|
2121
2127
|
--cell-size: 2rem;
|
|
2122
2128
|
}
|
|
2123
2129
|
|
|
2130
|
+
/* Static fallback; ThemeProvider overrides these with the chroma-js palette from lib/theme-palette.ts. */
|
|
2131
|
+
|
|
2124
2132
|
/* 弹窗蒙层全屏覆盖,层级高于画布 Ruler / 工具栏 */
|
|
2125
2133
|
|
|
2126
2134
|
[data-radix-dialog-overlay],
|
|
@@ -2182,6 +2190,10 @@ body {
|
|
|
2182
2190
|
background-color: hsl(var(--accent));
|
|
2183
2191
|
}
|
|
2184
2192
|
|
|
2193
|
+
.hover\:bg-border\/80:hover {
|
|
2194
|
+
background-color: hsl(var(--border) / 0.8);
|
|
2195
|
+
}
|
|
2196
|
+
|
|
2185
2197
|
.hover\:bg-destructive\/80:hover {
|
|
2186
2198
|
background-color: hsl(var(--destructive) / 0.8);
|
|
2187
2199
|
}
|
package/index.ts
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@arronqzy/ui",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.11",
|
|
4
4
|
"exports": {
|
|
5
5
|
".": "./index.ts",
|
|
6
6
|
"./counter": "./components/counter.ts",
|
|
@@ -38,6 +38,7 @@
|
|
|
38
38
|
"@radix-ui/react-toggle": "^1.1.9",
|
|
39
39
|
"@radix-ui/react-toggle-group": "^1.1.10",
|
|
40
40
|
"@radix-ui/react-tooltip": "^1.2.8",
|
|
41
|
+
"chroma-js": "^3.2.0",
|
|
41
42
|
"class-variance-authority": "^0.7.1",
|
|
42
43
|
"clsx": "^2.1.1",
|
|
43
44
|
"cmdk": "^1.1.1",
|
|
@@ -58,12 +59,13 @@
|
|
|
58
59
|
"zod": "^4.1.9"
|
|
59
60
|
},
|
|
60
61
|
"devDependencies": {
|
|
62
|
+
"@types/chroma-js": "^3.1.2",
|
|
61
63
|
"@types/react": "^19.0.1",
|
|
62
64
|
"@types/react-dom": "^19.0.2",
|
|
63
65
|
"eslint": "^8.57.0",
|
|
64
66
|
"typescript": "^5.5.4",
|
|
65
|
-
"@arronqzy/eslint-config": "0.0.0",
|
|
66
67
|
"@arronqzy/tailwind": "1.0.2",
|
|
68
|
+
"@arronqzy/eslint-config": "0.0.0",
|
|
67
69
|
"@arronqzy/typescript-config": "1.0.0"
|
|
68
70
|
},
|
|
69
71
|
"scripts": {
|
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
import chroma, { type Color } from "chroma-js";
|
|
2
|
+
|
|
3
|
+
export type ThemeMode = "light" | "dark";
|
|
4
|
+
|
|
5
|
+
export const THEME_TOKENS = [
|
|
6
|
+
"background",
|
|
7
|
+
"foreground",
|
|
8
|
+
"card",
|
|
9
|
+
"card-foreground",
|
|
10
|
+
"popover",
|
|
11
|
+
"popover-foreground",
|
|
12
|
+
"primary",
|
|
13
|
+
"primary-foreground",
|
|
14
|
+
"secondary",
|
|
15
|
+
"secondary-foreground",
|
|
16
|
+
"muted",
|
|
17
|
+
"muted-foreground",
|
|
18
|
+
"accent",
|
|
19
|
+
"accent-foreground",
|
|
20
|
+
"destructive",
|
|
21
|
+
"destructive-foreground",
|
|
22
|
+
"border",
|
|
23
|
+
"input",
|
|
24
|
+
"ring",
|
|
25
|
+
] as const;
|
|
26
|
+
|
|
27
|
+
export type ThemeToken = (typeof THEME_TOKENS)[number];
|
|
28
|
+
|
|
29
|
+
/** Any color string chroma-js can parse (hex, rgb(), hsl(), named…). */
|
|
30
|
+
export type ThemeColors = Record<ThemeToken, string>;
|
|
31
|
+
|
|
32
|
+
export type ThemePaletteOptions = {
|
|
33
|
+
/** Brand color; `primary-foreground` and `ring` are derived from it. */
|
|
34
|
+
primary?: string | Partial<Record<ThemeMode, string>>;
|
|
35
|
+
overrides?: Partial<Record<ThemeMode, Partial<ThemeColors>>>;
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
export const BASE_THEME_COLORS: Record<ThemeMode, ThemeColors> = {
|
|
39
|
+
light: {
|
|
40
|
+
background: "#ffffff",
|
|
41
|
+
foreground: "#020817",
|
|
42
|
+
card: "#ffffff",
|
|
43
|
+
"card-foreground": "#020817",
|
|
44
|
+
popover: "#ffffff",
|
|
45
|
+
"popover-foreground": "#020817",
|
|
46
|
+
primary: "#0f172a",
|
|
47
|
+
"primary-foreground": "#f8fafc",
|
|
48
|
+
secondary: "#f1f5f9",
|
|
49
|
+
"secondary-foreground": "#0f172a",
|
|
50
|
+
muted: "#f1f5f9",
|
|
51
|
+
"muted-foreground": "#64748b",
|
|
52
|
+
accent: "#f1f5f9",
|
|
53
|
+
"accent-foreground": "#0f172a",
|
|
54
|
+
destructive: "#ef4444",
|
|
55
|
+
"destructive-foreground": "#f8fafc",
|
|
56
|
+
border: "#e2e8f0",
|
|
57
|
+
input: "#e2e8f0",
|
|
58
|
+
ring: "#020817",
|
|
59
|
+
},
|
|
60
|
+
dark: {
|
|
61
|
+
background: "#020817",
|
|
62
|
+
foreground: "#f8fafc",
|
|
63
|
+
card: "#020817",
|
|
64
|
+
"card-foreground": "#f8fafc",
|
|
65
|
+
popover: "#020817",
|
|
66
|
+
"popover-foreground": "#f8fafc",
|
|
67
|
+
primary: "#f8fafc",
|
|
68
|
+
"primary-foreground": "#0f172a",
|
|
69
|
+
secondary: "#1e293b",
|
|
70
|
+
"secondary-foreground": "#f8fafc",
|
|
71
|
+
muted: "#1e293b",
|
|
72
|
+
"muted-foreground": "#94a3b8",
|
|
73
|
+
accent: "#1e293b",
|
|
74
|
+
"accent-foreground": "#f8fafc",
|
|
75
|
+
destructive: "#f87171",
|
|
76
|
+
"destructive-foreground": "#0f172a",
|
|
77
|
+
border: "#1e293b",
|
|
78
|
+
input: "#1e293b",
|
|
79
|
+
ring: "#cbd5e1",
|
|
80
|
+
},
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
type ContrastRule = { token: ThemeToken; against: ThemeToken[]; min: number };
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Applied in order: a token is only adjusted against tokens that are already final,
|
|
87
|
+
* so surfaces come first, then fills, then the text drawn on top of them.
|
|
88
|
+
*/
|
|
89
|
+
const CONTRAST_RULES: ContrastRule[] = [
|
|
90
|
+
{ token: "foreground", against: ["background"], min: 7 },
|
|
91
|
+
{ token: "card-foreground", against: ["card"], min: 7 },
|
|
92
|
+
{ token: "popover-foreground", against: ["popover"], min: 7 },
|
|
93
|
+
{ token: "primary", against: ["background"], min: 3 },
|
|
94
|
+
{ token: "destructive", against: ["background", "card"], min: 4.5 },
|
|
95
|
+
{ token: "ring", against: ["background"], min: 3 },
|
|
96
|
+
{ token: "primary-foreground", against: ["primary"], min: 4.5 },
|
|
97
|
+
{ token: "secondary-foreground", against: ["secondary"], min: 4.5 },
|
|
98
|
+
{ token: "accent-foreground", against: ["accent"], min: 4.5 },
|
|
99
|
+
{ token: "destructive-foreground", against: ["destructive"], min: 4.5 },
|
|
100
|
+
{ token: "muted-foreground", against: ["background", "card", "muted", "accent"], min: 4.5 },
|
|
101
|
+
{ token: "border", against: ["background"], min: 1.2 },
|
|
102
|
+
{ token: "input", against: ["background"], min: 1.5 },
|
|
103
|
+
];
|
|
104
|
+
|
|
105
|
+
const LIGHTNESS_STEP = 0.005;
|
|
106
|
+
const CHROMA_STEP = 0.002;
|
|
107
|
+
|
|
108
|
+
function minContrast(color: Color, against: Color[]): number {
|
|
109
|
+
return Math.min(...against.map((bg) => chroma.contrast(color, bg)));
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/** Drops chroma until the color fits sRGB; clipping would otherwise shift the hue. */
|
|
113
|
+
function inGamutOklch(l: number, c: number, h: number): Color {
|
|
114
|
+
let chromaValue = c;
|
|
115
|
+
let candidate = chroma.oklch(l, chromaValue, h);
|
|
116
|
+
while (candidate.clipped() && chromaValue > 0) {
|
|
117
|
+
chromaValue = Math.max(0, chromaValue - CHROMA_STEP);
|
|
118
|
+
candidate = chroma.oklch(l, chromaValue, h);
|
|
119
|
+
}
|
|
120
|
+
return candidate;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function walkLightness(
|
|
124
|
+
color: Color,
|
|
125
|
+
against: Color[],
|
|
126
|
+
min: number,
|
|
127
|
+
direction: 1 | -1,
|
|
128
|
+
): Color | null {
|
|
129
|
+
const [l, c, h] = color.oklch();
|
|
130
|
+
const hue = Number.isNaN(h) ? 0 : h;
|
|
131
|
+
for (let next = l; next >= 0 && next <= 1; next += direction * LIGHTNESS_STEP) {
|
|
132
|
+
const candidate = inGamutOklch(next, c, hue);
|
|
133
|
+
if (minContrast(candidate, against) >= min) return candidate;
|
|
134
|
+
}
|
|
135
|
+
return null;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Nudges `color` along OKLCH lightness (hue and chroma kept) until it reaches `min`
|
|
140
|
+
* contrast against every color in `against`. Moves away from the backgrounds first and
|
|
141
|
+
* flips direction only when that side cannot satisfy the target.
|
|
142
|
+
*/
|
|
143
|
+
export function ensureContrast(color: string | Color, against: Array<string | Color>, min: number): Color {
|
|
144
|
+
const fg = chroma(color);
|
|
145
|
+
const bgs = against.map((bg) => chroma(bg));
|
|
146
|
+
if (minContrast(fg, bgs) >= min) return fg;
|
|
147
|
+
|
|
148
|
+
const bgLuminance = bgs.reduce((sum, bg) => sum + bg.luminance(), 0) / bgs.length;
|
|
149
|
+
const preferred: 1 | -1 = fg.luminance() >= bgLuminance ? 1 : -1;
|
|
150
|
+
const result =
|
|
151
|
+
walkLightness(fg, bgs, min, preferred) ?? walkLightness(fg, bgs, min, preferred === 1 ? -1 : 1);
|
|
152
|
+
if (result) return result;
|
|
153
|
+
|
|
154
|
+
const extremes = [chroma("#ffffff"), chroma("#000000")];
|
|
155
|
+
return extremes.reduce((best, c) => (minContrast(c, bgs) > minContrast(best, bgs) ? c : best));
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/** Best of near-white / near-black text for a fill color. */
|
|
159
|
+
function readableOn(fill: Color, light: string, dark: string): string {
|
|
160
|
+
return chroma.contrast(light, fill) >= chroma.contrast(dark, fill) ? light : dark;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function resolvePrimary(option: ThemePaletteOptions["primary"], mode: ThemeMode): string | undefined {
|
|
164
|
+
if (!option) return undefined;
|
|
165
|
+
return typeof option === "string" ? option : option[mode];
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
export function createThemeColors(mode: ThemeMode, options: ThemePaletteOptions = {}): Record<ThemeToken, Color> {
|
|
169
|
+
const base = BASE_THEME_COLORS[mode];
|
|
170
|
+
const source: ThemeColors = { ...base };
|
|
171
|
+
|
|
172
|
+
const primary = resolvePrimary(options.primary, mode);
|
|
173
|
+
if (primary && chroma.valid(primary)) {
|
|
174
|
+
source.primary = primary;
|
|
175
|
+
source["primary-foreground"] = readableOn(
|
|
176
|
+
chroma(primary),
|
|
177
|
+
BASE_THEME_COLORS.light["primary-foreground"],
|
|
178
|
+
BASE_THEME_COLORS.dark["primary-foreground"],
|
|
179
|
+
);
|
|
180
|
+
source.ring = primary;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
for (const [token, value] of Object.entries(options.overrides?.[mode] ?? {})) {
|
|
184
|
+
if (value && chroma.valid(value)) source[token as ThemeToken] = value;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
const colors = Object.fromEntries(
|
|
188
|
+
THEME_TOKENS.map((token) => [token, chroma(source[token])]),
|
|
189
|
+
) as Record<ThemeToken, Color>;
|
|
190
|
+
|
|
191
|
+
for (const rule of CONTRAST_RULES) {
|
|
192
|
+
colors[rule.token] = ensureContrast(
|
|
193
|
+
colors[rule.token],
|
|
194
|
+
rule.against.map((token) => colors[token]),
|
|
195
|
+
rule.min,
|
|
196
|
+
);
|
|
197
|
+
}
|
|
198
|
+
return colors;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
const round = (value: number) => Math.round(value * 10) / 10;
|
|
202
|
+
|
|
203
|
+
/** Formats a color as the `H S% L%` triplet consumed by `hsl(var(--token))`. */
|
|
204
|
+
export function toHslTriplet(color: Color): string {
|
|
205
|
+
const [h, s, l] = color.hsl();
|
|
206
|
+
const hue = Number.isNaN(h) ? 0 : h;
|
|
207
|
+
const sat = Number.isNaN(s) ? 0 : s;
|
|
208
|
+
return `${round(hue)} ${round(sat * 100)}% ${round(l * 100)}%`;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
export function createThemePalette(mode: ThemeMode, options?: ThemePaletteOptions): Record<ThemeToken, string> {
|
|
212
|
+
const colors = createThemeColors(mode, options);
|
|
213
|
+
return Object.fromEntries(
|
|
214
|
+
THEME_TOKENS.map((token) => [token, toHslTriplet(colors[token])]),
|
|
215
|
+
) as Record<ThemeToken, string>;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function toCssBlock(selector: string, palette: Record<ThemeToken, string>): string {
|
|
219
|
+
const lines = THEME_TOKENS.map((token) => ` --${token}: ${palette[token]};`);
|
|
220
|
+
return `${selector} {\n${lines.join("\n")}\n}`;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
export function buildThemePaletteCss(options?: ThemePaletteOptions): string {
|
|
224
|
+
return [
|
|
225
|
+
toCssBlock(":root", createThemePalette("light", options)),
|
|
226
|
+
toCssBlock(".dark", createThemePalette("dark", options)),
|
|
227
|
+
].join("\n");
|
|
228
|
+
}
|
package/src/styles.css
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
@tailwind components;
|
|
3
3
|
@tailwind utilities;
|
|
4
4
|
|
|
5
|
+
/* Static fallback; ThemeProvider overrides these with the chroma-js palette from lib/theme-palette.ts. */
|
|
5
6
|
@layer base {
|
|
6
7
|
:root {
|
|
7
8
|
--background: 0 0% 100%;
|
|
@@ -20,16 +21,16 @@
|
|
|
20
21
|
--secondary-foreground: 222.2 47.4% 11.2%;
|
|
21
22
|
|
|
22
23
|
--muted: 210 40% 96.1%;
|
|
23
|
-
--muted-foreground: 215.4 16.
|
|
24
|
+
--muted-foreground: 215.4 16.7% 45.7%;
|
|
24
25
|
|
|
25
26
|
--accent: 210 40% 96.1%;
|
|
26
27
|
--accent-foreground: 222.2 47.4% 11.2%;
|
|
27
28
|
|
|
28
|
-
--destructive:
|
|
29
|
-
--destructive-foreground:
|
|
29
|
+
--destructive: 358.6 72.7% 53.5%;
|
|
30
|
+
--destructive-foreground: 0 0% 100%;
|
|
30
31
|
|
|
31
32
|
--border: 214.3 31.8% 91.4%;
|
|
32
|
-
--input: 214.3
|
|
33
|
+
--input: 214.3 15.9% 83%;
|
|
33
34
|
--ring: 222.2 84% 4.9%;
|
|
34
35
|
|
|
35
36
|
--radius: 0.5rem;
|
|
@@ -57,11 +58,11 @@
|
|
|
57
58
|
--accent: 217.2 32.6% 17.5%;
|
|
58
59
|
--accent-foreground: 210 40% 98%;
|
|
59
60
|
|
|
60
|
-
--destructive: 0
|
|
61
|
-
--destructive-foreground:
|
|
61
|
+
--destructive: 0 90.6% 70.8%;
|
|
62
|
+
--destructive-foreground: 222.2 47.4% 11.2%;
|
|
62
63
|
|
|
63
64
|
--border: 217.2 32.6% 17.5%;
|
|
64
|
-
--input: 217.2
|
|
65
|
+
--input: 217.2 28.5% 20.4%;
|
|
65
66
|
--ring: 212.7 26.8% 83.9%;
|
|
66
67
|
}
|
|
67
68
|
|
package/src/theme-provider.tsx
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
import * as React from "react";
|
|
4
4
|
import { ThemeProvider as NextThemesProvider } from "next-themes";
|
|
5
5
|
import type { ThemeProviderProps } from "next-themes";
|
|
6
|
+
import { buildThemePaletteCss, type ThemePaletteOptions } from "./lib/theme-palette";
|
|
6
7
|
|
|
7
8
|
export type AppTheme = "light" | "dark";
|
|
8
9
|
|
|
@@ -12,14 +13,23 @@ export type ArronqzyThemeProviderProps = Omit<
|
|
|
12
13
|
> & {
|
|
13
14
|
defaultTheme?: AppTheme;
|
|
14
15
|
enableSystem?: boolean;
|
|
16
|
+
/** Contrast-checked theme variables generated with chroma-js; `false` keeps the static CSS. */
|
|
17
|
+
palette?: ThemePaletteOptions | false;
|
|
15
18
|
};
|
|
16
19
|
|
|
17
20
|
export function ThemeProvider({
|
|
18
21
|
children,
|
|
19
22
|
defaultTheme = "dark",
|
|
20
23
|
enableSystem = true,
|
|
24
|
+
palette = {},
|
|
21
25
|
...props
|
|
22
26
|
}: ArronqzyThemeProviderProps) {
|
|
27
|
+
const paletteKey = palette ? JSON.stringify(palette) : "";
|
|
28
|
+
const paletteCss = React.useMemo(
|
|
29
|
+
() => (paletteKey ? buildThemePaletteCss(JSON.parse(paletteKey) as ThemePaletteOptions) : ""),
|
|
30
|
+
[paletteKey],
|
|
31
|
+
);
|
|
32
|
+
|
|
23
33
|
return (
|
|
24
34
|
<NextThemesProvider
|
|
25
35
|
attribute="class"
|
|
@@ -27,10 +37,12 @@ export function ThemeProvider({
|
|
|
27
37
|
enableSystem={enableSystem}
|
|
28
38
|
{...props}
|
|
29
39
|
>
|
|
40
|
+
{paletteCss ? (
|
|
41
|
+
<style data-arronqzy-theme-palette="" dangerouslySetInnerHTML={{ __html: paletteCss }} />
|
|
42
|
+
) : null}
|
|
30
43
|
{children}
|
|
31
44
|
</NextThemesProvider>
|
|
32
45
|
);
|
|
33
46
|
}
|
|
34
47
|
|
|
35
48
|
export { useTheme } from "next-themes";
|
|
36
|
-
|
package/src/ui/resizable.tsx
CHANGED
|
@@ -29,15 +29,14 @@ const ResizableHandle = ({
|
|
|
29
29
|
}) => (
|
|
30
30
|
<ResizablePrimitive.PanelResizeHandle
|
|
31
31
|
className={cn(
|
|
32
|
-
|
|
33
|
-
"relative flex w-px items-center justify-center bg-border/60 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring focus-visible:ring-offset-1 data-[panel-group-direction=vertical]:h-px data-[panel-group-direction=vertical]:w-full [&[data-panel-group-direction=vertical]>div]:rotate-90",
|
|
32
|
+
"relative flex w-px items-center justify-center bg-border/40 transition-colors hover:bg-border/80 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring focus-visible:ring-offset-1 data-[panel-group-direction=vertical]:h-px data-[panel-group-direction=vertical]:w-full [&[data-panel-group-direction=vertical]>div]:rotate-90",
|
|
34
33
|
className
|
|
35
34
|
)}
|
|
36
35
|
{...props}
|
|
37
36
|
>
|
|
38
37
|
{withHandle && (
|
|
39
|
-
<div className="z-10 flex h-
|
|
40
|
-
<GripVertical className="h-2.5 w-2.5 text-foreground
|
|
38
|
+
<div className="z-10 flex h-5 w-2.5 items-center justify-center rounded-md border border-border/50 bg-background/80 opacity-50 shadow-sm transition-opacity hover:opacity-100">
|
|
39
|
+
<GripVertical className="h-2.5 w-2.5 text-muted-foreground" />
|
|
41
40
|
</div>
|
|
42
41
|
)}
|
|
43
42
|
</ResizablePrimitive.PanelResizeHandle>
|