@hamster-note/components 0.2.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 +411 -0
- package/dist/badge-BxBwSGqN.js +16 -0
- package/dist/badge.d.ts +2 -0
- package/dist/badge.js +2 -0
- package/dist/button-DRz0VTkf.js +18 -0
- package/dist/button.d.ts +2 -0
- package/dist/button.js +2 -0
- package/dist/components/badge/Badge.d.ts +7 -0
- package/dist/components/badge/index.d.ts +2 -0
- package/dist/components/button/Button.d.ts +9 -0
- package/dist/components/button/index.d.ts +2 -0
- package/dist/components/confirm/Confirm.d.ts +24 -0
- package/dist/components/confirm/confirm.d.ts +3 -0
- package/dist/components/confirm/index.d.ts +3 -0
- package/dist/components/dialog/Dialog.d.ts +11 -0
- package/dist/components/dialog/index.d.ts +2 -0
- package/dist/components/dialog/use-modal.d.ts +22 -0
- package/dist/components/drawer/Drawer.d.ts +14 -0
- package/dist/components/drawer/index.d.ts +2 -0
- package/dist/components/icon/Icon.d.ts +28 -0
- package/dist/components/icon/icon-names.d.ts +15 -0
- package/dist/components/icon/index.d.ts +4 -0
- package/dist/components/kbd/Kbd.d.ts +6 -0
- package/dist/components/kbd/index.d.ts +2 -0
- package/dist/components/loading/Loading.d.ts +12 -0
- package/dist/components/loading/index.d.ts +3 -0
- package/dist/components/loading/loading.d.ts +3 -0
- package/dist/components/menu/Menu.d.ts +25 -0
- package/dist/components/menu/index.d.ts +2 -0
- package/dist/components/note-card/NoteCard.d.ts +9 -0
- package/dist/components/note-card/index.d.ts +2 -0
- package/dist/components/popover/Popover.d.ts +21 -0
- package/dist/components/popover/index.d.ts +2 -0
- package/dist/components/popover/use-anchor-position.d.ts +14 -0
- package/dist/components/text-field/TextField.d.ts +7 -0
- package/dist/components/text-field/index.d.ts +2 -0
- package/dist/components/theme/ThemeProvider.d.ts +77 -0
- package/dist/components/theme/index.d.ts +4 -0
- package/dist/components/theme/theme.d.ts +87 -0
- package/dist/confirm-3eBmopl8.js +109 -0
- package/dist/confirm.d.ts +2 -0
- package/dist/confirm.js +2 -0
- package/dist/dialog-B4xS5zlK.js +53 -0
- package/dist/dialog.d.ts +2 -0
- package/dist/dialog.js +2 -0
- package/dist/drawer-DoeuGAHD.js +60 -0
- package/dist/drawer.d.ts +2 -0
- package/dist/drawer.js +2 -0
- package/dist/hamster-note-components.css +2 -0
- package/dist/icon-C40ONpMt.js +451 -0
- package/dist/icon.d.ts +2 -0
- package/dist/icon.js +2 -0
- package/dist/index.d.ts +26 -0
- package/dist/index.js +14 -0
- package/dist/kbd-D9XXd3qf.js +39 -0
- package/dist/kbd.d.ts +2 -0
- package/dist/kbd.js +2 -0
- package/dist/loading-BpykjzMR.js +88 -0
- package/dist/loading.d.ts +2 -0
- package/dist/loading.js +2 -0
- package/dist/menu-B6uuZFif.js +142 -0
- package/dist/menu.d.ts +2 -0
- package/dist/menu.js +2 -0
- package/dist/note-card-Dm-WbNTO.js +50 -0
- package/dist/note-card.d.ts +2 -0
- package/dist/note-card.js +2 -0
- package/dist/popover-Qg_fA2A_.js +165 -0
- package/dist/popover.d.ts +2 -0
- package/dist/popover.js +2 -0
- package/dist/text-field-B8Scu04G.js +31 -0
- package/dist/text-field.d.ts +2 -0
- package/dist/text-field.js +2 -0
- package/dist/theme-C0p_hpKk.js +67 -0
- package/dist/theme.d.ts +2 -0
- package/dist/theme.js +2 -0
- package/dist/use-modal-K9L8aPS0.js +82 -0
- package/package.json +111 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 仓鼠笔记
|
|
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
ADDED
|
@@ -0,0 +1,411 @@
|
|
|
1
|
+
# @hamster-note/components
|
|
2
|
+
|
|
3
|
+
HamsterNote 的 React 19 基础组件库,使用 TypeScript 6 与 Vite 8 构建。
|
|
4
|
+
|
|
5
|
+
## 开发
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
yarn install
|
|
9
|
+
yarn dev
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
Demo 默认运行在 `http://localhost:9810`,并监听 `0.0.0.0`,局域网设备可通过开发机 IP
|
|
13
|
+
访问。
|
|
14
|
+
|
|
15
|
+
## 使用
|
|
16
|
+
|
|
17
|
+
先引入组件样式,再从根入口或独立组件入口导入:
|
|
18
|
+
|
|
19
|
+
```tsx
|
|
20
|
+
import { Button, Popover, PopoverSeparator, TextField } from '@hamster-note/components';
|
|
21
|
+
import '@hamster-note/components/styles.css';
|
|
22
|
+
|
|
23
|
+
export function Example() {
|
|
24
|
+
return (
|
|
25
|
+
<form>
|
|
26
|
+
<TextField label="笔记标题" placeholder="输入标题" />
|
|
27
|
+
<Button type="submit" variant="primary">
|
|
28
|
+
保存笔记
|
|
29
|
+
</Button>
|
|
30
|
+
</form>
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
Popover 只负责可复用表面和主题,定位、显示状态、外部点击关闭与焦点返回由使用方根据
|
|
36
|
+
`toolbar`、`menu` 或帮助面板等具体模式控制。外框圆角与内嵌 `Button` 的圆角同心衔接
|
|
37
|
+
(外框半径 = `--hn-radius-md` + 外框 padding):
|
|
38
|
+
|
|
39
|
+
```tsx
|
|
40
|
+
<Popover aria-label="文字操作" role="toolbar" theme="light">
|
|
41
|
+
<Button size="small" variant="ghost">
|
|
42
|
+
加粗
|
|
43
|
+
</Button>
|
|
44
|
+
<PopoverSeparator />
|
|
45
|
+
<Button size="small" variant="ghost">
|
|
46
|
+
添加链接
|
|
47
|
+
</Button>
|
|
48
|
+
</Popover>
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
竖排工具条:传入 `orientation="vertical"` 后浮层改为纵向排列,`PopoverSeparator` 会
|
|
52
|
+
自动变为水平分隔线并把 `aria-orientation` 同步为 `horizontal`:
|
|
53
|
+
|
|
54
|
+
```tsx
|
|
55
|
+
<Popover aria-label="侧边工具条" orientation="vertical" role="toolbar">
|
|
56
|
+
<Button size="small" variant="ghost">
|
|
57
|
+
加粗
|
|
58
|
+
</Button>
|
|
59
|
+
<Button size="small" variant="ghost">
|
|
60
|
+
斜体
|
|
61
|
+
</Button>
|
|
62
|
+
<PopoverSeparator />
|
|
63
|
+
<Button size="small" variant="ghost">
|
|
64
|
+
链接
|
|
65
|
+
</Button>
|
|
66
|
+
</Popover>
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
独立贴边使用:传入 `edge` 后浮层以 `position: fixed` 贴在视口对应边缘,`edgeOffset`
|
|
70
|
+
控制与边缘的距离(px,默认 16)。不传 `edge` 时行为与定位完全由使用方控制,不会注入
|
|
71
|
+
任何定位样式:
|
|
72
|
+
|
|
73
|
+
```tsx
|
|
74
|
+
<Popover aria-label="贴边操作" edge="top" edgeOffset={24} role="toolbar">
|
|
75
|
+
<Button size="small" variant="ghost">
|
|
76
|
+
操作
|
|
77
|
+
</Button>
|
|
78
|
+
</Popover>
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
锚点定位:传入 `anchor` 后浮层通过 Portal 渲染到 `document.body` 下,不再受祖先
|
|
82
|
+
`overflow` / `transform` / 层叠上下文的裁剪与遮挡影响。浮层以锚点为基准按 `placement`
|
|
83
|
+
(默认 `bottom-start`,另有 `top-start` / `top-end` / `bottom-end` / `left-start` /
|
|
84
|
+
`right-start`)展开;期望方向溢出视口时自动沿主轴翻转,翻转后仍放不下则 clamp 到
|
|
85
|
+
`viewportMargin`(默认 8px)的安全距离内,并跟随滚动 / 缩放 / 尺寸变化重新定位。
|
|
86
|
+
`anchorOffset`(默认 6)控制与锚点的间距。显示状态、外部点击关闭与焦点返回仍由使用方
|
|
87
|
+
控制(浮层在 body 下,外部点击需在 document 上监听):
|
|
88
|
+
|
|
89
|
+
```tsx
|
|
90
|
+
const [open, setOpen] = useState(false);
|
|
91
|
+
const [anchor, setAnchor] = useState<HTMLElement | null>(null);
|
|
92
|
+
|
|
93
|
+
<Button
|
|
94
|
+
aria-expanded={open}
|
|
95
|
+
aria-haspopup="menu"
|
|
96
|
+
onClick={(event) => {
|
|
97
|
+
setAnchor(event.currentTarget);
|
|
98
|
+
setOpen((value) => !value);
|
|
99
|
+
}}
|
|
100
|
+
>
|
|
101
|
+
编辑
|
|
102
|
+
</Button>;
|
|
103
|
+
{
|
|
104
|
+
open ? (
|
|
105
|
+
<Popover anchor={anchor} placement="bottom-start">
|
|
106
|
+
<Menu>
|
|
107
|
+
<MenuItem shortcut="⌘X">剪切</MenuItem>
|
|
108
|
+
<MenuItem shortcut="⌘C">复制</MenuItem>
|
|
109
|
+
</Menu>
|
|
110
|
+
</Popover>
|
|
111
|
+
) : null;
|
|
112
|
+
}
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
`Menu` 提供语义菜单列表,可独立渲染,也可嵌入 `Popover` 当作下拉菜单使用。`MenuItem`
|
|
116
|
+
支持 `shortcut` 显示快捷键、`tone="danger"` 标记破坏性操作;`MenuLabel` 用于分组小标题,
|
|
117
|
+
`MenuSeparator` 用于分组分隔线:
|
|
118
|
+
|
|
119
|
+
```tsx
|
|
120
|
+
import { Menu, MenuItem, MenuLabel, MenuSeparator } from '@hamster-note/components';
|
|
121
|
+
|
|
122
|
+
<Menu aria-label="文件操作">
|
|
123
|
+
<MenuLabel>文件</MenuLabel>
|
|
124
|
+
<MenuItem shortcut="⌘N">新建笔记</MenuItem>
|
|
125
|
+
<MenuItem shortcut="F2">重命名</MenuItem>
|
|
126
|
+
<MenuItem disabled>移动到…</MenuItem>
|
|
127
|
+
<MenuSeparator />
|
|
128
|
+
<MenuItem tone="danger">删除</MenuItem>
|
|
129
|
+
</Menu>;
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
嵌入 `Popover` 当作下拉菜单:`Menu` 复用全局颜色 token,自动继承 `Popover` 的 dark/light
|
|
133
|
+
主题,无需任何主题 prop。触发按钮需要设置 `aria-haspopup="menu"` 并用 `aria-controls` /
|
|
134
|
+
`aria-expanded` 连接浮层(显示状态由使用方控制,与 `Popover` 哲学一致)。下拉菜单推荐
|
|
135
|
+
配合 `anchor` 使用(见上文锚点定位),让菜单渲染到 body 下并自动躲避视口边缘:
|
|
136
|
+
|
|
137
|
+
```tsx
|
|
138
|
+
<Button aria-controls="file-menu" aria-expanded={open} aria-haspopup="menu" onClick={toggle}>
|
|
139
|
+
编辑
|
|
140
|
+
</Button>;
|
|
141
|
+
{
|
|
142
|
+
open ? (
|
|
143
|
+
<Popover anchor={anchorEl} aria-label="编辑操作" id="file-menu">
|
|
144
|
+
<Menu>
|
|
145
|
+
<MenuItem shortcut="⌘X">剪切</MenuItem>
|
|
146
|
+
<MenuItem shortcut="⌘C">复制</MenuItem>
|
|
147
|
+
<MenuSeparator />
|
|
148
|
+
<MenuItem tone="danger">清空选中</MenuItem>
|
|
149
|
+
</Menu>
|
|
150
|
+
</Popover>
|
|
151
|
+
) : null;
|
|
152
|
+
}
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
`MenuSubmenu` 提供嵌套子菜单能力:trigger 复用 `MenuItem` 的视觉,右侧带 ▸ chevron;panel
|
|
156
|
+
复用 `Popover` 作为表面(自动继承 dark/light 主题,与 `Menu` 同样的复用哲学),内部再嵌一层
|
|
157
|
+
`Menu` 承载子项。panel 经 Portal 渲染到 `document.body` 下,默认向 trigger 右侧展开,
|
|
158
|
+
右侧空间不足时自动向左翻转,垂直方向 clamp 在视口安全距离内。鼠标悬停展开(带 150ms 开 /
|
|
159
|
+
200ms 关延迟,避免移动缝隙误关),点击 trigger 切换(触屏 fallback);键盘上
|
|
160
|
+
`ArrowRight`/`Enter`/`Space` 进入子菜单并把焦点送到第一个
|
|
161
|
+
`menuitem`,`ArrowLeft`/`Escape` 关闭并回焦到 trigger。`label` 为字符串时自动作为子菜单的
|
|
162
|
+
`aria-label`,非字符串时可通过 `aria-label` prop 覆盖。`disabled` 时阻止一切展开:
|
|
163
|
+
|
|
164
|
+
```tsx
|
|
165
|
+
import { Menu, MenuItem, MenuSeparator, MenuSubmenu } from '@hamster-note/components';
|
|
166
|
+
|
|
167
|
+
<Menu aria-label="笔记整理">
|
|
168
|
+
<MenuItem>置顶</MenuItem>
|
|
169
|
+
<MenuSubmenu label="移动到…">
|
|
170
|
+
<MenuItem>收件箱</MenuItem>
|
|
171
|
+
<MenuItem>产品 / 研究</MenuItem>
|
|
172
|
+
<MenuItem>产品 / 规划</MenuItem>
|
|
173
|
+
<MenuSeparator />
|
|
174
|
+
<MenuItem>归档</MenuItem>
|
|
175
|
+
</MenuSubmenu>
|
|
176
|
+
<MenuSubmenu disabled label="添加标签">
|
|
177
|
+
<MenuItem>待处理</MenuItem>
|
|
178
|
+
<MenuItem>进行中</MenuItem>
|
|
179
|
+
</MenuSubmenu>
|
|
180
|
+
<MenuSeparator />
|
|
181
|
+
<MenuItem tone="danger">删除</MenuItem>
|
|
182
|
+
</Menu>;
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
已知限制:与现有 `Menu` 一致,不实现完整的 roving-tabindex
|
|
186
|
+
箭头导航——焦点进入子菜单后由 `Tab`/`Shift+Tab` 顺序聚焦
|
|
187
|
+
各个 `menuitem`。
|
|
188
|
+
|
|
189
|
+
也可以使用支持 tree-shaking 的独立入口:
|
|
190
|
+
|
|
191
|
+
```tsx
|
|
192
|
+
import { Button } from '@hamster-note/components/button';
|
|
193
|
+
import '@hamster-note/components/styles.css';
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
`Kbd` 是纯展示型快捷键键帽组件,无交互、无 `role=button`,适合在文案、菜单项、帮助面板
|
|
197
|
+
等场景渲染键帽。`keys` 数组会渲染组合键序列(键帽之间用不带键帽样式的小号 `+` 连接符
|
|
198
|
+
拼接);`children` 渲染单个键帽。`keys` 与 `children` 二选一,`keys` 优先。颜色一律引用
|
|
199
|
+
全局 token,嵌入 `Popover` 时自动继承主题覆盖(与 `Menu` 同样的复用哲学):
|
|
200
|
+
|
|
201
|
+
```tsx
|
|
202
|
+
import { Kbd, Menu, MenuItem } from '@hamster-note/components';
|
|
203
|
+
|
|
204
|
+
// 单个键帽
|
|
205
|
+
<Kbd>⌘</Kbd>
|
|
206
|
+
<Kbd>F2</Kbd>
|
|
207
|
+
|
|
208
|
+
// 组合键
|
|
209
|
+
<Kbd keys={['⌘', 'K']} />
|
|
210
|
+
<Kbd keys={['⌘', '⇧', 'P']} />
|
|
211
|
+
<Kbd keys={['Ctrl', 'Alt', 'Delete']} />
|
|
212
|
+
|
|
213
|
+
// 嵌入 MenuItem 的 shortcut
|
|
214
|
+
<Menu>
|
|
215
|
+
<MenuItem shortcut={<Kbd keys={['⌘', 'N']} />}>新建笔记</MenuItem>
|
|
216
|
+
<MenuItem shortcut={<Kbd>F2</Kbd>}>重命名</MenuItem>
|
|
217
|
+
</Menu>
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
`Dialog` 是受控模态对话框,经 Portal 渲染到 `document.body` 下,焦点循环、滚动锁、Esc /
|
|
221
|
+
背景点击关闭与出入场动画(opacity + 小幅 translateY,180ms transform / 140ms opacity,
|
|
222
|
+
尊重 `prefers-reduced-motion`)全部内置;`open` 与 `onClose` 由使用方控制,组件不维护内部
|
|
223
|
+
开合状态。传入 `title` / `description` 时自动通过 `aria-labelledby` /
|
|
224
|
+
`aria-describedby` 连接到面板;未传 `title` 时使用方需通过 `aria-label` 命名。
|
|
225
|
+
`closeOnEsc` / `closeOnBackdrop` 可分别关闭对应退出通道。z-index 1100,高于 Popover 的
|
|
226
|
+
1000:
|
|
227
|
+
|
|
228
|
+
```tsx
|
|
229
|
+
import { Button, Dialog } from '@hamster-note/components';
|
|
230
|
+
|
|
231
|
+
const [open, setOpen] = useState(false);
|
|
232
|
+
|
|
233
|
+
<Button onClick={() => setOpen(true)}>打开对话框</Button>;
|
|
234
|
+
<Dialog
|
|
235
|
+
description="确认把这篇笔记移动到选定的笔记本?移动后原位置将不再保留副本。"
|
|
236
|
+
onClose={() => setOpen(false)}
|
|
237
|
+
open={open}
|
|
238
|
+
title="移动笔记"
|
|
239
|
+
>
|
|
240
|
+
<Button onClick={() => setOpen(false)} variant="primary">
|
|
241
|
+
移动
|
|
242
|
+
</Button>
|
|
243
|
+
</Dialog>;
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
`Drawer` 是边缘贴附的模态抽屉,与 `Dialog` 共享同一套模态语义(受控 `open` / `onClose`、
|
|
247
|
+
Portal 到 body、焦点循环、滚动锁、Esc / 背景点击关闭、出入场动画)。`placement` 控制贴靠
|
|
248
|
+
边缘与滑入方向(`left` / `right` / `top` / `bottom`,默认 `right`);左右抽屉默认宽
|
|
249
|
+
360px、上下默认高 280px,可用 `size`(px 数字)覆盖。圆角只在远离边缘的两角做处理,制造
|
|
250
|
+
「从边缘滑出」的视觉。z-index 1200,高于 Dialog 的 1100,让抽屉内打开 Dialog 时 Dialog
|
|
251
|
+
仍可见,而 Dialog 内打开 Drawer 时 Drawer 压在 Dialog 之上:
|
|
252
|
+
|
|
253
|
+
```tsx
|
|
254
|
+
import { Button, Drawer } from '@hamster-note/components';
|
|
255
|
+
|
|
256
|
+
const [open, setOpen] = useState(false);
|
|
257
|
+
|
|
258
|
+
<Button onClick={() => setOpen(true)}>打开抽屉</Button>;
|
|
259
|
+
<Drawer
|
|
260
|
+
description="右侧滑入的详情面板"
|
|
261
|
+
onClose={() => setOpen(false)}
|
|
262
|
+
open={open}
|
|
263
|
+
placement="right"
|
|
264
|
+
size={420}
|
|
265
|
+
title="笔记详情"
|
|
266
|
+
>
|
|
267
|
+
<p>内容区可滚动,承载长表单或详情。</p>
|
|
268
|
+
</Drawer>;
|
|
269
|
+
```
|
|
270
|
+
|
|
271
|
+
`Confirm` 是确认对话框,基于 `Dialog` 表面叠加固定 footer(ghost 取消 + primary 确认),
|
|
272
|
+
提供三种等价形态共用同一展示核心:
|
|
273
|
+
|
|
274
|
+
- `<Confirm>`:受控组件,使用方持有 `open` 并处理 `onConfirm` / `onCancel`;
|
|
275
|
+
- `<ConfirmProvider>` + `useConfirm()`:Provider 在子树外包裹一次,hook 返回
|
|
276
|
+
`confirm(options): Promise<boolean>`(确认 `true` / 取消 `false`),并发采用 latest-wins
|
|
277
|
+
(新请求覆盖旧请求并 resolve `false`);
|
|
278
|
+
- 纯函数 `confirm(options): Promise<boolean>`:在 Provider 树外或非组件代码中调用,内部
|
|
279
|
+
惰性创建容器 + `createRoot`,settle 后卸载清理;SSR 环境下直接 `resolve(false)` 并打
|
|
280
|
+
`console.warn`。
|
|
281
|
+
|
|
282
|
+
`tone="danger"` 通过 `data-tone='danger'` 挂在 footer 上,CSS 局部覆盖确认按钮背景为
|
|
283
|
+
`--hn-color-danger`,不修改 `Button`。`loading` 禁用两个按钮并把确认按钮文案改为「处理中…」:
|
|
284
|
+
|
|
285
|
+
```tsx
|
|
286
|
+
import { Confirm, ConfirmProvider, confirm, useConfirm } from '@hamster-note/components';
|
|
287
|
+
|
|
288
|
+
// 形态一:受控组件
|
|
289
|
+
const [open, setOpen] = useState(false);
|
|
290
|
+
<Confirm
|
|
291
|
+
description="删除后无法恢复,该操作不可撤销。"
|
|
292
|
+
confirmText="删除"
|
|
293
|
+
onCancel={() => setOpen(false)}
|
|
294
|
+
onConfirm={() => {
|
|
295
|
+
setOpen(false);
|
|
296
|
+
/* 执行删除 */
|
|
297
|
+
}}
|
|
298
|
+
open={open}
|
|
299
|
+
title="删除笔记?"
|
|
300
|
+
tone="danger"
|
|
301
|
+
/>;
|
|
302
|
+
|
|
303
|
+
// 形态二:Provider + hook
|
|
304
|
+
<ConfirmProvider>
|
|
305
|
+
<Inner />
|
|
306
|
+
</ConfirmProvider>;
|
|
307
|
+
const confirmFn = useConfirm();
|
|
308
|
+
const ok = await confirmFn({
|
|
309
|
+
title: '保存修改?',
|
|
310
|
+
description: '未保存的改动将丢失。',
|
|
311
|
+
confirmText: '保存',
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
// 形态三:纯函数(无 Provider 也能用)
|
|
315
|
+
const ok2 = await confirm({
|
|
316
|
+
title: '退出登录?',
|
|
317
|
+
description: '需要重新登录后才能继续操作。',
|
|
318
|
+
confirmText: '退出',
|
|
319
|
+
tone: 'danger',
|
|
320
|
+
});
|
|
321
|
+
```
|
|
322
|
+
|
|
323
|
+
`ThemeProvider` 是主题色(accent)与明暗模式(mode)切换的包裹层。组件库所有组件只引用
|
|
324
|
+
全局 token(`--hn-color-accent` / `--hn-color-accent-hover` / `--hn-focus-ring` 以及
|
|
325
|
+
`--hn-color-surface` / `--hn-color-text` / `--hn-color-border` 等),ThemeProvider 通过在
|
|
326
|
+
包裹层重定义同名 token、经 CSS 级联让子树自动继承,**零侵入**实现主题切换
|
|
327
|
+
(与 Popover 的 dark/light 同范式)。`.hn-theme` 用 `display: contents` 让包裹 div
|
|
328
|
+
在盒模型上消失,不额外产生一层盒子、不打断外层 flex/grid 布局。
|
|
329
|
+
|
|
330
|
+
`accent` 与 `mode` 是两个正交轴:accent 重定义 accent 三件套,mode 重定义全套表面与文字
|
|
331
|
+
token,两者在同一包裹层上共存(`data-accent` + `data-mode` + 内联 accent style 互不冲突)。
|
|
332
|
+
|
|
333
|
+
`accent` 接受两种来源:预设名(`violet` / `blue` / `teal` / `orange` / `pink`,从
|
|
334
|
+
`THEME_ACCENTS` 读取精确的 accent / accentHover / focusRing 三件套内联下发)或任意 CSS
|
|
335
|
+
颜色字符串(只下发主色,hover 与 focusRing 由 CSS `color-mix` 实时派生)。默认 `violet`,
|
|
336
|
+
与 `tokens.css` 的默认值一致,不传时视觉无变化。
|
|
337
|
+
|
|
338
|
+
`mode` 接受 `'dark'`(默认)或 `'light'`,通过 `data-mode` 属性让 theme.css 重定义全套
|
|
339
|
+
表面/文字/边框 token。默认 `dark` 与 `tokens.css` 的 :root 默认值一致,不传时视觉无变化。
|
|
340
|
+
支持嵌套:内层 ThemeProvider 经级联自然覆盖外层,accent 与 mode 同时生效:
|
|
341
|
+
|
|
342
|
+
```tsx
|
|
343
|
+
import { Button, ThemeProvider } from '@hamster-note/components';
|
|
344
|
+
|
|
345
|
+
// 预设主题色
|
|
346
|
+
<ThemeProvider accent="blue">
|
|
347
|
+
<Button variant="primary">我是蓝色</Button>
|
|
348
|
+
</ThemeProvider>
|
|
349
|
+
|
|
350
|
+
// 自定义颜色(hover 与 focusRing 自动派生)
|
|
351
|
+
<ThemeProvider accent="#ff8a3d">
|
|
352
|
+
<Button variant="primary">我是自定义橙色</Button>
|
|
353
|
+
</ThemeProvider>
|
|
354
|
+
|
|
355
|
+
// 明暗模式:mode 与 accent 正交组合
|
|
356
|
+
<ThemeProvider mode="light" accent="blue">
|
|
357
|
+
<Button variant="primary">浅色背景 + 蓝色 accent</Button>
|
|
358
|
+
</ThemeProvider>
|
|
359
|
+
|
|
360
|
+
// 嵌套:内层覆盖外层(accent 与 mode 同时生效)
|
|
361
|
+
<ThemeProvider mode="light" accent="blue">
|
|
362
|
+
<Button variant="primary">浅色 + 蓝色</Button>
|
|
363
|
+
<ThemeProvider mode="dark" accent="teal">
|
|
364
|
+
<Button variant="primary">深色 + 青绿色</Button>
|
|
365
|
+
</ThemeProvider>
|
|
366
|
+
</ThemeProvider>
|
|
367
|
+
```
|
|
368
|
+
|
|
369
|
+
页面级主题:ThemeProvider 用 `display: contents` 不产生盒子,因此页面背景与文字色需要
|
|
370
|
+
使用方在 app 根上应用 `background: var(--hn-color-surface); color: var(--hn-color-text);`,
|
|
371
|
+
让根 token 经级联铺满整页。
|
|
372
|
+
|
|
373
|
+
## 命令
|
|
374
|
+
|
|
375
|
+
- `yarn dev`:启动 9810 端口的 Vite Demo。
|
|
376
|
+
- `yarn build`:构建 npm 组件库到 `dist/`。
|
|
377
|
+
- `yarn build:demo`:构建静态 Demo 到 `demo-dist/`。
|
|
378
|
+
- `yarn run check`:执行 TypeScript、ESLint 与 Prettier 门禁。
|
|
379
|
+
- `yarn preview`:在 9810 端口预览生产版 Demo。
|
|
380
|
+
|
|
381
|
+
## 组件
|
|
382
|
+
|
|
383
|
+
- `Button`:primary、secondary、ghost 三种层级与三种尺寸。
|
|
384
|
+
- `Badge`:neutral、accent、success、warning、danger 五种语义状态。
|
|
385
|
+
- `TextField`:持久标签、辅助信息、错误状态与完整原生 input 属性。
|
|
386
|
+
- `NoteCard`:静态语义文章或带选择状态的交互卡片。
|
|
387
|
+
- `Popover`:支持 `dark`(默认)与 `light` 主题的通用浮层表面,搭配语义化
|
|
388
|
+
`PopoverSeparator` 分组内容;`anchor` 模式下经 Portal 渲染到 body 并自动躲避视口边缘。
|
|
389
|
+
- `Menu`:`role="menu"` 语义菜单列表,含 `MenuItem`(支持 `shortcut` 与 `danger` tone)、
|
|
390
|
+
`MenuLabel`、`MenuSeparator` 与 `MenuSubmenu`(嵌套子菜单,panel 渲染到 body 下、
|
|
391
|
+
右侧不足自动左翻,复用 `Popover` 作为表面自动继承主题),可独立使用或嵌入 `Popover`
|
|
392
|
+
自动继承主题。
|
|
393
|
+
- `Kbd`:纯展示型快捷键键帽组件。`keys` 渲染组合键序列(键帽间用小号 `+` 连接符拼接),
|
|
394
|
+
`children` 渲染单个键帽;颜色一律引用全局 token,嵌入 `Popover` 自动继承主题。
|
|
395
|
+
- `Dialog`:受控模态对话框。Portal 渲染到 `document.body`,内置焦点循环、滚动锁、Esc /
|
|
396
|
+
背景点击关闭与出入场动画(尊重 `prefers-reduced-motion`);`open` / `onClose` 由使用方
|
|
397
|
+
控制,`title` / `description` 经 `aria-labelledby` / `aria-describedby` 连接面板。
|
|
398
|
+
- `Drawer`:边缘贴附模态抽屉。`placement` 支持 `left` / `right` / `top` / `bottom`
|
|
399
|
+
(默认 `right`),`size` 覆盖默认尺寸(左右 360 / 上下 280);与 `Dialog` 共享模态语义,
|
|
400
|
+
圆角只在远离边缘的两角做处理,z-index 1200 高于 Dialog 以支持嵌套组合。
|
|
401
|
+
- `Confirm`:确认对话框,基于 `Dialog` 叠加固定 footer。提供三种等价形态:`<Confirm>`
|
|
402
|
+
受控组件、`<ConfirmProvider>` + `useConfirm()` hook(返回 `Promise<boolean>`,并发
|
|
403
|
+
latest-wins)、纯函数 `confirm(options): Promise<boolean>`(自挂 React root,SSR 直接
|
|
404
|
+
`resolve(false)`);`tone="danger"` 通过 `data-tone` 局部覆盖确认按钮色,不修改 Button。
|
|
405
|
+
- `ThemeProvider`:主题色(accent)与明暗模式(mode)切换包裹层。accent 通过重定义
|
|
406
|
+
`--hn-color-accent` / `--hn-color-accent-hover` / `--hn-focus-ring` 三个 token、mode 通过
|
|
407
|
+
`data-mode` 重定义全套表面/文字/边框 token,经 CSS 级联让子树自动继承,零侵入实现主题
|
|
408
|
+
切换(与 Popover 的 dark/light 同范式)。`accent` 接受预设名(`violet` / `blue` / `teal` /
|
|
409
|
+
`orange` / `pink`,精确下发三件套)或任意 CSS 颜色字符串(hover 与 focusRing 由
|
|
410
|
+
`color-mix` 派生);`mode` 接受 `'dark'`(默认)/ `'light'`,两轴正交可组合;
|
|
411
|
+
`.hn-theme` 用 `display: contents` 不产生额外盒子,支持嵌套覆盖。
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/* empty css */
|
|
2
|
+
import { jsx as e } from "react/jsx-runtime";
|
|
3
|
+
//#region src/components/badge/Badge.tsx
|
|
4
|
+
function t({ children: t, className: n, tone: r = "neutral", ...i }) {
|
|
5
|
+
return /* @__PURE__ */ e("span", {
|
|
6
|
+
className: [
|
|
7
|
+
"hn-badge",
|
|
8
|
+
`hn-badge--${r}`,
|
|
9
|
+
n
|
|
10
|
+
].filter(Boolean).join(" "),
|
|
11
|
+
...i,
|
|
12
|
+
children: t
|
|
13
|
+
});
|
|
14
|
+
}
|
|
15
|
+
//#endregion
|
|
16
|
+
export { t };
|
package/dist/badge.d.ts
ADDED
package/dist/badge.js
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/* empty css */
|
|
2
|
+
import { jsx as e } from "react/jsx-runtime";
|
|
3
|
+
//#region src/components/button/Button.tsx
|
|
4
|
+
function t({ children: t, className: n, size: r = "medium", type: i = "button", variant: a = "secondary", ...o }) {
|
|
5
|
+
return /* @__PURE__ */ e("button", {
|
|
6
|
+
className: [
|
|
7
|
+
"hn-button",
|
|
8
|
+
`hn-button--${a}`,
|
|
9
|
+
`hn-button--${r}`,
|
|
10
|
+
n
|
|
11
|
+
].filter(Boolean).join(" "),
|
|
12
|
+
type: i,
|
|
13
|
+
...o,
|
|
14
|
+
children: t
|
|
15
|
+
});
|
|
16
|
+
}
|
|
17
|
+
//#endregion
|
|
18
|
+
export { t };
|
package/dist/button.d.ts
ADDED
package/dist/button.js
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { HTMLAttributes, ReactNode } from 'react';
|
|
2
|
+
export type BadgeTone = 'neutral' | 'accent' | 'success' | 'warning' | 'danger';
|
|
3
|
+
export interface BadgeProps extends HTMLAttributes<HTMLSpanElement> {
|
|
4
|
+
readonly children: ReactNode;
|
|
5
|
+
readonly tone?: BadgeTone;
|
|
6
|
+
}
|
|
7
|
+
export declare function Badge({ children, className, tone, ...props }: BadgeProps): import("react").JSX.Element;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { ButtonHTMLAttributes, ReactNode } from 'react';
|
|
2
|
+
export type ButtonVariant = 'primary' | 'secondary' | 'ghost';
|
|
3
|
+
export type ButtonSize = 'small' | 'medium' | 'large';
|
|
4
|
+
export interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
|
|
5
|
+
readonly children: ReactNode;
|
|
6
|
+
readonly variant?: ButtonVariant;
|
|
7
|
+
readonly size?: ButtonSize;
|
|
8
|
+
}
|
|
9
|
+
export declare function Button({ children, className, size, type, variant, ...props }: ButtonProps): import("react").JSX.Element;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { ReactNode } from 'react';
|
|
2
|
+
export type ConfirmTone = 'default' | 'danger';
|
|
3
|
+
export interface ConfirmProps {
|
|
4
|
+
readonly open: boolean;
|
|
5
|
+
readonly title: string;
|
|
6
|
+
readonly description?: string | undefined;
|
|
7
|
+
readonly confirmText?: string | undefined;
|
|
8
|
+
readonly cancelText?: string | undefined;
|
|
9
|
+
readonly tone?: ConfirmTone | undefined;
|
|
10
|
+
readonly loading?: boolean | undefined;
|
|
11
|
+
readonly onConfirm: () => void;
|
|
12
|
+
readonly onCancel: () => void;
|
|
13
|
+
readonly closeOnEsc?: boolean | undefined;
|
|
14
|
+
readonly closeOnBackdrop?: boolean | undefined;
|
|
15
|
+
}
|
|
16
|
+
export type ConfirmOptions = Omit<ConfirmProps, 'open' | 'onConfirm' | 'onCancel' | 'loading'> & {
|
|
17
|
+
readonly loading?: boolean;
|
|
18
|
+
};
|
|
19
|
+
export type ConfirmFunction = (options: ConfirmOptions) => Promise<boolean>;
|
|
20
|
+
export declare function Confirm({ open, title, description, confirmText, cancelText, tone, loading, onConfirm, onCancel, closeOnEsc, closeOnBackdrop, }: ConfirmProps): import("react").JSX.Element;
|
|
21
|
+
export declare function ConfirmProvider({ children }: {
|
|
22
|
+
readonly children: ReactNode;
|
|
23
|
+
}): import("react").JSX.Element;
|
|
24
|
+
export declare function useConfirm(): ConfirmFunction;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { HTMLAttributes, Ref } from 'react';
|
|
2
|
+
export interface DialogProps extends HTMLAttributes<HTMLDivElement> {
|
|
3
|
+
readonly open: boolean;
|
|
4
|
+
readonly onClose: () => void;
|
|
5
|
+
readonly title?: string | undefined;
|
|
6
|
+
readonly description?: string | undefined;
|
|
7
|
+
readonly closeOnEsc?: boolean | undefined;
|
|
8
|
+
readonly closeOnBackdrop?: boolean | undefined;
|
|
9
|
+
readonly ref?: Ref<HTMLDivElement>;
|
|
10
|
+
}
|
|
11
|
+
export declare function Dialog({ children, className, title, description, open, onClose, closeOnEsc, closeOnBackdrop, ref, style, ...props }: DialogProps): import('react').ReactPortal | null;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { KeyboardEvent, PointerEvent, RefObject } from 'react';
|
|
2
|
+
type ModalPhase = 'closed' | 'entering' | 'open' | 'leaving';
|
|
3
|
+
export interface UseModalOptions {
|
|
4
|
+
readonly open: boolean;
|
|
5
|
+
readonly onClose: () => void;
|
|
6
|
+
readonly closeOnEsc?: boolean;
|
|
7
|
+
readonly closeOnBackdrop?: boolean;
|
|
8
|
+
readonly exitDuration?: number;
|
|
9
|
+
}
|
|
10
|
+
export interface UseModalResult {
|
|
11
|
+
readonly phase: ModalPhase;
|
|
12
|
+
readonly mounted: boolean;
|
|
13
|
+
readonly dataState: 'enter' | 'exit' | undefined;
|
|
14
|
+
readonly panelRef: RefObject<HTMLDivElement | null>;
|
|
15
|
+
readonly backdropRef: RefObject<HTMLDivElement | null>;
|
|
16
|
+
readonly titleId: string;
|
|
17
|
+
readonly descriptionId: string;
|
|
18
|
+
readonly handlePanelKeyDown: (event: KeyboardEvent<HTMLDivElement>) => void;
|
|
19
|
+
readonly handleBackdropPointerDown: (event: PointerEvent<HTMLDivElement>) => void;
|
|
20
|
+
}
|
|
21
|
+
export declare function useModal({ open, onClose, closeOnEsc, closeOnBackdrop, exitDuration, }: UseModalOptions): UseModalResult;
|
|
22
|
+
export {};
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { HTMLAttributes, Ref } from 'react';
|
|
2
|
+
export type DrawerPlacement = 'left' | 'right' | 'top' | 'bottom';
|
|
3
|
+
export interface DrawerProps extends HTMLAttributes<HTMLDivElement> {
|
|
4
|
+
readonly open: boolean;
|
|
5
|
+
readonly onClose: () => void;
|
|
6
|
+
readonly placement?: DrawerPlacement;
|
|
7
|
+
readonly title?: string | undefined;
|
|
8
|
+
readonly description?: string | undefined;
|
|
9
|
+
readonly size?: number | undefined;
|
|
10
|
+
readonly closeOnEsc?: boolean | undefined;
|
|
11
|
+
readonly closeOnBackdrop?: boolean | undefined;
|
|
12
|
+
readonly ref?: Ref<HTMLDivElement>;
|
|
13
|
+
}
|
|
14
|
+
export declare function Drawer({ children, className, title, description, open, onClose, placement, size, closeOnEsc, closeOnBackdrop, ref, style, ...props }: DrawerProps): import('react').ReactPortal | null;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { SVGProps } from 'react';
|
|
2
|
+
import { IconName } from './icon-names';
|
|
3
|
+
/**
|
|
4
|
+
* Icon 组件 -- 第一期 outline 风格图标集
|
|
5
|
+
* ───────────────────────────────────────────────────────────────────────────
|
|
6
|
+
* 设计规范(第一期统一遵循):
|
|
7
|
+
* - viewBox 固定 24 × 24
|
|
8
|
+
* - stroke = "currentColor",stroke-width = 1.5
|
|
9
|
+
* - stroke-linecap = "round"、stroke-linejoin = "round"
|
|
10
|
+
* - fill = "none"(纯描边 outline 风格)
|
|
11
|
+
* - 仅用 path / line / rect / circle / polyline 等基础图元,不依赖外部资源
|
|
12
|
+
* - 每个图标都在 24×24 的画布内留出约 1.5px 的视觉边距,避免被裁切
|
|
13
|
+
*
|
|
14
|
+
* 用法:
|
|
15
|
+
* <Icon name="file" />
|
|
16
|
+
* <Icon name="save" className="my-save" />
|
|
17
|
+
* <Icon name="delete" aria-label="删除" />
|
|
18
|
+
*
|
|
19
|
+
* 颜色:通过外层 color 控制(fill/stroke 都是 currentColor);尺寸:通过外层
|
|
20
|
+
* font-size 或 CSS width/height 控制(默认 1em 见方)。
|
|
21
|
+
*/
|
|
22
|
+
export interface IconProps extends Omit<SVGProps<SVGSVGElement>, 'name'> {
|
|
23
|
+
/** 图标名称,对应 IconName 联合类型 */
|
|
24
|
+
readonly name: IconName;
|
|
25
|
+
/** 可访问标签;传入后会渲染 aria-label,并将 svg 标记为 role="img" */
|
|
26
|
+
readonly label?: string;
|
|
27
|
+
}
|
|
28
|
+
export declare function Icon({ label, name, className, ...props }: IconProps): import("react").JSX.Element;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Icon 名称数组 + 派生类型
|
|
3
|
+
* ───────────────────────────────────────────────────────────────────────────
|
|
4
|
+
* 把名称数组从 Icon.tsx 拆出来的原因:
|
|
5
|
+
* `react-refresh/only-export-components` 要求一个文件要么只导出组件,要么把
|
|
6
|
+
* 非组件导出拆到独立文件。Icon.tsx 同时导出 Icon 组件和 ICON_NAMES 常量会
|
|
7
|
+
* 触发该规则,故把名称数组 + 类型派生放到这里。
|
|
8
|
+
*
|
|
9
|
+
* 设计:单一数据源。IconName 类型从 ICON_NAMES 数组派生,避免数组与联合
|
|
10
|
+
* 类型之间出现漂移。新增图标只需在数组里追加一个字面量。
|
|
11
|
+
*/
|
|
12
|
+
/** 50 个图标名称字面量数组(运行时枚举与 demo 遍历用) */
|
|
13
|
+
export declare const ICON_NAMES: readonly ["file", "save", "study", "switch", "mouse", "line", "curve", "rectangle", "ellipse", "pen", "eraser", "magnet", "locate", "delete", "add", "copy", "paste", "info", "touch", "edit", "keyboard", "type", "handle", "card", "search", "close", "check", "chevron-down", "chevron-up", "chevron-left", "chevron-right", "arrow-up", "arrow-down", "arrow-left", "arrow-right", "settings", "user", "home", "star", "bookmark", "link", "image", "calendar", "clock", "bell", "download", "upload", "share", "undo", "redo", "zoom-in", "zoom-out", "menu", "more", "filter", "sort"];
|
|
14
|
+
/** 50 个图标的名称联合类型(从 ICON_NAMES 派生,单一数据源) */
|
|
15
|
+
export type IconName = (typeof ICON_NAMES)[number];
|