@arronqzy/ui 1.0.8 → 1.0.10
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 +23 -7
- package/index.ts +1 -0
- package/package.json +5 -3
- package/src/lib/theme-palette.ts +228 -0
- package/src/styles.css +8 -7
- package/src/theme-provider.tsx +13 -1
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
|
}
|
|
@@ -1946,6 +1950,10 @@ body {
|
|
|
1946
1950
|
text-underline-offset: 4px;
|
|
1947
1951
|
}
|
|
1948
1952
|
|
|
1953
|
+
.accent-foreground {
|
|
1954
|
+
accent-color: hsl(var(--foreground));
|
|
1955
|
+
}
|
|
1956
|
+
|
|
1949
1957
|
.opacity-0 {
|
|
1950
1958
|
opacity: 0;
|
|
1951
1959
|
}
|
|
@@ -1999,6 +2007,12 @@ body {
|
|
|
1999
2007
|
outline-style: solid;
|
|
2000
2008
|
}
|
|
2001
2009
|
|
|
2010
|
+
.ring {
|
|
2011
|
+
--tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);
|
|
2012
|
+
--tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(3px + var(--tw-ring-offset-width)) var(--tw-ring-color);
|
|
2013
|
+
box-shadow: var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow, 0 0 #0000);
|
|
2014
|
+
}
|
|
2015
|
+
|
|
2002
2016
|
.ring-0 {
|
|
2003
2017
|
--tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);
|
|
2004
2018
|
--tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(0px + var(--tw-ring-offset-width)) var(--tw-ring-color);
|
|
@@ -2121,6 +2135,8 @@ body {
|
|
|
2121
2135
|
--cell-size: 2rem;
|
|
2122
2136
|
}
|
|
2123
2137
|
|
|
2138
|
+
/* Static fallback; ThemeProvider overrides these with the chroma-js palette from lib/theme-palette.ts. */
|
|
2139
|
+
|
|
2124
2140
|
/* 弹窗蒙层全屏覆盖,层级高于画布 Ruler / 工具栏 */
|
|
2125
2141
|
|
|
2126
2142
|
[data-radix-dialog-overlay],
|
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.10",
|
|
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,13 +59,14 @@
|
|
|
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
67
|
"@arronqzy/eslint-config": "0.0.0",
|
|
66
|
-
"@arronqzy/
|
|
67
|
-
"@arronqzy/
|
|
68
|
+
"@arronqzy/tailwind": "1.0.2",
|
|
69
|
+
"@arronqzy/typescript-config": "1.0.0"
|
|
68
70
|
},
|
|
69
71
|
"scripts": {
|
|
70
72
|
"lint": "eslint \"**/*.{ts,tsx}\"",
|
|
@@ -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
|
-
|