@arronqzy/ui 1.0.9 → 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 CHANGED
@@ -1,15 +1,12 @@
1
1
  # @arronqzy/ui
2
2
 
3
- Abuilder 的**共享 UI 组件库**,基于 [shadcn/ui](https://ui.shadcn.com/) + Radix UI + Tailwind CSS,为编辑器与蓝图提供统一视觉与交互。
3
+ React 编辑器和蓝图用的组件:Button、Dialog、Menu、Toast、可调面板等。基于 Radix 和 Tailwind,视觉和 `@arronqzy/tailwind` 的令牌一致。
4
4
 
5
- ## 功能概览
5
+ 这是编辑器内部 UI,不是给业务大屏用的组件库。大屏上的图表、表格、三维在 `react-view` 的物料里。
6
6
 
7
- - **基础组件**:Button、Input、Dialog、Dropdown、Tabs、Tooltip、Toast 等
8
- - **表单**:与 `react-hook-form`、`zod` 集成的表单控件
9
- - **布局**:Resizable Panels、ScrollArea、Separator
10
- - **主题**:`ThemeProvider` 支持亮/暗色与 `class` 策略
11
- - **层级管理**:`UI_Z_INDEX` / `MODAL_Z_INDEX` 统一浮层 z-index
12
- - **图表**:封装 Recharts 常用图表(按需使用)
7
+ 下图顶栏、侧栏、按钮都来自本包:
8
+
9
+ ![编辑器界面由本包的菜单、按钮和面板拼出来](https://github.com/qianzhuoyao/APanel5/raw/v5/docs/guide-assets/live-zh/01-overview.png)
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` — 全部组件、`cn`、`ThemeProvider`、`useToast`
49
- - `@arronqzy/ui/styles.css` — 编译后的 Tailwind 样式
44
+ - `@arronqzy/ui` — 组件、`cn`、`ThemeProvider`、`useToast`
45
+ - `@arronqzy/ui/styles.css` — 编好的 Tailwind
50
46
 
51
- 组件源码位于 `src/ui/`,遵循 shadcn 可组合模式,可按需在本包内扩展。
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
- 依赖 `@arronqzy/tailwind` 预设保持与设计令牌一致。
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.3% 46.9%;
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: 0 84.2% 60.2%;
573
- --destructive-foreground: 210 40% 98%;
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 31.8% 91.4%;
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 62.8% 30.6%;
596
- --destructive-foreground: 210 40% 98%;
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 32.6% 17.5%;
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
@@ -5,3 +5,4 @@ export * from "./src/lib/utils";
5
5
  export { MODAL_Z_INDEX, UI_Z_INDEX } from "./src/lib/overlay-z-index";
6
6
  export * from "./src/hooks/use-toast";
7
7
  export * from "./src/theme-provider";
8
+ export * from "./src/lib/theme-palette";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@arronqzy/ui",
3
- "version": "1.0.9",
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,6 +59,7 @@
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",
@@ -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.3% 46.9%;
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: 0 84.2% 60.2%;
29
- --destructive-foreground: 210 40% 98%;
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 31.8% 91.4%;
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 62.8% 30.6%;
61
- --destructive-foreground: 210 40% 98%;
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 32.6% 17.5%;
65
+ --input: 217.2 28.5% 20.4%;
65
66
  --ring: 212.7 26.8% 83.9%;
66
67
  }
67
68
 
@@ -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
-