@badnpm/ui 0.0.0-stage → 0.0.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/LICENSE +21 -0
- package/README.md +193 -2
- package/dist/cjs/components/button.cjs +41 -0
- package/dist/cjs/components/input.cjs +11 -0
- package/dist/cjs/components/theme-toggle.cjs +19 -0
- package/dist/cjs/hooks/use-hydrated.cjs +15 -0
- package/dist/cjs/index.cjs +30 -0
- package/dist/cjs/lib/preference.cjs +40 -0
- package/dist/cjs/providers/preference-provider.cjs +127 -0
- package/dist/cjs/providers/theme-provider.cjs +181 -0
- package/dist/cjs/types/components/button.d.ts +12 -0
- package/dist/cjs/types/components/index.d.ts +3 -0
- package/dist/cjs/types/components/input.d.ts +5 -0
- package/dist/cjs/types/components/theme-toggle.d.ts +14 -0
- package/dist/cjs/types/hooks/index.d.ts +1 -0
- package/dist/cjs/types/hooks/use-hydrated.d.ts +4 -0
- package/dist/cjs/types/index.d.ts +8 -0
- package/dist/cjs/types/lib/index.d.ts +2 -0
- package/dist/cjs/types/lib/preference.d.ts +85 -0
- package/dist/cjs/types/lib/utils.d.ts +1 -0
- package/dist/cjs/types/package.json +1 -0
- package/dist/cjs/types/providers/index.d.ts +2 -0
- package/dist/cjs/types/providers/preference-provider.d.ts +20 -0
- package/dist/cjs/types/providers/theme-provider.d.ts +25 -0
- package/dist/esm/components/button.js +38 -0
- package/dist/esm/components/input.js +9 -0
- package/dist/esm/components/theme-toggle.js +17 -0
- package/dist/esm/hooks/use-hydrated.js +13 -0
- package/dist/esm/index.js +8 -0
- package/dist/esm/lib/preference.js +35 -0
- package/dist/esm/providers/preference-provider.js +124 -0
- package/dist/esm/providers/theme-provider.js +178 -0
- package/dist/styles/body.css +7 -0
- package/dist/styles/font/LICENSE +93 -0
- package/dist/styles/font/files/11944884.woff2 +0 -0
- package/dist/styles/font/files/3f081d19.woff2 +0 -0
- package/dist/styles/font/files/aa4aa22f.woff2 +0 -0
- package/dist/styles/font/files/ba1a9b92.woff2 +0 -0
- package/dist/styles/font/files/eadd8e6c.woff2 +0 -0
- package/dist/styles/font/geist.css +2 -0
- package/dist/styles/index.css +2 -0
- package/dist/types/components/button.d.ts +12 -0
- package/dist/types/components/index.d.ts +3 -0
- package/dist/types/components/input.d.ts +5 -0
- package/dist/types/components/theme-toggle.d.ts +14 -0
- package/dist/types/hooks/index.d.ts +1 -0
- package/dist/types/hooks/use-hydrated.d.ts +4 -0
- package/dist/types/index.d.ts +8 -0
- package/dist/types/lib/index.d.ts +2 -0
- package/dist/types/lib/preference.d.ts +85 -0
- package/dist/types/lib/utils.d.ts +1 -0
- package/dist/types/providers/index.d.ts +2 -0
- package/dist/types/providers/preference-provider.d.ts +20 -0
- package/dist/types/providers/theme-provider.d.ts +25 -0
- package/dist/umd/index.js +3541 -0
- package/package.json +81 -4
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 badnpm
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,194 @@
|
|
|
1
|
-
#
|
|
1
|
+
# UI components
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
React 19 组件库,提供 ESM、CommonJS、UMD 三种产物,以及独立的预编译 CSS。
|
|
4
|
+
|
|
5
|
+
## 安装
|
|
6
|
+
|
|
7
|
+
包发布后,在应用中安装组件库及 React 依赖:
|
|
8
|
+
|
|
9
|
+
```sh
|
|
10
|
+
npm install @badnpm/ui react@^19 react-dom@^19
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
当前包标记为 private,发布前可使用本地 tarball 验证。下文统一使用当前包名 `@badnpm/ui`。
|
|
14
|
+
|
|
15
|
+
## 样式入口
|
|
16
|
+
|
|
17
|
+
三种 JS 格式共用 `dist/styles`,JS 入口不会自动加载 CSS。
|
|
18
|
+
|
|
19
|
+
| 样式入口 | 用途 | 引入方式 |
|
|
20
|
+
| ---------------------------------- | ------------------------------------------- | ---------------------------- |
|
|
21
|
+
| `@badnpm/ui/styles/index.css` | Tailwind 重置、组件工具类、主题和基础色变量 | 使用内置组件样式时引入 |
|
|
22
|
+
| `@badnpm/ui/styles/body.css` | 将 `body` 背景色和文字色设为主题颜色 | 按需引入,与默认样式配合使用 |
|
|
23
|
+
| `@badnpm/ui/styles/font/geist.css` | 加载 Geist 字体,并设置默认无衬线字体 | 按需引入 |
|
|
24
|
+
|
|
25
|
+
仅引入默认样式即可使用组件。页面配色与字体可以分别选择,字体文件随包提供。
|
|
26
|
+
主题和基础色已包含在默认样式中。`@badnpm/ui/globals.css` 是默认样式的兼容入口,使用示例统一采用 `styles/index.css`。
|
|
27
|
+
|
|
28
|
+
使用方无需安装 Tailwind、shadcn、tw-animate-css 或 Geist 字体包。
|
|
29
|
+
应用自行编写的额外 Tailwind 类需要由应用自己的 Tailwind 配置生成。
|
|
30
|
+
|
|
31
|
+
## ESM
|
|
32
|
+
|
|
33
|
+
适用于支持 TSX 和 CSS 导入的应用构建工具。`import` 自动选择 `dist/esm/index.js`。
|
|
34
|
+
以下示例作为浏览器入口使用,HTML 中需提供 `<div id="root"></div>`。
|
|
35
|
+
|
|
36
|
+
```tsx
|
|
37
|
+
import { createRoot } from 'react-dom/client';
|
|
38
|
+
import { Button, ThemeProvider, ThemeToggle } from '@badnpm/ui';
|
|
39
|
+
|
|
40
|
+
// 默认组件样式。
|
|
41
|
+
import '@badnpm/ui/styles/index.css';
|
|
42
|
+
// 可选,让整个页面的背景色、文字色跟随主题。
|
|
43
|
+
import '@badnpm/ui/styles/body.css';
|
|
44
|
+
// 可选,使用 Geist 字体。
|
|
45
|
+
import '@badnpm/ui/styles/font/geist.css';
|
|
46
|
+
|
|
47
|
+
function App() {
|
|
48
|
+
return (
|
|
49
|
+
<ThemeProvider>
|
|
50
|
+
<Button>保存</Button>
|
|
51
|
+
<ThemeToggle />
|
|
52
|
+
</ThemeProvider>
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
createRoot(document.getElementById('root')!).render(<App />);
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
删除可选的 CSS 导入,即可由应用自行决定页面配色和字体。
|
|
60
|
+
`ThemeToggle` 通过 `ThemeProvider` 读取并切换深浅模式,单独使用 `Button`、`Input` 时可直接渲染。
|
|
61
|
+
|
|
62
|
+
## CommonJS
|
|
63
|
+
|
|
64
|
+
`require` 自动选择 `dist/cjs/index.cjs`。以下示例用于支持 CommonJS 且配置了 CSS 处理的浏览器打包入口,HTML 中需提供 `<div id="root"></div>`。
|
|
65
|
+
|
|
66
|
+
```js
|
|
67
|
+
const React = require('react');
|
|
68
|
+
const { createRoot } = require('react-dom/client');
|
|
69
|
+
const { Button, ThemeProvider, ThemeToggle } = require('@badnpm/ui');
|
|
70
|
+
|
|
71
|
+
// 这些 CSS require 由应用的打包器处理。
|
|
72
|
+
require('@badnpm/ui/styles/index.css');
|
|
73
|
+
// 可选页面配色。
|
|
74
|
+
require('@badnpm/ui/styles/body.css');
|
|
75
|
+
// 可选字体。
|
|
76
|
+
require('@badnpm/ui/styles/font/geist.css');
|
|
77
|
+
|
|
78
|
+
const app = React.createElement(
|
|
79
|
+
ThemeProvider,
|
|
80
|
+
null,
|
|
81
|
+
React.createElement(Button, null, '保存'),
|
|
82
|
+
React.createElement(ThemeToggle)
|
|
83
|
+
);
|
|
84
|
+
|
|
85
|
+
createRoot(document.getElementById('root')).render(app);
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
在 Node.js 中进行服务端渲染时,可以直接加载 CJS 组件。CSS 通过浏览器端入口或 HTML 的 `link` 标签加载。
|
|
89
|
+
|
|
90
|
+
```js
|
|
91
|
+
const React = require('react');
|
|
92
|
+
const { renderToString } = require('react-dom/server');
|
|
93
|
+
const { Button } = require('@badnpm/ui');
|
|
94
|
+
|
|
95
|
+
const html = renderToString(React.createElement(Button, null, '保存'));
|
|
96
|
+
console.log(html);
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
## UMD
|
|
100
|
+
|
|
101
|
+
将安装包中的 `dist` 目录放到网站静态目录,例如 `public/vendor/ui`。浏览器可以通过 `/vendor/ui/umd/index.js` 加载组件库,通过 `/vendor/ui/styles` 加载样式和字体。部署时保留整个 `styles` 目录结构,使 CSS 中的字体相对路径保持有效。
|
|
102
|
+
|
|
103
|
+
UMD 导出挂载到 `window.BadUI`。页面先准备 React、ReactDOM 全局对象,再加载组件库。
|
|
104
|
+
React 19 的浏览器加载示例采用 ESM CDN,并将 React 显式共享给 ReactDOM,下面使用 import map 配置依赖地址。 [React 官方说明](https://react.dev/blog/2024/04/25/react-19-upgrade-guide#umd-builds-removed)
|
|
105
|
+
|
|
106
|
+
```html
|
|
107
|
+
<!doctype html>
|
|
108
|
+
<html lang="zh-CN">
|
|
109
|
+
<head>
|
|
110
|
+
<meta charset="UTF-8" />
|
|
111
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
112
|
+
<title>UI 组件示例</title>
|
|
113
|
+
|
|
114
|
+
<!-- 默认组件样式。 -->
|
|
115
|
+
<link rel="stylesheet" href="/vendor/ui/styles/index.css" />
|
|
116
|
+
<!-- 可选,删除此行后由应用自行设置 body 配色。 -->
|
|
117
|
+
<link rel="stylesheet" href="/vendor/ui/styles/body.css" />
|
|
118
|
+
<!-- 可选,删除此行后使用应用自己的字体。 -->
|
|
119
|
+
<link rel="stylesheet" href="/vendor/ui/styles/font/geist.css" />
|
|
120
|
+
|
|
121
|
+
<script type="importmap">
|
|
122
|
+
{
|
|
123
|
+
"imports": {
|
|
124
|
+
"react": "https://esm.sh/react@19",
|
|
125
|
+
"react-dom": "https://esm.sh/react-dom@19?external=react",
|
|
126
|
+
"react-dom/client": "https://esm.sh/react-dom@19/client?external=react,react-dom"
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
</script>
|
|
130
|
+
</head>
|
|
131
|
+
<body>
|
|
132
|
+
<div id="root"></div>
|
|
133
|
+
<script type="module">
|
|
134
|
+
import React from 'react';
|
|
135
|
+
import * as ReactDOM from 'react-dom';
|
|
136
|
+
import { createRoot } from 'react-dom/client';
|
|
137
|
+
|
|
138
|
+
window.React = React;
|
|
139
|
+
window.ReactDOM = ReactDOM;
|
|
140
|
+
|
|
141
|
+
// 等待全局对象设置完成后再加载 UMD 文件。
|
|
142
|
+
await import('/vendor/ui/umd/index.js');
|
|
143
|
+
const { Button, ThemeProvider, ThemeToggle } = window.BadUI;
|
|
144
|
+
|
|
145
|
+
createRoot(document.getElementById('root')).render(
|
|
146
|
+
React.createElement(
|
|
147
|
+
ThemeProvider,
|
|
148
|
+
null,
|
|
149
|
+
React.createElement(Button, null, '保存'),
|
|
150
|
+
React.createElement(ThemeToggle)
|
|
151
|
+
)
|
|
152
|
+
);
|
|
153
|
+
</script>
|
|
154
|
+
</body>
|
|
155
|
+
</html>
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
通过 HTTP 静态服务访问上述页面。示例中的 CDN 地址可替换为应用自行托管的 React 依赖地址。
|
|
159
|
+
UMD 已合并普通运行依赖,React 与 ReactDOM 由页面提供,CSS 通过 `link` 独立加载。
|
|
160
|
+
|
|
161
|
+
## TypeScript
|
|
162
|
+
|
|
163
|
+
ESM 使用 `dist/types/index.d.ts`,CommonJS 使用 `dist/cjs/types/index.d.ts`,包的 `exports` 会根据导入方式选择对应声明。两份声明均按源码目录逐文件生成。
|
|
164
|
+
|
|
165
|
+
TypeScript 项目可以加载构建工具提供的 CSS 类型声明,例如 Vite 的 `vite/client`。
|
|
166
|
+
也可在应用纳入编译范围的声明文件(例如 `src/styles.d.ts`)中统一添加:
|
|
167
|
+
|
|
168
|
+
```ts
|
|
169
|
+
declare module '*.css' {}
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
## 构建与打包
|
|
173
|
+
|
|
174
|
+
在仓库根目录执行:
|
|
175
|
+
|
|
176
|
+
```sh
|
|
177
|
+
pnpm --filter @badnpm/ui build
|
|
178
|
+
npm pack ./packages/ui --pack-destination /tmp
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
构建命令直接运行 `rollup -c`,产物目录如下:
|
|
182
|
+
|
|
183
|
+
```text
|
|
184
|
+
dist/
|
|
185
|
+
├── esm/ # ESM 模块
|
|
186
|
+
├── types/ # 按源码结构生成的 ESM 类型声明
|
|
187
|
+
├── cjs/ # CommonJS 模块,types 子目录提供逐文件类型声明
|
|
188
|
+
├── umd/index.js # 浏览器单文件,导出 window.BadUI
|
|
189
|
+
└── styles/ # 默认样式、可选 body 配色和字体资源
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
TypeScript 插件编译 JS,rollup-plugin-dts 配合 preserveModules 按源码目录逐文件生成类型声明。PostCSS 编译独立 CSS 并处理字体资源,样式规则集中在 `postcss.config.mjs`。
|
|
193
|
+
声明生成阶段会将目录导入和源码路径别名转换为完整相对路径,兼容使用方的 NodeNext 类型解析。
|
|
194
|
+
入口和依赖统一在包根目录的 `package.json` 中维护。工作区 demo 的 Vite 别名指向源码,保留本地热更新。
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var jsxRuntime = require('react/jsx-runtime');
|
|
4
|
+
var button = require('@base-ui/react/button');
|
|
5
|
+
var classVarianceAuthority = require('class-variance-authority');
|
|
6
|
+
var cn = require('cn');
|
|
7
|
+
|
|
8
|
+
const buttonVariants = classVarianceAuthority.cva("group/button inline-flex shrink-0 items-center justify-center rounded-lg border border-transparent bg-clip-padding text-sm font-medium whitespace-nowrap transition-all outline-none select-none focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 active:not-aria-[haspopup]:translate-y-px disabled:pointer-events-none disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4", {
|
|
9
|
+
variants: {
|
|
10
|
+
variant: {
|
|
11
|
+
default: 'bg-primary text-primary-foreground hover:bg-primary/80',
|
|
12
|
+
outline: 'border-border bg-background hover:bg-muted hover:text-foreground aria-expanded:bg-muted aria-expanded:text-foreground dark:border-input dark:bg-input/30 dark:hover:bg-input/50',
|
|
13
|
+
secondary: 'bg-secondary text-secondary-foreground hover:bg-[color-mix(in_oklch,var(--secondary),var(--foreground)_5%)] aria-expanded:bg-secondary aria-expanded:text-secondary-foreground',
|
|
14
|
+
ghost: 'hover:bg-muted hover:text-foreground aria-expanded:bg-muted aria-expanded:text-foreground dark:hover:bg-muted/50',
|
|
15
|
+
destructive: 'bg-destructive/10 text-destructive hover:bg-destructive/20 focus-visible:border-destructive/40 focus-visible:ring-destructive/20 dark:bg-destructive/20 dark:hover:bg-destructive/30 dark:focus-visible:ring-destructive/40',
|
|
16
|
+
link: 'text-primary underline-offset-4 hover:underline',
|
|
17
|
+
},
|
|
18
|
+
size: {
|
|
19
|
+
default: 'h-8 gap-1.5 px-2.5 has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2',
|
|
20
|
+
xs: "h-6 gap-1 rounded-[min(var(--radius-md),10px)] px-2 text-xs in-data-[slot=button-group]:rounded-lg has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&_svg:not([class*='size-'])]:size-3",
|
|
21
|
+
sm: "h-7 gap-1 rounded-[min(var(--radius-md),12px)] px-2.5 text-[0.8rem] in-data-[slot=button-group]:rounded-lg has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&_svg:not([class*='size-'])]:size-3.5",
|
|
22
|
+
lg: 'h-9 gap-1.5 px-2.5 has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2',
|
|
23
|
+
icon: 'size-8',
|
|
24
|
+
'icon-xs': "size-6 rounded-[min(var(--radius-md),10px)] in-data-[slot=button-group]:rounded-lg [&_svg:not([class*='size-'])]:size-3",
|
|
25
|
+
'icon-sm': 'size-7 rounded-[min(var(--radius-md),12px)] in-data-[slot=button-group]:rounded-lg',
|
|
26
|
+
'icon-lg': 'size-9',
|
|
27
|
+
},
|
|
28
|
+
},
|
|
29
|
+
defaultVariants: {
|
|
30
|
+
variant: 'default',
|
|
31
|
+
size: 'default',
|
|
32
|
+
},
|
|
33
|
+
});
|
|
34
|
+
function Button({ className, variant = 'default', size = 'default', ...props }) {
|
|
35
|
+
return (jsxRuntime.jsx(button.Button, { "data-slot": "button", className: typeof className === 'function'
|
|
36
|
+
? (state) => cn.cn(buttonVariants({ variant, size }), className(state))
|
|
37
|
+
: cn.cn(buttonVariants({ variant, size }), className), ...props }));
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
exports.Button = Button;
|
|
41
|
+
exports.buttonVariants = buttonVariants;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var jsxRuntime = require('react/jsx-runtime');
|
|
4
|
+
var input = require('@base-ui/react/input');
|
|
5
|
+
var cn = require('cn');
|
|
6
|
+
|
|
7
|
+
function Input({ className, type, ...props }) {
|
|
8
|
+
return (jsxRuntime.jsx(input.Input, { type: type, "data-slot": "input", className: cn.cn("h-8 w-full min-w-0 rounded-lg border border-input bg-transparent px-2.5 py-1 text-base transition-colors outline-none file:inline-flex file:h-6 file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 disabled:pointer-events-none disabled:cursor-not-allowed disabled:bg-input/50 disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 md:text-sm dark:bg-input/30 dark:disabled:bg-input/80 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40", className), ...props }));
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
exports.Input = Input;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
var jsxRuntime = require('react/jsx-runtime');
|
|
5
|
+
var lucideReact = require('lucide-react');
|
|
6
|
+
var button = require('./button.cjs');
|
|
7
|
+
var themeProvider = require('../providers/theme-provider.cjs');
|
|
8
|
+
|
|
9
|
+
function ThemeToggle({ darkLabel = '切换到深色主题', lightLabel = '切换到浅色主题', variant = 'outline', size = 'icon', title = '切换主题', onClick, ...props }) {
|
|
10
|
+
const { toggleTheme } = themeProvider.useTheme();
|
|
11
|
+
return (jsxRuntime.jsxs(button.Button, { type: "button", variant: variant, size: size, title: title, ...props, onClick: (event) => {
|
|
12
|
+
// 先执行使用方回调,允许通过 preventDefault() 取消主题切换。
|
|
13
|
+
onClick?.(event);
|
|
14
|
+
if (!event.defaultPrevented)
|
|
15
|
+
toggleTheme();
|
|
16
|
+
}, children: [jsxRuntime.jsx(lucideReact.MoonIcon, { "data-icon": "inline-start", "aria-hidden": "true", className: "animate-in duration-200 ease-out zoom-in-75 fade-in spin-in-90 motion-reduce:animate-none dark:hidden" }), jsxRuntime.jsx(lucideReact.SunIcon, { "data-icon": "inline-start", "aria-hidden": "true", className: "hidden animate-in duration-200 ease-out zoom-in-75 fade-in spin-in-90 motion-reduce:animate-none dark:block" }), jsxRuntime.jsx("span", { className: "sr-only dark:hidden", children: darkLabel }), jsxRuntime.jsx("span", { className: "sr-only hidden dark:block", children: lightLabel })] }));
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
exports.ThemeToggle = ThemeToggle;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
var react = require('react');
|
|
5
|
+
|
|
6
|
+
// 水合完成与否由 React 的服务端快照机制区分,无需 effect 中同步设置状态。
|
|
7
|
+
const subscribe = () => () => { };
|
|
8
|
+
const getSnapshot = () => true;
|
|
9
|
+
const getServerSnapshot = () => false;
|
|
10
|
+
/** 服务端及客户端水合首帧返回 false,之后返回 true;纯客户端渲染直接返回 true。 */
|
|
11
|
+
function useHydrated() {
|
|
12
|
+
return react.useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
exports.useHydrated = useHydrated;
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var button = require('./components/button.cjs');
|
|
4
|
+
var input = require('./components/input.cjs');
|
|
5
|
+
var themeToggle = require('./components/theme-toggle.cjs');
|
|
6
|
+
var preferenceProvider = require('./providers/preference-provider.cjs');
|
|
7
|
+
var themeProvider = require('./providers/theme-provider.cjs');
|
|
8
|
+
var useHydrated = require('./hooks/use-hydrated.cjs');
|
|
9
|
+
var preference = require('./lib/preference.cjs');
|
|
10
|
+
var cn = require('cn');
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
exports.Button = button.Button;
|
|
15
|
+
exports.buttonVariants = button.buttonVariants;
|
|
16
|
+
exports.Input = input.Input;
|
|
17
|
+
exports.ThemeToggle = themeToggle.ThemeToggle;
|
|
18
|
+
exports.PreferenceProvider = preferenceProvider.PreferenceProvider;
|
|
19
|
+
exports.usePreference = preferenceProvider.usePreference;
|
|
20
|
+
exports.ThemeProvider = themeProvider.ThemeProvider;
|
|
21
|
+
exports.useTheme = themeProvider.useTheme;
|
|
22
|
+
exports.useHydrated = useHydrated.useHydrated;
|
|
23
|
+
exports.BASE_COLORS = preference.BASE_COLORS;
|
|
24
|
+
exports.COLOR_THEMES = preference.COLOR_THEMES;
|
|
25
|
+
exports.DEFAULT_PREFERENCE = preference.DEFAULT_PREFERENCE;
|
|
26
|
+
exports.RADIUS_OPTIONS = preference.RADIUS_OPTIONS;
|
|
27
|
+
Object.defineProperty(exports, "cn", {
|
|
28
|
+
enumerable: true,
|
|
29
|
+
get: function () { return cn.cn; }
|
|
30
|
+
});
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// color 用于选项色块预览:基础色取浅色模式的 muted-foreground,主题色取 primary。
|
|
4
|
+
const BASE_COLORS = [
|
|
5
|
+
{ value: 'neutral', label: 'Neutral', color: 'oklch(0.556 0 0)' },
|
|
6
|
+
{ value: 'mist', label: 'Mist', color: 'oklch(0.56 0.021 213.5)' },
|
|
7
|
+
{ value: 'zinc', label: 'Zinc', color: 'oklch(0.552 0.016 285.938)' },
|
|
8
|
+
];
|
|
9
|
+
// value 对应 CSS 的 data-theme 属性,label 用于展示,color 是固定的预览色。
|
|
10
|
+
const COLOR_THEMES = [
|
|
11
|
+
{ value: 'neutral', label: 'Neutral', color: 'oklch(0.205 0 0)' },
|
|
12
|
+
{ value: 'blue', label: 'Blue', color: 'oklch(0.488 0.243 264.376)' },
|
|
13
|
+
{ value: 'green', label: 'Green', color: 'oklch(0.527 0.154 150.069)' },
|
|
14
|
+
{ value: 'orange', label: 'Orange', color: 'oklch(0.553 0.195 38.402)' },
|
|
15
|
+
{ value: 'pink', label: 'Pink', color: 'oklch(0.525 0.223 3.958)' },
|
|
16
|
+
{ value: 'purple', label: 'Purple', color: 'oklch(0.496 0.265 301.924)' },
|
|
17
|
+
{ value: 'red', label: 'Red', color: 'oklch(0.505 0.213 27.518)' },
|
|
18
|
+
{ value: 'rose', label: 'Rose', color: 'oklch(0.514 0.222 16.935)' },
|
|
19
|
+
{ value: 'sky', label: 'Sky', color: 'oklch(0.5 0.134 242.749)' },
|
|
20
|
+
{ value: 'yellow', label: 'Yellow', color: 'oklch(0.852 0.199 91.936)' },
|
|
21
|
+
];
|
|
22
|
+
// 圆角预设的 value 单位为 rem,可直接传给 setRadius,也可用于 borderRadius 预览。
|
|
23
|
+
const RADIUS_OPTIONS = [
|
|
24
|
+
{ value: 0, label: '0' },
|
|
25
|
+
{ value: 0.25, label: '0.25' },
|
|
26
|
+
{ value: 0.5, label: '0.5' },
|
|
27
|
+
{ value: 0.625, label: 'Default' },
|
|
28
|
+
{ value: 0.75, label: '0.75' },
|
|
29
|
+
{ value: 1, label: '1' },
|
|
30
|
+
];
|
|
31
|
+
const DEFAULT_PREFERENCE = {
|
|
32
|
+
baseColor: 'neutral',
|
|
33
|
+
theme: 'neutral',
|
|
34
|
+
radius: 0.625,
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
exports.BASE_COLORS = BASE_COLORS;
|
|
38
|
+
exports.COLOR_THEMES = COLOR_THEMES;
|
|
39
|
+
exports.DEFAULT_PREFERENCE = DEFAULT_PREFERENCE;
|
|
40
|
+
exports.RADIUS_OPTIONS = RADIUS_OPTIONS;
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
var jsxRuntime = require('react/jsx-runtime');
|
|
5
|
+
var react = require('react');
|
|
6
|
+
var useHydrated = require('../hooks/use-hydrated.cjs');
|
|
7
|
+
var preference = require('../lib/preference.cjs');
|
|
8
|
+
|
|
9
|
+
const PreferenceContext = react.createContext(undefined);
|
|
10
|
+
// 校验选项标识,并将 unknown 收窄为对应的字符串联合类型。
|
|
11
|
+
function isOption(options, value) {
|
|
12
|
+
return options.some((option) => option.value === value);
|
|
13
|
+
}
|
|
14
|
+
// 存储内容不可信:逐项校验,单个字段无效时不影响其他有效偏好。
|
|
15
|
+
function normalizePreference(value, fallback) {
|
|
16
|
+
const candidate = typeof value === 'object' && value !== null ? value : {};
|
|
17
|
+
return {
|
|
18
|
+
baseColor: isOption(preference.BASE_COLORS, candidate.baseColor) ? candidate.baseColor : fallback.baseColor,
|
|
19
|
+
theme: isOption(preference.COLOR_THEMES, candidate.theme) ? candidate.theme : fallback.theme,
|
|
20
|
+
// 预设用于展示,不限制自定义圆角;只接受有限的非负数。
|
|
21
|
+
radius: typeof candidate.radius === 'number' && Number.isFinite(candidate.radius) && candidate.radius >= 0
|
|
22
|
+
? candidate.radius
|
|
23
|
+
: fallback.radius,
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
// 解析并校验存储字符串;没有记录或 JSON 损坏时回退到默认值。
|
|
27
|
+
function parsePreference(value, fallback) {
|
|
28
|
+
try {
|
|
29
|
+
return normalizePreference(value === null ? null : JSON.parse(value), fallback);
|
|
30
|
+
}
|
|
31
|
+
catch {
|
|
32
|
+
return fallback;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
// 将存储访问与 JSON 解析分开处理,兼容浏览器禁止存储的情况。
|
|
36
|
+
function readPreference(storageKey, fallback) {
|
|
37
|
+
try {
|
|
38
|
+
return parsePreference(localStorage.getItem(storageKey), fallback);
|
|
39
|
+
}
|
|
40
|
+
catch {
|
|
41
|
+
// 存储不可用时,偏好仍可在当前页面内使用。
|
|
42
|
+
return fallback;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
// 比较字段值,避免相同偏好触发重复渲染。
|
|
46
|
+
function isSamePreference(current, next) {
|
|
47
|
+
return current.baseColor === next.baseColor && current.theme === next.theme && current.radius === next.radius;
|
|
48
|
+
}
|
|
49
|
+
// 管理基础色、主题配色和圆角;深浅模式继续由 ThemeProvider 管理。
|
|
50
|
+
function PreferenceProvider({ children, defaultPreference, storageKey = 'bad-ui:preference', }) {
|
|
51
|
+
// 先补齐并校验调用方传入的默认值,再作为存储读取和重置时的回退值。
|
|
52
|
+
const { baseColor, theme, radius } = normalizePreference(defaultPreference, preference.DEFAULT_PREFERENCE);
|
|
53
|
+
const defaults = react.useMemo(() => ({ baseColor, theme, radius }), [baseColor, theme, radius]);
|
|
54
|
+
const hydrated = useHydrated.useHydrated();
|
|
55
|
+
const [storedPreference, setPreference] = react.useState(() => readPreference(storageKey, defaults));
|
|
56
|
+
// 保存最新偏好,保证同一批次连续修改不同字段时保留先前修改。
|
|
57
|
+
const preferenceRef = react.useRef(storedPreference);
|
|
58
|
+
// 服务端及客户端水合首帧使用相同默认值,水合后才向子组件公开本地偏好。
|
|
59
|
+
const preference$1 = hydrated ? storedPreference : defaults;
|
|
60
|
+
// 主动修改时才写入存储;跨标签页同步仅更新内存,避免删除后又写回默认值。
|
|
61
|
+
const updatePreference = react.useCallback((patch) => {
|
|
62
|
+
const current = preferenceRef.current;
|
|
63
|
+
const next = normalizePreference({ ...current, ...patch }, current);
|
|
64
|
+
if (isSamePreference(current, next))
|
|
65
|
+
return;
|
|
66
|
+
preferenceRef.current = next;
|
|
67
|
+
setPreference(next);
|
|
68
|
+
try {
|
|
69
|
+
localStorage.setItem(storageKey, JSON.stringify(next));
|
|
70
|
+
}
|
|
71
|
+
catch {
|
|
72
|
+
// 写入失败不阻断页面偏好更新。
|
|
73
|
+
}
|
|
74
|
+
}, [storageKey]);
|
|
75
|
+
const setBaseColor = react.useCallback((nextBaseColor) => {
|
|
76
|
+
updatePreference({ baseColor: nextBaseColor });
|
|
77
|
+
}, [updatePreference]);
|
|
78
|
+
// 此处 theme 是配色方案(如 blue),并非 light / dark / system。
|
|
79
|
+
const setTheme = react.useCallback((nextTheme) => {
|
|
80
|
+
updatePreference({ theme: nextTheme });
|
|
81
|
+
}, [updatePreference]);
|
|
82
|
+
// 圆角以 rem 保存,转换为 CSS 长度的工作由下方 effect 统一完成。
|
|
83
|
+
const setRadius = react.useCallback((nextRadius) => {
|
|
84
|
+
updatePreference({ radius: nextRadius });
|
|
85
|
+
}, [updatePreference]);
|
|
86
|
+
// 配色通过独立属性组合;圆角以内联变量覆盖主题默认值,不改动深浅模式类名。
|
|
87
|
+
react.useLayoutEffect(() => {
|
|
88
|
+
if (!hydrated)
|
|
89
|
+
return;
|
|
90
|
+
const root = document.documentElement;
|
|
91
|
+
// 在浏览器绘制前更新属性,分别匹配 base-color 和 themes 中的选择器。
|
|
92
|
+
root.dataset.baseColor = preference$1.baseColor;
|
|
93
|
+
root.dataset.theme = preference$1.theme;
|
|
94
|
+
root.style.setProperty('--radius', `${preference$1.radius}rem`);
|
|
95
|
+
}, [preference$1, hydrated]);
|
|
96
|
+
react.useEffect(() => {
|
|
97
|
+
// 接收其他标签页的变更;删除或清空存储时恢复默认偏好。
|
|
98
|
+
function handleStorageChange(event) {
|
|
99
|
+
// 只处理 localStorage 中当前键的变化;clear() 的事件 key 为 null。
|
|
100
|
+
if (event.storageArea !== localStorage || (event.key !== null && event.key !== storageKey)) {
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
const next = parsePreference(event.newValue, defaults);
|
|
104
|
+
if (isSamePreference(preferenceRef.current, next))
|
|
105
|
+
return;
|
|
106
|
+
preferenceRef.current = next;
|
|
107
|
+
setPreference(next);
|
|
108
|
+
}
|
|
109
|
+
window.addEventListener('storage', handleStorageChange);
|
|
110
|
+
// 卸载或依赖变化时注销监听,避免重复订阅。
|
|
111
|
+
return () => window.removeEventListener('storage', handleStorageChange);
|
|
112
|
+
}, [defaults, storageKey]);
|
|
113
|
+
// 保持 Context 值的引用稳定,避免父组件重渲染引起无关的订阅更新。
|
|
114
|
+
const value = react.useMemo(() => ({ ...preference$1, setBaseColor, setTheme, setRadius }), [preference$1, setBaseColor, setTheme, setRadius]);
|
|
115
|
+
return jsxRuntime.jsx(PreferenceContext.Provider, { value: value, children: children });
|
|
116
|
+
}
|
|
117
|
+
// 统一读取偏好上下文,在缺少 Provider 时给出明确的使用错误。
|
|
118
|
+
function usePreference() {
|
|
119
|
+
const context = react.useContext(PreferenceContext);
|
|
120
|
+
if (context === undefined) {
|
|
121
|
+
throw new Error('usePreference must be used within a PreferenceProvider');
|
|
122
|
+
}
|
|
123
|
+
return context;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
exports.PreferenceProvider = PreferenceProvider;
|
|
127
|
+
exports.usePreference = usePreference;
|