@openconsole/shadcn 0.0.0 → 0.0.1

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.
Files changed (71) hide show
  1. package/accordion.tsx +66 -66
  2. package/alert-dialog.tsx +196 -196
  3. package/alert.tsx +66 -66
  4. package/aspect-ratio.tsx +11 -11
  5. package/avatar.tsx +53 -53
  6. package/badge.tsx +46 -46
  7. package/breadcrumb.tsx +109 -109
  8. package/button-group.tsx +83 -83
  9. package/button.tsx +60 -60
  10. package/calendar.tsx +219 -219
  11. package/card.tsx +92 -92
  12. package/carousel.tsx +241 -241
  13. package/chart.tsx +374 -374
  14. package/checkbox.tsx +32 -32
  15. package/collapsible.tsx +33 -33
  16. package/command.tsx +184 -184
  17. package/context-menu.tsx +252 -252
  18. package/dialog.tsx +143 -143
  19. package/direction.tsx +22 -22
  20. package/drawer.tsx +135 -135
  21. package/dropdown-menu.tsx +257 -257
  22. package/empty.tsx +104 -104
  23. package/field.tsx +248 -248
  24. package/form.tsx +167 -167
  25. package/hooks/index.ts +1 -1
  26. package/hooks/use-mobile.ts +19 -19
  27. package/hover-card.tsx +44 -44
  28. package/icon.tsx +21 -21
  29. package/index.ts +59 -59
  30. package/input-group.tsx +170 -170
  31. package/input-otp.tsx +77 -77
  32. package/input.tsx +21 -21
  33. package/item.tsx +193 -193
  34. package/kbd.tsx +28 -28
  35. package/label.tsx +24 -24
  36. package/lib/index.ts +1 -1
  37. package/lib/utils.ts +6 -6
  38. package/menubar.tsx +276 -276
  39. package/native-select.tsx +62 -62
  40. package/navigation-menu.tsx +168 -168
  41. package/package.json +10 -2
  42. package/pagination.tsx +127 -127
  43. package/popover.tsx +89 -89
  44. package/progress.tsx +31 -31
  45. package/radio-group.tsx +45 -45
  46. package/resizable.tsx +53 -53
  47. package/scroll-area.tsx +58 -58
  48. package/select.tsx +187 -187
  49. package/separator.tsx +28 -28
  50. package/sheet.tsx +139 -139
  51. package/sidebar.tsx +724 -724
  52. package/skeleton.tsx +13 -13
  53. package/skill/SKILL.md +620 -599
  54. package/skill/customization.md +301 -263
  55. package/skill/rules/base-vs-radix.md +167 -167
  56. package/skill/rules/composition.md +240 -240
  57. package/skill/rules/forms.md +271 -271
  58. package/skill/rules/icons.md +136 -136
  59. package/skill/rules/styling.md +180 -180
  60. package/slider.tsx +63 -63
  61. package/sonner.tsx +40 -40
  62. package/spinner.tsx +16 -16
  63. package/styles.css +122 -0
  64. package/switch.tsx +35 -35
  65. package/table.tsx +116 -116
  66. package/tabs.tsx +66 -66
  67. package/textarea.tsx +18 -18
  68. package/toggle-group.tsx +83 -83
  69. package/toggle.tsx +47 -47
  70. package/tooltip.tsx +61 -61
  71. package/tsconfig.json +12 -12
package/skill/SKILL.md CHANGED
@@ -1,599 +1,620 @@
1
- ---
2
- name: openconsole-shadcn
3
- description: >
4
- `@openconsole/shadcn` 的使用指南。完整的 shadcn UI 原语集合(Button、
5
- Dialog、Form、Sidebar、Table、Card、Tabs 等),`cn` / `useIsMobile` /
6
- `Icon` / `Direction` 工具,Tailwind v4 语义化 token。
7
- 适用场景包括: 搭建页面与表单、选择正确的组件原语、修复样式问题、
8
- 组合复杂交互(设置页、数据表格、仪表盘、命令面板、抽屉、确认对话框
9
- 等)、应用主题与品牌色。
10
- type: ui
11
- library: "@openconsole/shadcn"
12
- runtime:
13
- react: "^19"
14
- tailwind: "^4"
15
- peers:
16
- "lucide-react": "*"
17
- "next-themes": "*"
18
- "react-hook-form": "*"
19
- "zod": "*"
20
- ---
21
-
22
- # `@openconsole/shadcn` —— UI 原语组件
23
-
24
- 一个 npm 包,把整套 shadcn/ui 原语 + 一小撮工具(`cn`、`useIsMobile`、
25
- `Icon`、`Direction`)通过单一入口 `@openconsole/shadcn` 平铺导出。
26
-
27
- 本包是**只读消费**:所有可用的组件就是 `index.ts` 导出的全部内容。
28
- 没有 CLI、没有源码改动、不需要额外安装。
29
-
30
- 本文档覆盖:
31
-
32
- 1. 怎么从用户的口语化需求识别到正确的组件([应用场景速查](#应用场景速查))
33
- 2. 选对原语和正确组合(Item 在 Group 里、Tabs 在 TabsList 里等)
34
- 3. 不破坏主题地写样式(语义 token,不裸用色,不手写 `dark:`)
35
- 4. 用 `Form` + `FieldGroup` + `Field` 接表单
36
- 5. 处理图标(`lucide-react` + `data-icon` 槽位)
37
- 6. 调用本包组件时正确的 prop 形状([rules/base-vs-radix.md](./rules/base-vs-radix.md))
38
- 7. 主题化与扩展边界([customization.md](./customization.md))
39
-
40
- ---
41
-
42
- ## 项目上下文
43
-
44
- | 字段 | |
45
- |---|---|
46
- | 导入路径 | `@openconsole/shadcn`(唯一入口) |
47
- | 工具集 | `cn`、`useIsMobile`、`Icon`、`Direction`、`Kbd`、`KbdGroup`、`Toaster` |
48
- | 样式 | Tailwind v4 + 语义 token(`--background`、`--primary`、`--muted`…) |
49
- | 图标库 | `lucide-react`(也通过 `Icon` 二次导出,用于按名字动态渲染) |
50
- | 表单栈 | `react-hook-form` + `zod`(经 `@hookform/resolvers`) |
51
- | API 风格 | 统一 `asChild`、`type="single"` 显式、`Slider` 用数组等。见 [rules/base-vs-radix.md](./rules/base-vs-radix.md) |
52
- | 主题 | 配合 `next-themes` 做亮 / 暗切换;语义 token 自动跟随 |
53
-
54
- ---
55
-
56
- ## 应用场景速查
57
-
58
- 用户用自然语言描述需求时,按下表识别意图,挑出本包中正确的组件。
59
-
60
- | 用户描述(关键词) | 选这个 | 关键组合 |
61
- |---|---|---|
62
- | "搭一个登录页 / 注册表单 / 创建表单" | `Card` + `Form` | Card 包外层 → CardHeader + CardContent → `Form` + `FormField` + `FormItem` + `FormLabel` + `FormControl(Input)` + `FormMessage` |
63
- | "设置页 / 偏好 / Profile" | `Tabs` + `Field` | Tabs 分组 → 每页用 `Field orientation="horizontal"` + `Switch`/`Select`/`Input` |
64
- | "用户列表 / 数据表格 / 列表" | `Table` | TableHeader / TableRow / TableCell;要排序筛选,应用层接 `@tanstack/react-table` |
65
- | "纯展示表格" | `Table` | 见上 |
66
- | "仪表盘 / Dashboard / 首页指标" | `Card` 网格 + `Chart*` + `Badge` | Card 拼数据卡 → Chart 系列展可视化 → Badge 标状态 |
67
- | "用户头像下拉菜单" | `Avatar` + `DropdownMenu` | DropdownMenuTrigger(asChild) → Avatar + AvatarFallback → DropdownMenuContent → DropdownMenuGroup → DropdownMenuItem |
68
- | "删除确认 / 二次确认" | `AlertDialog` | AlertDialogTrigger + AlertDialogContent + AlertDialogFooter + AlertDialogAction(Button variant="destructive") |
69
- | "侧拉面板 / 详情抽屉 / 筛选侧栏" | `Sheet` | `<Sheet>` + `<SheetContent side="right">` |
70
- | "移动端底部抽屉 / 半屏" | `Drawer` | Drawer + DrawerContent |
71
- | "主框架 / 后台外壳" | `SidebarProvider` + `Sidebar` | SidebarProvider 包根 → Sidebar + SidebarMenu + SidebarMenuItem 拼侧栏 → main 区域是主内容 |
72
- | "空状态 / 暂无数据" | `Empty` | Empty → EmptyHeader → EmptyMedia + EmptyTitle + EmptyDescription → EmptyContent(Button) |
73
- | "加载中骨架" | `Skeleton` | 拼网格匹配实际布局 |
74
- | "加载中转圈" | `Spinner` | 在按钮里配 `data-icon` + `disabled` |
75
- | "命令面板 / 快速跳转 / Cmd+K" | `Dialog` + `Command` | Dialog 包外,里面 Command + CommandInput + CommandList + CommandGroup + CommandItem |
76
- | "下拉菜单(点开)" | `DropdownMenu` | 点击触发 |
77
- | "右键菜单" | `ContextMenu` | 长按 / 右键触发 |
78
- | "应用顶部菜单条" | `Menubar` | macOS 顶部菜单 |
79
- | "面包屑导航" | `Breadcrumb` | BreadcrumbList → BreadcrumbItem → BreadcrumbLink |
80
- | "分页" | `Pagination` | PaginationContentPaginationItemPaginationLink/Previous/Next |
81
- | "标签页" | `Tabs` | Tabs → TabsList TabsTrigger TabsContent |
82
- | "可折叠区块" | `Collapsible`(单个)或 `Accordion`(多个分组) | Accordion 用于 FAQ;Collapsible 用于单个开关区 |
83
- | "悬浮提示" | `Tooltip` | TooltipTrigger + TooltipContent,可配 `Kbd` |
84
- | "悬浮卡片 / 用户名 hover 预览" | `HoverCard` | HoverCardTrigger + HoverCardContent |
85
- | "点击弹出小卡片 / 颜色 / 日期" | `Popover` | PopoverTrigger + PopoverContent |
86
- | "Toast / 通知 / 短反馈" | `toast()` from `sonner` | 根上挂一次 `<Toaster />`,业务里直接调 `toast.success(...)` |
87
- | "进度条" | `Progress` | 已知进度用 Progress;未知用 Spinner |
88
- | "标签 / 状态徽章" | `Badge` | variant: default / secondary / destructive / outline |
89
- | "可搜索下拉 / 自动补全" | `Popover` + `Command` | PopoverTrigger 触发PopoverContent Command + CommandInput + CommandList |
90
- | "下拉选项(不搜索)" | `Select` | inline SelectItem,详见 [rules/base-vs-radix.md](./rules/base-vs-radix.md) |
91
- | "日期选择器" | `Popover` + `Calendar` | PopoverTrigger(Button) → PopoverContent 包 Calendar |
92
- | "纯日历视图" | `Calendar` | 渲染月历 |
93
- | "主题切换按钮" | `Button` + `next-themes` | 见下 [完整代码示例](#场景--完整代码示例) |
94
- | "设置抽屉(侧拉式)" | `Sheet` | Sheet + SheetContent 包 Tabs / Field 表单 |
95
- | "OTP / 验证码输入" | `InputOTP` | 4-6 位分格输入 |
96
- | "评分滑块 / 调音量" | `Slider` | **value 必须是数组**: `[50]` 不是 `50` |
97
- | "可调整大小的面板" | `Resizable` | ResizablePanelGroup + ResizablePanel + ResizableHandle |
98
- | "长内容滚动" | `ScrollArea` | 自定义滚动条样式 |
99
- | "图片占位(保持比例)" | `AspectRatio` | 包图片避免布局抖动 |
100
- | "辐射 / 分割" | `Separator` | 替代 `<hr>` 和带 border 的 div |
101
- | "图标按钮分组" | `ButtonGroup` 或 `ToggleGroup` | 互斥用 `ToggleGroup type="single"`;并列动作用 `ButtonGroup` |
102
-
103
- ### 场景 完整代码示例
104
-
105
- #### 登录表单
106
-
107
- ```tsx
108
- <Card className="mx-auto max-w-sm">
109
- <CardHeader>
110
- <CardTitle>登录</CardTitle>
111
- <CardDescription>使用邮箱和密码登录</CardDescription>
112
- </CardHeader>
113
- <CardContent>
114
- <Form {...form}>
115
- <form onSubmit={form.handleSubmit(onSubmit)} className="flex flex-col gap-4">
116
- <FormField
117
- control={form.control}
118
- name="email"
119
- render={({ field }) => (
120
- <FormItem>
121
- <FormLabel>邮箱</FormLabel>
122
- <FormControl><Input {...field} /></FormControl>
123
- <FormMessage />
124
- </FormItem>
125
- )}
126
- />
127
- <FormField
128
- control={form.control}
129
- name="password"
130
- render={({ field }) => (
131
- <FormItem>
132
- <FormLabel>密码</FormLabel>
133
- <FormControl><Input type="password" {...field} /></FormControl>
134
- <FormMessage />
135
- </FormItem>
136
- )}
137
- />
138
- <Button type="submit" disabled={form.formState.isSubmitting}>
139
- {form.formState.isSubmitting && <Spinner data-icon="inline-start" />}
140
- 登录
141
- </Button>
142
- </form>
143
- </Form>
144
- </CardContent>
145
- </Card>
146
- ```
147
-
148
- #### 删除确认
149
-
150
- ```tsx
151
- <AlertDialog>
152
- <AlertDialogTrigger asChild>
153
- <Button variant="destructive">删除项目</Button>
154
- </AlertDialogTrigger>
155
- <AlertDialogContent>
156
- <AlertDialogHeader>
157
- <AlertDialogTitle>确认删除?</AlertDialogTitle>
158
- <AlertDialogDescription>
159
- 此操作不可撤销。项目下的所有数据会一并删除。
160
- </AlertDialogDescription>
161
- </AlertDialogHeader>
162
- <AlertDialogFooter>
163
- <AlertDialogCancel>取消</AlertDialogCancel>
164
- <AlertDialogAction onClick={onConfirm}>删除</AlertDialogAction>
165
- </AlertDialogFooter>
166
- </AlertDialogContent>
167
- </AlertDialog>
168
- ```
169
-
170
- #### 命令面板(Cmd+K)
171
-
172
- ```tsx
173
- <Dialog open={open} onOpenChange={setOpen}>
174
- <DialogContent className="p-0">
175
- <DialogTitle className="sr-only">命令面板</DialogTitle>
176
- <Command>
177
- <CommandInput placeholder="输入命令或搜索..." />
178
- <CommandList>
179
- <CommandEmpty>没有匹配结果。</CommandEmpty>
180
- <CommandGroup heading="导航">
181
- <CommandItem onSelect={() => router.push("/dashboard")}>
182
- <LayoutDashboardIcon />
183
- 仪表盘
184
- </CommandItem>
185
- <CommandItem onSelect={() => router.push("/settings")}>
186
- <SettingsIcon />
187
- 设置
188
- </CommandItem>
189
- </CommandGroup>
190
- </CommandList>
191
- </Command>
192
- </DialogContent>
193
- </Dialog>
194
- ```
195
-
196
- #### 仪表盘数据卡
197
-
198
- ```tsx
199
- <div className="grid gap-4 md:grid-cols-3">
200
- <Card>
201
- <CardHeader>
202
- <CardDescription>总营收</CardDescription>
203
- <CardTitle className="text-3xl">¥45,231</CardTitle>
204
- </CardHeader>
205
- <CardFooter>
206
- <Badge variant="secondary">+20.1% 较上月</Badge>
207
- </CardFooter>
208
- </Card>
209
- <Card>
210
- <CardHeader>
211
- <CardDescription>活跃用户</CardDescription>
212
- <CardTitle className="text-3xl">2,350</CardTitle>
213
- </CardHeader>
214
- <CardFooter>
215
- <Badge variant="secondary">+18.1% 较上月</Badge>
216
- </CardFooter>
217
- </Card>
218
- <Card>
219
- <CardHeader>
220
- <CardDescription>转化率</CardDescription>
221
- <CardTitle className="text-3xl">3.2%</CardTitle>
222
- </CardHeader>
223
- <CardFooter>
224
- <span className="text-destructive text-sm">-2.4% 较上月</span>
225
- </CardFooter>
226
- </Card>
227
- </div>
228
- ```
229
-
230
- #### 用户头像下拉
231
-
232
- ```tsx
233
- <DropdownMenu>
234
- <DropdownMenuTrigger asChild>
235
- <Button variant="ghost" className="size-8 rounded-full p-0">
236
- <Avatar className="size-8">
237
- <AvatarImage src={user.avatar} alt={user.name} />
238
- <AvatarFallback>{user.initials}</AvatarFallback>
239
- </Avatar>
240
- </Button>
241
- </DropdownMenuTrigger>
242
- <DropdownMenuContent align="end">
243
- <DropdownMenuLabel>{user.name}</DropdownMenuLabel>
244
- <DropdownMenuSeparator />
245
- <DropdownMenuGroup>
246
- <DropdownMenuItem onSelect={() => router.push("/profile")}>
247
- <UserIcon />
248
- 个人资料
249
- </DropdownMenuItem>
250
- <DropdownMenuItem onSelect={() => router.push("/settings")}>
251
- <SettingsIcon />
252
- 设置
253
- </DropdownMenuItem>
254
- </DropdownMenuGroup>
255
- <DropdownMenuSeparator />
256
- <DropdownMenuItem onSelect={signOut}>
257
- <LogOutIcon />
258
- 退出
259
- </DropdownMenuItem>
260
- </DropdownMenuContent>
261
- </DropdownMenu>
262
- ```
263
-
264
- #### 空状态
265
-
266
- ```tsx
267
- <Empty>
268
- <EmptyHeader>
269
- <EmptyMedia variant="icon"><FolderIcon /></EmptyMedia>
270
- <EmptyTitle>还没有项目</EmptyTitle>
271
- <EmptyDescription>创建你的第一个项目开始使用。</EmptyDescription>
272
- </EmptyHeader>
273
- <EmptyContent>
274
- <Button>新建项目</Button>
275
- </EmptyContent>
276
- </Empty>
277
- ```
278
-
279
- #### 设置页(分页布局)
280
-
281
- ```tsx
282
- <Tabs defaultValue="account" className="flex flex-col gap-6">
283
- <TabsList>
284
- <TabsTrigger value="account">账户</TabsTrigger>
285
- <TabsTrigger value="notifications">通知</TabsTrigger>
286
- <TabsTrigger value="appearance">外观</TabsTrigger>
287
- </TabsList>
288
- <TabsContent value="account">
289
- <FieldGroup>
290
- <Field orientation="horizontal">
291
- <FieldLabel>姓名</FieldLabel>
292
- <Input defaultValue={user.name} />
293
- </Field>
294
- <Field orientation="horizontal">
295
- <FieldLabel>邮箱</FieldLabel>
296
- <Input type="email" defaultValue={user.email} />
297
- </Field>
298
- </FieldGroup>
299
- </TabsContent>
300
- <TabsContent value="notifications">
301
- <FieldSet>
302
- <FieldLegend>邮件通知</FieldLegend>
303
- <FieldGroup>
304
- <Field orientation="horizontal">
305
- <Switch id="weekly" />
306
- <FieldLabel htmlFor="weekly">每周摘要</FieldLabel>
307
- </Field>
308
- <Field orientation="horizontal">
309
- <Switch id="security" />
310
- <FieldLabel htmlFor="security">安全提醒</FieldLabel>
311
- </Field>
312
- </FieldGroup>
313
- </FieldSet>
314
- </TabsContent>
315
- </Tabs>
316
- ```
317
-
318
- #### 主题切换按钮
319
-
320
- ```tsx
321
- "use client";
322
- import { Moon, Sun } from "lucide-react";
323
- import { useTheme } from "next-themes";
324
- import { Button } from "@openconsole/shadcn";
325
-
326
- export function ThemeToggle() {
327
- // resolvedTheme,处于 System 模式时也能正确翻转。
328
- const { resolvedTheme, setTheme } = useTheme();
329
- return (
330
- <Button
331
- variant="outline"
332
- size="icon"
333
- onClick={() => setTheme(resolvedTheme === "dark" ? "light" : "dark")}
334
- >
335
- <Sun className="rotate-0 scale-100 transition-all dark:-rotate-90 dark:scale-0" />
336
- <Moon className="absolute rotate-90 scale-0 transition-all dark:rotate-0 dark:scale-100" />
337
- <span className="sr-only">切换主题</span>
338
- </Button>
339
- );
340
- }
341
- ```
342
-
343
- #### 可搜索下拉
344
-
345
- ```tsx
346
- <Popover open={open} onOpenChange={setOpen}>
347
- <PopoverTrigger asChild>
348
- <Button variant="outline" role="combobox" aria-expanded={open} className="w-[200px] justify-between">
349
- {value ? options.find((o) => o.value === value)?.label : "选择..."}
350
- <ChevronsUpDownIcon data-icon="inline-end" />
351
- </Button>
352
- </PopoverTrigger>
353
- <PopoverContent className="w-[200px] p-0">
354
- <Command>
355
- <CommandInput placeholder="搜索..." />
356
- <CommandList>
357
- <CommandEmpty>没有匹配项。</CommandEmpty>
358
- <CommandGroup>
359
- {options.map((option) => (
360
- <CommandItem
361
- key={option.value}
362
- onSelect={() => {
363
- setValue(option.value);
364
- setOpen(false);
365
- }}
366
- >
367
- <CheckIcon className={cn("mr-2", value === option.value ? "opacity-100" : "opacity-0")} />
368
- {option.label}
369
- </CommandItem>
370
- ))}
371
- </CommandGroup>
372
- </CommandList>
373
- </Command>
374
- </PopoverContent>
375
- </Popover>
376
- ```
377
-
378
- #### 日期选择器
379
-
380
- ```tsx
381
- <Popover>
382
- <PopoverTrigger asChild>
383
- <Button variant="outline" className={cn("justify-start text-left font-normal", !date && "text-muted-foreground")}>
384
- <CalendarIcon data-icon="inline-start" />
385
- {date ? format(date, "PPP") : "选择日期"}
386
- </Button>
387
- </PopoverTrigger>
388
- <PopoverContent className="w-auto p-0">
389
- <Calendar mode="single" selected={date} onSelect={setDate} initialFocus />
390
- </PopoverContent>
391
- </Popover>
392
- ```
393
-
394
- ---
395
-
396
- ## 关键规则
397
-
398
- 下面的规则**总是被强制**。看到违反就直接修。每条都链到一个
399
- Incorrect/Correct 代码对照的细则文件。
400
-
401
- ### 样式 & Tailwind [rules/styling.md](./rules/styling.md)
402
-
403
- - **`className` 只管布局,不管样式**。从外面覆盖组件颜色或排版是错的。
404
- - **不要 `space-x-*` / `space-y-*`**。改用 `flex … gap-*`。
405
- - **宽高相等时用 `size-*`**。`size-10` 不是 `w-10 h-10`。
406
- - **`truncate` 是简写**,不要 `overflow-hidden text-ellipsis whitespace-nowrap`。
407
- - **别手写 `dark:` 颜色覆盖**。用语义 token。
408
- - **状态色别用裸值**。用 `Badge` variant 或 `text-destructive`。
409
- - **条件 className 用 `cn()`**(从 `@openconsole/shadcn` 导入)。
410
- - **overlay 别加 z-index**(`Dialog`、`Popover`、`Tooltip` 等自管堆叠)。
411
-
412
- ### 表单 → [rules/forms.md](./rules/forms.md)
413
-
414
- - **表单布局用 `FieldGroup` + `Field`**,绝不要 `<div className="space-y-*">`。
415
- - **schema 用 `Form` + `FormField` + `FormItem` + `FormControl` + `FormMessage`**。
416
- - **`InputGroup` 用 `InputGroupInput` / `InputGroupTextarea`**。
417
- - **输入框里的按钮用 `InputGroup` + `InputGroupAddon`**。
418
- - **2–7 个互斥选项用 `ToggleGroup`**。
419
- - **相关 checkbox / radio 分组用 `FieldSet` + `FieldLegend`**。
420
- - **校验状态: `data-invalid` 在 `Field` + `aria-invalid` 在 control**。
421
-
422
- ### 组合 [rules/composition.md](./rules/composition.md)
423
-
424
- - **Item 一定在自己的 Group 里**(`SelectItem` `SelectGroup`,
425
- `DropdownMenuItem` `DropdownMenuGroup`,`CommandItem`
426
- `CommandGroup`,`TabsTrigger` `TabsList`)。
427
- - **`Dialog` / `Sheet` / `Drawer` 一定要有 Title**(视觉隐藏用 `className="sr-only"`)。
428
- - **`Card` 用完整组合**: `CardHeader` / `CardTitle` / `CardDescription` /
429
- `CardContent` / `CardFooter`。
430
- - **`Avatar` 必须有 `AvatarFallback`**。
431
- - **`Button` 没有 `isLoading` prop**: 用 `Spinner` + `data-icon` + `disabled` 拼。
432
- - **自定义触发器用 `asChild`**。
433
-
434
- ### 用组件,别堆裸标签 [rules/composition.md](./rules/composition.md)
435
-
436
- - 提示框 `Alert`。空状态 → `Empty`。Toast → `sonner` 的 `toast()`。
437
- - `Separator` 替代 `<hr>`。`Skeleton` 替代 `animate-pulse` div。
438
- - `Badge` 替代加样式的 span。`Kbd` 用于键盘提示。`Spinner` 替代手写转圈。
439
-
440
- ### 图标 → [rules/icons.md](./rules/icons.md)
441
-
442
- - **按钮里的图标用 `data-icon="inline-start"` / `"inline-end"`**。
443
- - **组件内部图标不要加尺寸 class**(组件自管)。
444
- - **图标当组件对象传**,别用字符串 key 查表。
445
- - **按名字动态渲染用 `Icon`**(本包导出的 `lucide-react` 包装)。
446
-
447
- ### API 形状 → [rules/base-vs-radix.md](./rules/base-vs-radix.md)
448
-
449
- - 自定义 trigger 用 `asChild`。
450
- - `ToggleGroup` / `Accordion` 显式 `type="single"` 或 `type="multiple"`。
451
- - `Slider` 的 `value` 永远是数组。
452
- - `Select` inline `<SelectItem>`,placeholder 在 `<SelectValue>` 上。
453
-
454
- ---
455
-
456
- ## 关键模式
457
-
458
- ```tsx
459
- // 表单布局: FieldGroup + Field(不是 div + Label)
460
- <FieldGroup>
461
- <Field>
462
- <FieldLabel htmlFor="email">Email</FieldLabel>
463
- <Input id="email" />
464
- </Field>
465
- </FieldGroup>
466
-
467
- // 校验: data-invalid Field, aria-invalid 在 control
468
- <Field data-invalid>
469
- <FieldLabel>Email</FieldLabel>
470
- <Input aria-invalid />
471
- <FieldDescription>Invalid email.</FieldDescription>
472
- </Field>
473
-
474
- // 按钮里的图标: data-icon, 不加尺寸
475
- <Button>
476
- <SearchIcon data-icon="inline-start" />
477
- Search
478
- </Button>
479
-
480
- // 间距: gap, 不要 space-y / space-x
481
- <div className="flex flex-col gap-4">
482
-
483
- // 等宽高: size-*
484
- <Avatar className="size-10">
485
-
486
- // 状态色: Badge variant 或语义 token
487
- <Badge variant="secondary">+20.1%</Badge>
488
-
489
- // 条件 class: cn()
490
- <div className={cn("flex items-center", isActive && "bg-primary text-primary-foreground")} />
491
-
492
- // 不显示但 a11y-合规的 Dialog: sr-only 的 title
493
- <Dialog>
494
- <DialogContent>
495
- <DialogTitle className="sr-only">Settings</DialogTitle>
496
- </DialogContent>
497
- </Dialog>
498
-
499
- // 加载中的按钮: Spinner + data-icon + disabled
500
- <Button disabled={isPending}>
501
- {isPending && <Spinner data-icon="inline-start" />}
502
- Save
503
- </Button>
504
- ```
505
-
506
- ---
507
-
508
- ## 导入
509
-
510
- 所有东西从 `@openconsole/shadcn` 平铺导出,**入口只有这一个**。
511
-
512
- ```ts
513
- import {
514
- // 原语
515
- Button, Badge, Avatar, AvatarImage, AvatarFallback,
516
- Card, CardHeader, CardTitle, CardDescription, CardContent, CardFooter,
517
- Dialog, DialogTrigger, DialogContent, DialogTitle,
518
- Sheet, SheetTrigger, SheetContent, SheetTitle,
519
- Tabs, TabsList, TabsTrigger, TabsContent,
520
- Tooltip, TooltipTrigger, TooltipContent,
521
- Popover, PopoverTrigger, PopoverContent,
522
- // 表单
523
- Form, FormField, FormItem, FormLabel, FormControl, FormDescription, FormMessage,
524
- useFormField,
525
- FieldGroup, Field, FieldLabel, FieldDescription, FieldSet, FieldLegend,
526
- Input, InputGroup, InputGroupInput, InputGroupAddon,
527
- Select, SelectTrigger, SelectContent, SelectGroup, SelectItem, SelectValue,
528
- // 开关类
529
- Switch, Checkbox, RadioGroup, RadioGroupItem,
530
- ToggleGroup, ToggleGroupItem,
531
- // 反馈
532
- Alert, AlertTitle, AlertDescription,
533
- Empty, EmptyHeader, EmptyMedia, EmptyTitle, EmptyDescription, EmptyContent,
534
- Skeleton, Spinner, Progress,
535
- Toaster, // 根上挂一次,然后从 sonner 调 toast()
536
- // 导航
537
- Sidebar, SidebarProvider, SidebarTrigger, SidebarMenu, SidebarMenuItem,
538
- Breadcrumb, NavigationMenu, Pagination,
539
- // 浮层
540
- DropdownMenu, ContextMenu, Menubar, HoverCard,
541
- Command, CommandInput, CommandList, CommandGroup, CommandItem, CommandEmpty,
542
- // 日期
543
- Calendar,
544
- // 工具
545
- cn, useIsMobile, Icon, Direction, Kbd, KbdGroup,
546
- } from "@openconsole/shadcn";
547
-
548
- import { toast } from "sonner"; // toast() 在 sonner 里,不在本包
549
- ```
550
-
551
- 需要某个组件但本包没导出 —— 不要碰运气拼 import 路径,也不要从别处
552
- 复制源码进来。在应用层用已有原语自己组合。
553
-
554
- ---
555
-
556
- ## 主题化
557
-
558
- 详见 [customization.md](./customization.md)。短版本:
559
-
560
- - 主题色由 CSS 变量驱动 —— 改 `:root` / `.dark` 里的变量就改了所有组件。
561
- - 颜色用 OKLCH(`oklch(L C H)`)。
562
- - 加自定义色: 在 app 的全局 CSS 加 `:root` 变量 + 在 `@theme inline`
563
- 里映射(Tailwind v4)。
564
- - / 暗切换用 `next-themes` 的 `ThemeProvider` 包根 + `useTheme()`
565
- 钩子写自己的切换按钮。
566
-
567
- ---
568
-
569
- ## 常见坑
570
-
571
- - **本包没有的组件不要伪造**: `import { Calendar2 } from "@openconsole/shadcn"`
572
- 这种碰运气的 import 永远不会成功。先翻 [导入](#导入) 或
573
- [应用场景速查](#应用场景速查) 确认。
574
- - **入口只有 `@openconsole/shadcn` 根**: 不存在 `@openconsole/shadcn/dialog`、
575
- `@openconsole/shadcn/lib/utils` 这种 subpath import。
576
- - **wrapper 用了 `useState` / `useEffect` / `onClick` 却忘了 `"use client"`**:
577
- 每个交互原语本身已经标了,但你自己包出来的 wrapper 要自己加。
578
- - **给 `DialogContent` `z-50`**: 浮层组件自己分层。手写 z-index
579
- 破坏跟 `Tooltip` / `Toaster` 的堆叠顺序。
580
- - **手写 `dark:bg-*`**: 跟主题 token 打架。用 `bg-background`、
581
- `bg-muted`、`bg-card` 等。
582
- - **`SelectItem` 写在 `SelectGroup` 外面**: TS 检查能过,但键盘导航
583
- 和屏幕阅读器都坏。
584
- - **`Dialog` 没有 `DialogTitle`**: 屏幕阅读器会报缺标题。不想显示就
585
- `<DialogTitle className="sr-only">…</DialogTitle>`。
586
- - **主题切换读 `theme` 而不是 `resolvedTheme`**: 用户处于 System 模式
587
- 时 `theme === "system"`,naive 的 `theme === "dark" ? "light" : "dark"`
588
- 第一次点会无动作。读 `resolvedTheme` 来决定切到哪边。
589
-
590
- ---
591
-
592
- ## 详细参考
593
-
594
- - [rules/styling.md](./rules/styling.md) —— 语义色、variant、`className`、间距、`size-*`、`truncate`、暗色、`cn()`、z-index。
595
- - [rules/forms.md](./rules/forms.md) —— `FieldGroup`、`Field`、`InputGroup`、`ToggleGroup`、`FieldSet`、校验状态、react-hook-form 整合。
596
- - [rules/composition.md](./rules/composition.md) —— Group、overlay、`Card`、`Tabs`、`Avatar`、`Alert`、`Empty`、toast、`Separator`、`Skeleton`、`Badge`、按钮加载态。
597
- - [rules/icons.md](./rules/icons.md) —— `data-icon`、图标尺寸、图标作为对象传递、`Icon` 名字查找。
598
- - [rules/base-vs-radix.md](./rules/base-vs-radix.md) —— 本包 API 速查(`asChild`、`Select`、`ToggleGroup`、`Slider`、`Accordion`)。
599
- - [customization.md](./customization.md) —— 主题、CSS 变量、新增自定义色。
1
+ ---
2
+ name: openconsole-shadcn
3
+ description: >
4
+ `@openconsole/shadcn` 的使用指南。完整的 shadcn UI 原语集合(Button、
5
+ Dialog、Form、Sidebar、Table、Card、Tabs 等),`cn` / `useIsMobile` /
6
+ `Icon` / `Direction` 工具,Tailwind v4 语义化 token。
7
+ 适用场景包括: 搭建页面与表单、选择正确的组件原语、修复样式问题、
8
+ 组合复杂交互(设置页、数据表格、仪表盘、命令面板、抽屉、确认对话框
9
+ 等)、应用主题与品牌色。
10
+ type: ui
11
+ library: "@openconsole/shadcn"
12
+ runtime:
13
+ react: "^19"
14
+ tailwind: "^4"
15
+ peers:
16
+ "lucide-react": "*"
17
+ "next-themes": "*"
18
+ "react-hook-form": "*"
19
+ "zod": "*"
20
+ ---
21
+
22
+ # `@openconsole/shadcn` —— UI 原语组件
23
+
24
+ 一个 npm 包,把整套 shadcn/ui 原语 + 一小撮工具(`cn`、`useIsMobile`、
25
+ `Icon`、`Direction`)通过单一入口 `@openconsole/shadcn` 平铺导出。
26
+
27
+ 本包是**只读消费**:所有可用的组件就是 `index.ts` 导出的全部内容。
28
+ 没有 CLI、没有源码改动、不需要额外安装。
29
+
30
+ 本文档覆盖:
31
+
32
+ 1. 怎么从用户的口语化需求识别到正确的组件([应用场景速查](#应用场景速查))
33
+ 2. 选对原语和正确组合(Item 在 Group 里、Tabs 在 TabsList 里等)
34
+ 3. 不破坏主题地写样式(语义 token,不裸用色,不手写 `dark:`)
35
+ 4. 用 `Form` + `FieldGroup` + `Field` 接表单
36
+ 5. 处理图标(`lucide-react` + `data-icon` 槽位)
37
+ 6. 调用本包组件时正确的 prop 形状([rules/base-vs-radix.md](./rules/base-vs-radix.md))
38
+ 7. 主题化与扩展边界([customization.md](./customization.md))
39
+
40
+ ---
41
+
42
+ ## 接入
43
+
44
+ app 的全局 CSS(一般是 `app/globals.css`)里 @import 本包的 styles.css:
45
+
46
+ ```css
47
+ @import "tailwindcss";
48
+ @import "@openconsole/shadcn/styles.css";
49
+ ```
50
+
51
+ styles.css 自带 `@source` 指令、完整 token、`@theme inline` 映射、
52
+ `@custom-variant dark`、`tw-animate-css` base reset —— 一行
53
+ `@import` 就齐了, 不需要在消费方重复定义 token 或手写 `@source`。
54
+
55
+ 需要覆盖 token 在 `@import` 之后重新声明同名变量即可。详见
56
+ [customization.md —— 接入](./customization.md#接入)。
57
+
58
+ ---
59
+
60
+ ## 项目上下文
61
+
62
+ | 字段 | |
63
+ |---|---|
64
+ | 导入路径 | `@openconsole/shadcn`(唯一入口) |
65
+ | 工具集 | `cn`、`useIsMobile`、`Icon`、`Direction`、`Kbd`、`KbdGroup`、`Toaster` |
66
+ | 样式 | Tailwind v4 + 语义 token(`--background`、`--primary`、`--muted`…) |
67
+ | 图标库 | `lucide-react`(也通过 `Icon` 二次导出,用于按名字动态渲染) |
68
+ | 表单栈 | `react-hook-form` + `zod`(经 `@hookform/resolvers`) |
69
+ | API 风格 | 统一 `asChild`、`type="single"` 显式、`Slider` 用数组等。见 [rules/base-vs-radix.md](./rules/base-vs-radix.md) |
70
+ | 主题 | 配合 `next-themes` 做亮 / 暗切换;语义 token 自动跟随 |
71
+
72
+ ---
73
+
74
+ ## 应用场景速查
75
+
76
+ 用户用自然语言描述需求时,按下表识别意图,挑出本包中正确的组件。
77
+
78
+ | 用户描述(关键词) | 选这个 | 关键组合 |
79
+ |---|---|---|
80
+ | "搭一个登录页 / 注册表单 / 创建表单" | `Card` + `Form` | Card 包外层 CardHeader + CardContent `Form` + `FormField` + `FormItem` + `FormLabel` + `FormControl(Input)` + `FormMessage` |
81
+ | "设置页 / 偏好 / Profile" | `Tabs` + `Field` | Tabs 分组 每页用 `Field orientation="horizontal"` + `Switch`/`Select`/`Input` |
82
+ | "用户列表 / 数据表格 / 列表" | `Table` | TableHeader / TableRow / TableCell;要排序筛选,应用层接 `@tanstack/react-table` |
83
+ | "纯展示表格" | `Table` | 见上 |
84
+ | "仪表盘 / Dashboard / 首页指标" | `Card` 网格 + `Chart*` + `Badge` | Card 拼数据卡 → Chart 系列展可视化 → Badge 标状态 |
85
+ | "用户头像下拉菜单" | `Avatar` + `DropdownMenu` | DropdownMenuTrigger(asChild) → Avatar + AvatarFallback → DropdownMenuContent → DropdownMenuGroup → DropdownMenuItem |
86
+ | "删除确认 / 二次确认" | `AlertDialog` | AlertDialogTrigger + AlertDialogContent + AlertDialogFooter + AlertDialogAction(Button variant="destructive") |
87
+ | "侧拉面板 / 详情抽屉 / 筛选侧栏" | `Sheet` | `<Sheet>` + `<SheetContent side="right">` |
88
+ | "移动端底部抽屉 / 半屏" | `Drawer` | Drawer + DrawerContent |
89
+ | "主框架 / 后台外壳" | `SidebarProvider` + `Sidebar` | SidebarProvider 包根Sidebar + SidebarMenu + SidebarMenuItem 拼侧栏 main 区域是主内容 |
90
+ | "空状态 / 暂无数据" | `Empty` | Empty EmptyHeader → EmptyMedia + EmptyTitle + EmptyDescription → EmptyContent(Button) |
91
+ | "加载中骨架" | `Skeleton` | 拼网格匹配实际布局 |
92
+ | "加载中转圈" | `Spinner` | 在按钮里配 `data-icon` + `disabled` |
93
+ | "命令面板 / 快速跳转 / Cmd+K" | `Dialog` + `Command` | Dialog 包外,里面 Command + CommandInput + CommandList + CommandGroup + CommandItem |
94
+ | "下拉菜单(点开)" | `DropdownMenu` | 点击触发 |
95
+ | "右键菜单" | `ContextMenu` | 长按 / 右键触发 |
96
+ | "应用顶部菜单条" | `Menubar` | macOS 顶部菜单 |
97
+ | "面包屑导航" | `Breadcrumb` | BreadcrumbList BreadcrumbItem BreadcrumbLink |
98
+ | "分页" | `Pagination` | PaginationContent → PaginationItem → PaginationLink/Previous/Next |
99
+ | "标签页" | `Tabs` | Tabs → TabsList → TabsTrigger → TabsContent |
100
+ | "可折叠区块" | `Collapsible`(单个)或 `Accordion`(多个分组) | Accordion 用于 FAQ;Collapsible 用于单个开关区 |
101
+ | "悬浮提示" | `Tooltip` | TooltipTrigger + TooltipContent,可配 `Kbd` |
102
+ | "悬浮卡片 / 用户名 hover 预览" | `HoverCard` | HoverCardTrigger + HoverCardContent |
103
+ | "点击弹出小卡片 / 颜色 / 日期" | `Popover` | PopoverTrigger + PopoverContent |
104
+ | "Toast / 通知 / 短反馈" | `toast()` from `sonner` | 根上挂一次 `<Toaster />`,业务里直接调 `toast.success(...)` |
105
+ | "进度条" | `Progress` | 已知进度用 Progress;未知用 Spinner |
106
+ | "标签 / 状态徽章" | `Badge` | variant: default / secondary / destructive / outline |
107
+ | "可搜索下拉 / 自动补全" | `Popover` + `Command` | PopoverTrigger 触发 → PopoverContent 包 Command + CommandInput + CommandList |
108
+ | "下拉选项(不搜索)" | `Select` | inline SelectItem,详见 [rules/base-vs-radix.md](./rules/base-vs-radix.md) |
109
+ | "日期选择器" | `Popover` + `Calendar` | PopoverTrigger(Button) → PopoverContent 包 Calendar |
110
+ | "纯日历视图" | `Calendar` | 渲染月历 |
111
+ | "主题切换按钮" | `Button` + `next-themes` | 见下 [完整代码示例](#场景--完整代码示例) |
112
+ | "设置抽屉(侧拉式)" | `Sheet` | Sheet + SheetContent 包 Tabs / Field 表单 |
113
+ | "OTP / 验证码输入" | `InputOTP` | 4-6 位分格输入 |
114
+ | "评分滑块 / 调音量" | `Slider` | **value 必须是数组**: `[50]` 不是 `50` |
115
+ | "可调整大小的面板" | `Resizable` | ResizablePanelGroup + ResizablePanel + ResizableHandle |
116
+ | "长内容滚动" | `ScrollArea` | 自定义滚动条样式 |
117
+ | "图片占位(保持比例)" | `AspectRatio` | 包图片避免布局抖动 |
118
+ | "辐射 / 分割" | `Separator` | 替代 `<hr>` 和带 border 的 div |
119
+ | "图标按钮分组" | `ButtonGroup` 或 `ToggleGroup` | 互斥用 `ToggleGroup type="single"`;并列动作用 `ButtonGroup` |
120
+
121
+ ### 场景 → 完整代码示例
122
+
123
+ #### 登录表单
124
+
125
+ ```tsx
126
+ <Card className="mx-auto max-w-sm">
127
+ <CardHeader>
128
+ <CardTitle>登录</CardTitle>
129
+ <CardDescription>使用邮箱和密码登录</CardDescription>
130
+ </CardHeader>
131
+ <CardContent>
132
+ <Form {...form}>
133
+ <form onSubmit={form.handleSubmit(onSubmit)} className="flex flex-col gap-4">
134
+ <FormField
135
+ control={form.control}
136
+ name="email"
137
+ render={({ field }) => (
138
+ <FormItem>
139
+ <FormLabel>邮箱</FormLabel>
140
+ <FormControl><Input {...field} /></FormControl>
141
+ <FormMessage />
142
+ </FormItem>
143
+ )}
144
+ />
145
+ <FormField
146
+ control={form.control}
147
+ name="password"
148
+ render={({ field }) => (
149
+ <FormItem>
150
+ <FormLabel>密码</FormLabel>
151
+ <FormControl><Input type="password" {...field} /></FormControl>
152
+ <FormMessage />
153
+ </FormItem>
154
+ )}
155
+ />
156
+ <Button type="submit" disabled={form.formState.isSubmitting}>
157
+ {form.formState.isSubmitting && <Spinner data-icon="inline-start" />}
158
+ 登录
159
+ </Button>
160
+ </form>
161
+ </Form>
162
+ </CardContent>
163
+ </Card>
164
+ ```
165
+
166
+ #### 删除确认
167
+
168
+ ```tsx
169
+ <AlertDialog>
170
+ <AlertDialogTrigger asChild>
171
+ <Button variant="destructive">删除项目</Button>
172
+ </AlertDialogTrigger>
173
+ <AlertDialogContent>
174
+ <AlertDialogHeader>
175
+ <AlertDialogTitle>确认删除?</AlertDialogTitle>
176
+ <AlertDialogDescription>
177
+ 此操作不可撤销。项目下的所有数据会一并删除。
178
+ </AlertDialogDescription>
179
+ </AlertDialogHeader>
180
+ <AlertDialogFooter>
181
+ <AlertDialogCancel>取消</AlertDialogCancel>
182
+ <AlertDialogAction onClick={onConfirm}>删除</AlertDialogAction>
183
+ </AlertDialogFooter>
184
+ </AlertDialogContent>
185
+ </AlertDialog>
186
+ ```
187
+
188
+ #### 命令面板(Cmd+K)
189
+
190
+ ```tsx
191
+ <Dialog open={open} onOpenChange={setOpen}>
192
+ <DialogContent className="p-0">
193
+ <DialogTitle className="sr-only">命令面板</DialogTitle>
194
+ <Command>
195
+ <CommandInput placeholder="输入命令或搜索..." />
196
+ <CommandList>
197
+ <CommandEmpty>没有匹配结果。</CommandEmpty>
198
+ <CommandGroup heading="导航">
199
+ <CommandItem onSelect={() => router.push("/dashboard")}>
200
+ <LayoutDashboardIcon />
201
+ 仪表盘
202
+ </CommandItem>
203
+ <CommandItem onSelect={() => router.push("/settings")}>
204
+ <SettingsIcon />
205
+ 设置
206
+ </CommandItem>
207
+ </CommandGroup>
208
+ </CommandList>
209
+ </Command>
210
+ </DialogContent>
211
+ </Dialog>
212
+ ```
213
+
214
+ #### 仪表盘数据卡
215
+
216
+ ```tsx
217
+ <div className="grid gap-4 md:grid-cols-3">
218
+ <Card>
219
+ <CardHeader>
220
+ <CardDescription>总营收</CardDescription>
221
+ <CardTitle className="text-3xl">¥45,231</CardTitle>
222
+ </CardHeader>
223
+ <CardFooter>
224
+ <Badge variant="secondary">+20.1% 较上月</Badge>
225
+ </CardFooter>
226
+ </Card>
227
+ <Card>
228
+ <CardHeader>
229
+ <CardDescription>活跃用户</CardDescription>
230
+ <CardTitle className="text-3xl">2,350</CardTitle>
231
+ </CardHeader>
232
+ <CardFooter>
233
+ <Badge variant="secondary">+18.1% 较上月</Badge>
234
+ </CardFooter>
235
+ </Card>
236
+ <Card>
237
+ <CardHeader>
238
+ <CardDescription>转化率</CardDescription>
239
+ <CardTitle className="text-3xl">3.2%</CardTitle>
240
+ </CardHeader>
241
+ <CardFooter>
242
+ <span className="text-destructive text-sm">-2.4% 较上月</span>
243
+ </CardFooter>
244
+ </Card>
245
+ </div>
246
+ ```
247
+
248
+ #### 用户头像下拉
249
+
250
+ ```tsx
251
+ <DropdownMenu>
252
+ <DropdownMenuTrigger asChild>
253
+ <Button variant="ghost" className="size-8 rounded-full p-0">
254
+ <Avatar className="size-8">
255
+ <AvatarImage src={user.avatar} alt={user.name} />
256
+ <AvatarFallback>{user.initials}</AvatarFallback>
257
+ </Avatar>
258
+ </Button>
259
+ </DropdownMenuTrigger>
260
+ <DropdownMenuContent align="end">
261
+ <DropdownMenuLabel>{user.name}</DropdownMenuLabel>
262
+ <DropdownMenuSeparator />
263
+ <DropdownMenuGroup>
264
+ <DropdownMenuItem onSelect={() => router.push("/profile")}>
265
+ <UserIcon />
266
+ 个人资料
267
+ </DropdownMenuItem>
268
+ <DropdownMenuItem onSelect={() => router.push("/settings")}>
269
+ <SettingsIcon />
270
+ 设置
271
+ </DropdownMenuItem>
272
+ </DropdownMenuGroup>
273
+ <DropdownMenuSeparator />
274
+ <DropdownMenuItem onSelect={signOut}>
275
+ <LogOutIcon />
276
+ 退出
277
+ </DropdownMenuItem>
278
+ </DropdownMenuContent>
279
+ </DropdownMenu>
280
+ ```
281
+
282
+ #### 空状态
283
+
284
+ ```tsx
285
+ <Empty>
286
+ <EmptyHeader>
287
+ <EmptyMedia variant="icon"><FolderIcon /></EmptyMedia>
288
+ <EmptyTitle>还没有项目</EmptyTitle>
289
+ <EmptyDescription>创建你的第一个项目开始使用。</EmptyDescription>
290
+ </EmptyHeader>
291
+ <EmptyContent>
292
+ <Button>新建项目</Button>
293
+ </EmptyContent>
294
+ </Empty>
295
+ ```
296
+
297
+ #### 设置页(分页布局)
298
+
299
+ ```tsx
300
+ <Tabs defaultValue="account" className="flex flex-col gap-6">
301
+ <TabsList>
302
+ <TabsTrigger value="account">账户</TabsTrigger>
303
+ <TabsTrigger value="notifications">通知</TabsTrigger>
304
+ <TabsTrigger value="appearance">外观</TabsTrigger>
305
+ </TabsList>
306
+ <TabsContent value="account">
307
+ <FieldGroup>
308
+ <Field orientation="horizontal">
309
+ <FieldLabel>姓名</FieldLabel>
310
+ <Input defaultValue={user.name} />
311
+ </Field>
312
+ <Field orientation="horizontal">
313
+ <FieldLabel>邮箱</FieldLabel>
314
+ <Input type="email" defaultValue={user.email} />
315
+ </Field>
316
+ </FieldGroup>
317
+ </TabsContent>
318
+ <TabsContent value="notifications">
319
+ <FieldSet>
320
+ <FieldLegend>邮件通知</FieldLegend>
321
+ <FieldGroup>
322
+ <Field orientation="horizontal">
323
+ <Switch id="weekly" />
324
+ <FieldLabel htmlFor="weekly">每周摘要</FieldLabel>
325
+ </Field>
326
+ <Field orientation="horizontal">
327
+ <Switch id="security" />
328
+ <FieldLabel htmlFor="security">安全提醒</FieldLabel>
329
+ </Field>
330
+ </FieldGroup>
331
+ </FieldSet>
332
+ </TabsContent>
333
+ </Tabs>
334
+ ```
335
+
336
+ #### 主题切换按钮
337
+
338
+ ```tsx
339
+ "use client";
340
+ import { Moon, Sun } from "lucide-react";
341
+ import { useTheme } from "next-themes";
342
+ import { Button } from "@openconsole/shadcn";
343
+
344
+ export function ThemeToggle() {
345
+ // 用 resolvedTheme,处于 System 模式时也能正确翻转。
346
+ const { resolvedTheme, setTheme } = useTheme();
347
+ return (
348
+ <Button
349
+ variant="outline"
350
+ size="icon"
351
+ onClick={() => setTheme(resolvedTheme === "dark" ? "light" : "dark")}
352
+ >
353
+ <Sun className="rotate-0 scale-100 transition-all dark:-rotate-90 dark:scale-0" />
354
+ <Moon className="absolute rotate-90 scale-0 transition-all dark:rotate-0 dark:scale-100" />
355
+ <span className="sr-only">切换主题</span>
356
+ </Button>
357
+ );
358
+ }
359
+ ```
360
+
361
+ #### 可搜索下拉
362
+
363
+ ```tsx
364
+ <Popover open={open} onOpenChange={setOpen}>
365
+ <PopoverTrigger asChild>
366
+ <Button variant="outline" role="combobox" aria-expanded={open} className="w-[200px] justify-between">
367
+ {value ? options.find((o) => o.value === value)?.label : "选择..."}
368
+ <ChevronsUpDownIcon data-icon="inline-end" />
369
+ </Button>
370
+ </PopoverTrigger>
371
+ <PopoverContent className="w-[200px] p-0">
372
+ <Command>
373
+ <CommandInput placeholder="搜索..." />
374
+ <CommandList>
375
+ <CommandEmpty>没有匹配项。</CommandEmpty>
376
+ <CommandGroup>
377
+ {options.map((option) => (
378
+ <CommandItem
379
+ key={option.value}
380
+ onSelect={() => {
381
+ setValue(option.value);
382
+ setOpen(false);
383
+ }}
384
+ >
385
+ <CheckIcon className={cn("mr-2", value === option.value ? "opacity-100" : "opacity-0")} />
386
+ {option.label}
387
+ </CommandItem>
388
+ ))}
389
+ </CommandGroup>
390
+ </CommandList>
391
+ </Command>
392
+ </PopoverContent>
393
+ </Popover>
394
+ ```
395
+
396
+ #### 日期选择器
397
+
398
+ ```tsx
399
+ <Popover>
400
+ <PopoverTrigger asChild>
401
+ <Button variant="outline" className={cn("justify-start text-left font-normal", !date && "text-muted-foreground")}>
402
+ <CalendarIcon data-icon="inline-start" />
403
+ {date ? format(date, "PPP") : "选择日期"}
404
+ </Button>
405
+ </PopoverTrigger>
406
+ <PopoverContent className="w-auto p-0">
407
+ <Calendar mode="single" selected={date} onSelect={setDate} initialFocus />
408
+ </PopoverContent>
409
+ </Popover>
410
+ ```
411
+
412
+ ---
413
+
414
+ ## 关键规则
415
+
416
+ 下面的规则**总是被强制**。看到违反就直接修。每条都链到一个
417
+ Incorrect/Correct 代码对照的细则文件。
418
+
419
+ ### 样式 & Tailwind [rules/styling.md](./rules/styling.md)
420
+
421
+ - **`className` 只管布局,不管样式**。从外面覆盖组件颜色或排版是错的。
422
+ - **不要 `space-x-*` / `space-y-*`**。改用 `flex … gap-*`。
423
+ - **宽高相等时用 `size-*`**。`size-10` 不是 `w-10 h-10`。
424
+ - **`truncate` 是简写**,不要 `overflow-hidden text-ellipsis whitespace-nowrap`。
425
+ - **别手写 `dark:` 颜色覆盖**。用语义 token。
426
+ - **状态色别用裸值**。用 `Badge` variant `text-destructive`。
427
+ - **条件 className `cn()`**(从 `@openconsole/shadcn` 导入)。
428
+ - **overlay 别加 z-index**(`Dialog`、`Popover`、`Tooltip` 等自管堆叠)。
429
+
430
+ ### 表单 [rules/forms.md](./rules/forms.md)
431
+
432
+ - **表单布局用 `FieldGroup` + `Field`**,绝不要 `<div className="space-y-*">`。
433
+ - **schema 用 `Form` + `FormField` + `FormItem` + `FormControl` + `FormMessage`**。
434
+ - **`InputGroup` `InputGroupInput` / `InputGroupTextarea`**。
435
+ - **输入框里的按钮用 `InputGroup` + `InputGroupAddon`**。
436
+ - **2–7 个互斥选项用 `ToggleGroup`**。
437
+ - **相关 checkbox / radio 分组用 `FieldSet` + `FieldLegend`**。
438
+ - **校验状态: `data-invalid` `Field` + `aria-invalid` 在 control**。
439
+
440
+ ### 组合 → [rules/composition.md](./rules/composition.md)
441
+
442
+ - **Item 一定在自己的 Group 里**(`SelectItem` `SelectGroup`,
443
+ `DropdownMenuItem` `DropdownMenuGroup`,`CommandItem` →
444
+ `CommandGroup`,`TabsTrigger` `TabsList`)。
445
+ - **`Dialog` / `Sheet` / `Drawer` 一定要有 Title**(视觉隐藏用 `className="sr-only"`)。
446
+ - **`Card` 用完整组合**: `CardHeader` / `CardTitle` / `CardDescription` /
447
+ `CardContent` / `CardFooter`。
448
+ - **`Avatar` 必须有 `AvatarFallback`**。
449
+ - **`Button` 没有 `isLoading` prop**: 用 `Spinner` + `data-icon` + `disabled` 拼。
450
+ - **自定义触发器用 `asChild`**。
451
+
452
+ ### 用组件,别堆裸标签 [rules/composition.md](./rules/composition.md)
453
+
454
+ - 提示框 → `Alert`。空状态 → `Empty`。Toast → `sonner` 的 `toast()`。
455
+ - `Separator` 替代 `<hr>`。`Skeleton` 替代 `animate-pulse` div。
456
+ - `Badge` 替代加样式的 span。`Kbd` 用于键盘提示。`Spinner` 替代手写转圈。
457
+
458
+ ### 图标 → [rules/icons.md](./rules/icons.md)
459
+
460
+ - **按钮里的图标用 `data-icon="inline-start"` / `"inline-end"`**。
461
+ - **组件内部图标不要加尺寸 class**(组件自管)。
462
+ - **图标当组件对象传**,别用字符串 key 查表。
463
+ - **按名字动态渲染用 `Icon`**(本包导出的 `lucide-react` 包装)。
464
+
465
+ ### API 形状 → [rules/base-vs-radix.md](./rules/base-vs-radix.md)
466
+
467
+ - 自定义 trigger `asChild`。
468
+ - `ToggleGroup` / `Accordion` 显式 `type="single"` 或 `type="multiple"`。
469
+ - `Slider` 的 `value` 永远是数组。
470
+ - `Select` 用 inline `<SelectItem>`,placeholder 在 `<SelectValue>` 上。
471
+
472
+ ---
473
+
474
+ ## 关键模式
475
+
476
+ ```tsx
477
+ // 表单布局: FieldGroup + Field(不是 div + Label)
478
+ <FieldGroup>
479
+ <Field>
480
+ <FieldLabel htmlFor="email">Email</FieldLabel>
481
+ <Input id="email" />
482
+ </Field>
483
+ </FieldGroup>
484
+
485
+ // 校验: data-invalid 在 Field, aria-invalid 在 control
486
+ <Field data-invalid>
487
+ <FieldLabel>Email</FieldLabel>
488
+ <Input aria-invalid />
489
+ <FieldDescription>Invalid email.</FieldDescription>
490
+ </Field>
491
+
492
+ // 按钮里的图标: data-icon, 不加尺寸
493
+ <Button>
494
+ <SearchIcon data-icon="inline-start" />
495
+ Search
496
+ </Button>
497
+
498
+ // 间距: gap, 不要 space-y / space-x
499
+ <div className="flex flex-col gap-4">
500
+
501
+ // 等宽高: size-*
502
+ <Avatar className="size-10">
503
+
504
+ // 状态色: Badge variant 或语义 token
505
+ <Badge variant="secondary">+20.1%</Badge>
506
+
507
+ // 条件 class: cn()
508
+ <div className={cn("flex items-center", isActive && "bg-primary text-primary-foreground")} />
509
+
510
+ // 不显示但 a11y-合规的 Dialog: sr-only 的 title
511
+ <Dialog>
512
+ <DialogContent>
513
+ <DialogTitle className="sr-only">Settings</DialogTitle>
514
+ </DialogContent>
515
+ </Dialog>
516
+
517
+ // 加载中的按钮: Spinner + data-icon + disabled
518
+ <Button disabled={isPending}>
519
+ {isPending && <Spinner data-icon="inline-start" />}
520
+ Save
521
+ </Button>
522
+ ```
523
+
524
+ ---
525
+
526
+ ## 导入
527
+
528
+ 所有东西从 `@openconsole/shadcn` 平铺导出,**入口只有这一个**。
529
+
530
+ ```ts
531
+ import {
532
+ // 原语
533
+ Button, Badge, Avatar, AvatarImage, AvatarFallback,
534
+ Card, CardHeader, CardTitle, CardDescription, CardContent, CardFooter,
535
+ Dialog, DialogTrigger, DialogContent, DialogTitle,
536
+ Sheet, SheetTrigger, SheetContent, SheetTitle,
537
+ Tabs, TabsList, TabsTrigger, TabsContent,
538
+ Tooltip, TooltipTrigger, TooltipContent,
539
+ Popover, PopoverTrigger, PopoverContent,
540
+ // 表单
541
+ Form, FormField, FormItem, FormLabel, FormControl, FormDescription, FormMessage,
542
+ useFormField,
543
+ FieldGroup, Field, FieldLabel, FieldDescription, FieldSet, FieldLegend,
544
+ Input, InputGroup, InputGroupInput, InputGroupAddon,
545
+ Select, SelectTrigger, SelectContent, SelectGroup, SelectItem, SelectValue,
546
+ // 开关类
547
+ Switch, Checkbox, RadioGroup, RadioGroupItem,
548
+ ToggleGroup, ToggleGroupItem,
549
+ // 反馈
550
+ Alert, AlertTitle, AlertDescription,
551
+ Empty, EmptyHeader, EmptyMedia, EmptyTitle, EmptyDescription, EmptyContent,
552
+ Skeleton, Spinner, Progress,
553
+ Toaster, // 根上挂一次,然后从 sonner 调 toast()
554
+ // 导航
555
+ Sidebar, SidebarProvider, SidebarTrigger, SidebarMenu, SidebarMenuItem,
556
+ Breadcrumb, NavigationMenu, Pagination,
557
+ // 浮层
558
+ DropdownMenu, ContextMenu, Menubar, HoverCard,
559
+ Command, CommandInput, CommandList, CommandGroup, CommandItem, CommandEmpty,
560
+ // 日期
561
+ Calendar,
562
+ // 工具
563
+ cn, useIsMobile, Icon, Direction, Kbd, KbdGroup,
564
+ } from "@openconsole/shadcn";
565
+
566
+ import { toast } from "sonner"; // toast() 在 sonner 里,不在本包
567
+ ```
568
+
569
+ 需要某个组件但本包没导出 —— 不要碰运气拼 import 路径,也不要从别处
570
+ 复制源码进来。在应用层用已有原语自己组合。
571
+
572
+ ---
573
+
574
+ ## 主题化
575
+
576
+ 详见 [customization.md](./customization.md)。短版本:
577
+
578
+ - 接入: app 全局 CSS `@import "@openconsole/shadcn/styles.css"`
579
+ —— 默认 token、`@theme inline` 映射、`@source` 注册等全部自带。
580
+ - 主题色由 CSS 变量驱动 —— `:root` / `.dark` 里的变量就改了所有组件。
581
+ - 颜色用 OKLCH(`oklch(L C H)`)。
582
+ - 覆盖默认 token: 在 `@import` 之后重新声明同名变量。
583
+ - 加自定义色: 同样在全局 CSS 里加 `:root` 变量 + 在 `@theme inline`
584
+ 里映射(Tailwind v4)。
585
+ - 亮 / 暗切换用 `next-themes` 的 `ThemeProvider` 包根 + `useTheme()`
586
+ 钩子写自己的切换按钮。
587
+
588
+ ---
589
+
590
+ ## 常见坑
591
+
592
+ - **本包没有的组件不要伪造**: `import { Calendar2 } from "@openconsole/shadcn"`
593
+ 这种碰运气的 import 永远不会成功。先翻 [导入](#导入) 或
594
+ [应用场景速查](#应用场景速查) 确认。
595
+ - **入口只有 `@openconsole/shadcn` 根**: 不存在 `@openconsole/shadcn/dialog`、
596
+ `@openconsole/shadcn/lib/utils` 这种 subpath import。
597
+ - **wrapper 用了 `useState` / `useEffect` / `onClick` 却忘了 `"use client"`**:
598
+ 每个交互原语本身已经标了,但你自己包出来的 wrapper 要自己加。
599
+ - **给 `DialogContent` `z-50`**: 浮层组件自己分层。手写 z-index 会
600
+ 破坏跟 `Tooltip` / `Toaster` 的堆叠顺序。
601
+ - **手写 `dark:bg-*`**: 跟主题 token 打架。用 `bg-background`、
602
+ `bg-muted`、`bg-card` 等。
603
+ - **`SelectItem` 写在 `SelectGroup` 外面**: TS 检查能过,但键盘导航
604
+ 和屏幕阅读器都坏。
605
+ - **`Dialog` 没有 `DialogTitle`**: 屏幕阅读器会报缺标题。不想显示就
606
+ `<DialogTitle className="sr-only">…</DialogTitle>`。
607
+ - **主题切换读 `theme` 而不是 `resolvedTheme`**: 用户处于 System 模式
608
+ 时 `theme === "system"`,naive 的 `theme === "dark" ? "light" : "dark"`
609
+ 第一次点会无动作。读 `resolvedTheme` 来决定切到哪边。
610
+
611
+ ---
612
+
613
+ ## 详细参考
614
+
615
+ - [rules/styling.md](./rules/styling.md) —— 语义色、variant、`className`、间距、`size-*`、`truncate`、暗色、`cn()`、z-index。
616
+ - [rules/forms.md](./rules/forms.md) —— `FieldGroup`、`Field`、`InputGroup`、`ToggleGroup`、`FieldSet`、校验状态、react-hook-form 整合。
617
+ - [rules/composition.md](./rules/composition.md) —— Group、overlay、`Card`、`Tabs`、`Avatar`、`Alert`、`Empty`、toast、`Separator`、`Skeleton`、`Badge`、按钮加载态。
618
+ - [rules/icons.md](./rules/icons.md) —— `data-icon`、图标尺寸、图标作为对象传递、`Icon` 名字查找。
619
+ - [rules/base-vs-radix.md](./rules/base-vs-radix.md) —— 本包 API 速查(`asChild`、`Select`、`ToggleGroup`、`Slider`、`Accordion`)。
620
+ - [customization.md](./customization.md) —— 主题、CSS 变量、新增自定义色。