@teamix-evo/ui 0.1.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 (270) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +336 -0
  3. package/_data.json +12 -0
  4. package/manifest.json +1688 -0
  5. package/package.json +90 -0
  6. package/src/components/accordion/accordion.meta.md +87 -0
  7. package/src/components/accordion/accordion.stories.tsx +67 -0
  8. package/src/components/accordion/accordion.tsx +58 -0
  9. package/src/components/affix/affix.meta.md +80 -0
  10. package/src/components/affix/affix.stories.tsx +57 -0
  11. package/src/components/affix/affix.tsx +97 -0
  12. package/src/components/alert/alert.meta.md +101 -0
  13. package/src/components/alert/alert.stories.tsx +93 -0
  14. package/src/components/alert/alert.tsx +132 -0
  15. package/src/components/alert-dialog/alert-dialog.meta.md +107 -0
  16. package/src/components/alert-dialog/alert-dialog.stories.tsx +81 -0
  17. package/src/components/alert-dialog/alert-dialog.tsx +136 -0
  18. package/src/components/anchor/anchor.meta.md +87 -0
  19. package/src/components/anchor/anchor.stories.tsx +74 -0
  20. package/src/components/anchor/anchor.tsx +130 -0
  21. package/src/components/app/app.meta.md +86 -0
  22. package/src/components/app/app.stories.tsx +62 -0
  23. package/src/components/app/app.tsx +58 -0
  24. package/src/components/aspect-ratio/aspect-ratio.meta.md +81 -0
  25. package/src/components/aspect-ratio/aspect-ratio.stories.tsx +59 -0
  26. package/src/components/aspect-ratio/aspect-ratio.tsx +22 -0
  27. package/src/components/auto-complete/auto-complete.meta.md +102 -0
  28. package/src/components/auto-complete/auto-complete.stories.tsx +93 -0
  29. package/src/components/auto-complete/auto-complete.tsx +205 -0
  30. package/src/components/avatar/avatar.meta.md +94 -0
  31. package/src/components/avatar/avatar.stories.tsx +80 -0
  32. package/src/components/avatar/avatar.tsx +126 -0
  33. package/src/components/badge/badge.meta.md +119 -0
  34. package/src/components/badge/badge.stories.tsx +153 -0
  35. package/src/components/badge/badge.tsx +210 -0
  36. package/src/components/breadcrumb/breadcrumb.meta.md +107 -0
  37. package/src/components/breadcrumb/breadcrumb.stories.tsx +84 -0
  38. package/src/components/breadcrumb/breadcrumb.tsx +122 -0
  39. package/src/components/button/button.meta.md +98 -0
  40. package/src/components/button/button.stories.tsx +235 -0
  41. package/src/components/button/button.tsx +160 -0
  42. package/src/components/button-group/button-group.meta.md +92 -0
  43. package/src/components/button-group/button-group.stories.tsx +90 -0
  44. package/src/components/button-group/button-group.tsx +75 -0
  45. package/src/components/calendar/calendar.meta.md +118 -0
  46. package/src/components/calendar/calendar.stories.tsx +68 -0
  47. package/src/components/calendar/calendar.tsx +107 -0
  48. package/src/components/card/card.meta.md +117 -0
  49. package/src/components/card/card.stories.tsx +112 -0
  50. package/src/components/card/card.tsx +222 -0
  51. package/src/components/carousel/carousel.meta.md +117 -0
  52. package/src/components/carousel/carousel.stories.tsx +84 -0
  53. package/src/components/carousel/carousel.tsx +224 -0
  54. package/src/components/cascader/cascader.meta.md +110 -0
  55. package/src/components/cascader/cascader.stories.tsx +108 -0
  56. package/src/components/cascader/cascader.tsx +198 -0
  57. package/src/components/checkbox/checkbox.meta.md +99 -0
  58. package/src/components/checkbox/checkbox.stories.tsx +130 -0
  59. package/src/components/checkbox/checkbox.tsx +125 -0
  60. package/src/components/collapsible/collapsible.meta.md +80 -0
  61. package/src/components/collapsible/collapsible.stories.tsx +35 -0
  62. package/src/components/collapsible/collapsible.tsx +18 -0
  63. package/src/components/color-picker/color-picker.meta.md +84 -0
  64. package/src/components/color-picker/color-picker.stories.tsx +80 -0
  65. package/src/components/color-picker/color-picker.tsx +160 -0
  66. package/src/components/combobox/combobox.meta.md +93 -0
  67. package/src/components/combobox/combobox.stories.tsx +55 -0
  68. package/src/components/combobox/combobox.tsx +130 -0
  69. package/src/components/command/command.meta.md +104 -0
  70. package/src/components/command/command.stories.tsx +59 -0
  71. package/src/components/command/command.tsx +147 -0
  72. package/src/components/context-menu/context-menu.meta.md +90 -0
  73. package/src/components/context-menu/context-menu.stories.tsx +46 -0
  74. package/src/components/context-menu/context-menu.tsx +191 -0
  75. package/src/components/data-table/data-table.meta.md +149 -0
  76. package/src/components/data-table/data-table.stories.tsx +125 -0
  77. package/src/components/data-table/data-table.tsx +185 -0
  78. package/src/components/date-picker/date-picker.meta.md +106 -0
  79. package/src/components/date-picker/date-picker.stories.tsx +58 -0
  80. package/src/components/date-picker/date-picker.tsx +156 -0
  81. package/src/components/descriptions/descriptions.meta.md +78 -0
  82. package/src/components/descriptions/descriptions.stories.tsx +60 -0
  83. package/src/components/descriptions/descriptions.tsx +129 -0
  84. package/src/components/dialog/dialog.meta.md +105 -0
  85. package/src/components/dialog/dialog.stories.tsx +93 -0
  86. package/src/components/dialog/dialog.tsx +128 -0
  87. package/src/components/drawer/drawer.meta.md +96 -0
  88. package/src/components/drawer/drawer.stories.tsx +54 -0
  89. package/src/components/drawer/drawer.tsx +114 -0
  90. package/src/components/dropdown-menu/dropdown-menu.meta.md +103 -0
  91. package/src/components/dropdown-menu/dropdown-menu.stories.tsx +112 -0
  92. package/src/components/dropdown-menu/dropdown-menu.tsx +195 -0
  93. package/src/components/empty/empty.meta.md +81 -0
  94. package/src/components/empty/empty.stories.tsx +46 -0
  95. package/src/components/empty/empty.tsx +47 -0
  96. package/src/components/field/field.meta.md +116 -0
  97. package/src/components/field/field.stories.tsx +117 -0
  98. package/src/components/field/field.tsx +164 -0
  99. package/src/components/flex/flex.meta.md +94 -0
  100. package/src/components/flex/flex.stories.tsx +112 -0
  101. package/src/components/flex/flex.tsx +122 -0
  102. package/src/components/float-button/float-button.meta.md +87 -0
  103. package/src/components/float-button/float-button.stories.tsx +78 -0
  104. package/src/components/float-button/float-button.tsx +143 -0
  105. package/src/components/form/form.meta.md +131 -0
  106. package/src/components/form/form.stories.tsx +122 -0
  107. package/src/components/form/form.tsx +194 -0
  108. package/src/components/grid/grid.meta.md +87 -0
  109. package/src/components/grid/grid.stories.tsx +99 -0
  110. package/src/components/grid/grid.tsx +130 -0
  111. package/src/components/hover-card/hover-card.meta.md +92 -0
  112. package/src/components/hover-card/hover-card.stories.tsx +68 -0
  113. package/src/components/hover-card/hover-card.tsx +29 -0
  114. package/src/components/image/image.meta.md +94 -0
  115. package/src/components/image/image.stories.tsx +55 -0
  116. package/src/components/image/image.tsx +138 -0
  117. package/src/components/input/input.meta.md +109 -0
  118. package/src/components/input/input.stories.tsx +117 -0
  119. package/src/components/input/input.tsx +213 -0
  120. package/src/components/input-group/input-group.meta.md +92 -0
  121. package/src/components/input-group/input-group.stories.tsx +88 -0
  122. package/src/components/input-group/input-group.tsx +107 -0
  123. package/src/components/input-number/input-number.meta.md +91 -0
  124. package/src/components/input-number/input-number.stories.tsx +87 -0
  125. package/src/components/input-number/input-number.tsx +210 -0
  126. package/src/components/input-otp/input-otp.meta.md +105 -0
  127. package/src/components/input-otp/input-otp.stories.tsx +65 -0
  128. package/src/components/input-otp/input-otp.tsx +97 -0
  129. package/src/components/item/item.meta.md +116 -0
  130. package/src/components/item/item.stories.tsx +113 -0
  131. package/src/components/item/item.tsx +171 -0
  132. package/src/components/kbd/kbd.meta.md +85 -0
  133. package/src/components/kbd/kbd.stories.tsx +70 -0
  134. package/src/components/kbd/kbd.tsx +81 -0
  135. package/src/components/label/label.meta.md +91 -0
  136. package/src/components/label/label.stories.tsx +87 -0
  137. package/src/components/label/label.tsx +66 -0
  138. package/src/components/masonry/masonry.meta.md +85 -0
  139. package/src/components/masonry/masonry.stories.tsx +66 -0
  140. package/src/components/masonry/masonry.tsx +59 -0
  141. package/src/components/mentions/mentions.meta.md +89 -0
  142. package/src/components/mentions/mentions.stories.tsx +75 -0
  143. package/src/components/mentions/mentions.tsx +237 -0
  144. package/src/components/menubar/menubar.meta.md +100 -0
  145. package/src/components/menubar/menubar.stories.tsx +81 -0
  146. package/src/components/menubar/menubar.tsx +232 -0
  147. package/src/components/native-select/native-select.meta.md +88 -0
  148. package/src/components/native-select/native-select.stories.tsx +80 -0
  149. package/src/components/native-select/native-select.tsx +54 -0
  150. package/src/components/navigation-menu/navigation-menu.meta.md +108 -0
  151. package/src/components/navigation-menu/navigation-menu.stories.tsx +112 -0
  152. package/src/components/navigation-menu/navigation-menu.tsx +125 -0
  153. package/src/components/notification/notification.meta.md +91 -0
  154. package/src/components/notification/notification.stories.tsx +96 -0
  155. package/src/components/notification/notification.tsx +84 -0
  156. package/src/components/pagination/pagination.meta.md +127 -0
  157. package/src/components/pagination/pagination.stories.tsx +62 -0
  158. package/src/components/pagination/pagination.tsx +285 -0
  159. package/src/components/popconfirm/popconfirm.meta.md +109 -0
  160. package/src/components/popconfirm/popconfirm.stories.tsx +76 -0
  161. package/src/components/popconfirm/popconfirm.tsx +134 -0
  162. package/src/components/popover/popover.meta.md +97 -0
  163. package/src/components/popover/popover.stories.tsx +82 -0
  164. package/src/components/popover/popover.tsx +55 -0
  165. package/src/components/progress/progress.meta.md +86 -0
  166. package/src/components/progress/progress.stories.tsx +75 -0
  167. package/src/components/progress/progress.tsx +195 -0
  168. package/src/components/radio-group/radio-group.meta.md +103 -0
  169. package/src/components/radio-group/radio-group.stories.tsx +77 -0
  170. package/src/components/radio-group/radio-group.tsx +78 -0
  171. package/src/components/rate/rate.meta.md +87 -0
  172. package/src/components/rate/rate.stories.tsx +81 -0
  173. package/src/components/rate/rate.tsx +153 -0
  174. package/src/components/resizable/resizable.meta.md +92 -0
  175. package/src/components/resizable/resizable.stories.tsx +104 -0
  176. package/src/components/resizable/resizable.tsx +56 -0
  177. package/src/components/result/result.meta.md +90 -0
  178. package/src/components/result/result.stories.tsx +71 -0
  179. package/src/components/result/result.tsx +91 -0
  180. package/src/components/scroll-area/scroll-area.meta.md +84 -0
  181. package/src/components/scroll-area/scroll-area.stories.tsx +41 -0
  182. package/src/components/scroll-area/scroll-area.tsx +51 -0
  183. package/src/components/segmented/segmented.meta.md +103 -0
  184. package/src/components/segmented/segmented.stories.tsx +101 -0
  185. package/src/components/segmented/segmented.tsx +138 -0
  186. package/src/components/select/select.meta.md +110 -0
  187. package/src/components/select/select.stories.tsx +100 -0
  188. package/src/components/select/select.tsx +188 -0
  189. package/src/components/separator/separator.meta.md +74 -0
  190. package/src/components/separator/separator.stories.tsx +71 -0
  191. package/src/components/separator/separator.tsx +104 -0
  192. package/src/components/sheet/sheet.meta.md +97 -0
  193. package/src/components/sheet/sheet.stories.tsx +82 -0
  194. package/src/components/sheet/sheet.tsx +139 -0
  195. package/src/components/sidebar/sidebar.meta.md +131 -0
  196. package/src/components/sidebar/sidebar.stories.tsx +82 -0
  197. package/src/components/sidebar/sidebar.tsx +351 -0
  198. package/src/components/skeleton/skeleton.meta.md +95 -0
  199. package/src/components/skeleton/skeleton.stories.tsx +79 -0
  200. package/src/components/skeleton/skeleton.tsx +144 -0
  201. package/src/components/slider/slider.meta.md +94 -0
  202. package/src/components/slider/slider.stories.tsx +69 -0
  203. package/src/components/slider/slider.tsx +86 -0
  204. package/src/components/sonner/sonner.meta.md +96 -0
  205. package/src/components/sonner/sonner.stories.tsx +91 -0
  206. package/src/components/sonner/sonner.tsx +40 -0
  207. package/src/components/space/space.meta.md +94 -0
  208. package/src/components/space/space.stories.tsx +94 -0
  209. package/src/components/space/space.tsx +106 -0
  210. package/src/components/spinner/spinner.meta.md +76 -0
  211. package/src/components/spinner/spinner.stories.tsx +71 -0
  212. package/src/components/spinner/spinner.tsx +64 -0
  213. package/src/components/statistic/statistic.meta.md +99 -0
  214. package/src/components/statistic/statistic.stories.tsx +71 -0
  215. package/src/components/statistic/statistic.tsx +197 -0
  216. package/src/components/steps/steps.meta.md +102 -0
  217. package/src/components/steps/steps.stories.tsx +75 -0
  218. package/src/components/steps/steps.tsx +170 -0
  219. package/src/components/switch/switch.meta.md +92 -0
  220. package/src/components/switch/switch.stories.tsx +75 -0
  221. package/src/components/switch/switch.tsx +101 -0
  222. package/src/components/table/table.meta.md +95 -0
  223. package/src/components/table/table.stories.tsx +75 -0
  224. package/src/components/table/table.tsx +122 -0
  225. package/src/components/tabs/tabs.meta.md +98 -0
  226. package/src/components/tabs/tabs.stories.tsx +70 -0
  227. package/src/components/tabs/tabs.tsx +119 -0
  228. package/src/components/tag/tag.meta.md +94 -0
  229. package/src/components/tag/tag.stories.tsx +77 -0
  230. package/src/components/tag/tag.tsx +185 -0
  231. package/src/components/textarea/textarea.meta.md +83 -0
  232. package/src/components/textarea/textarea.stories.tsx +63 -0
  233. package/src/components/textarea/textarea.tsx +113 -0
  234. package/src/components/time-picker/time-picker.meta.md +83 -0
  235. package/src/components/time-picker/time-picker.stories.tsx +59 -0
  236. package/src/components/time-picker/time-picker.tsx +94 -0
  237. package/src/components/timeline/timeline.meta.md +102 -0
  238. package/src/components/timeline/timeline.stories.tsx +104 -0
  239. package/src/components/timeline/timeline.tsx +147 -0
  240. package/src/components/toggle/toggle.meta.md +88 -0
  241. package/src/components/toggle/toggle.stories.tsx +66 -0
  242. package/src/components/toggle/toggle.tsx +53 -0
  243. package/src/components/toggle-group/toggle-group.meta.md +90 -0
  244. package/src/components/toggle-group/toggle-group.stories.tsx +83 -0
  245. package/src/components/toggle-group/toggle-group.tsx +78 -0
  246. package/src/components/tooltip/tooltip.meta.md +99 -0
  247. package/src/components/tooltip/tooltip.stories.tsx +71 -0
  248. package/src/components/tooltip/tooltip.tsx +93 -0
  249. package/src/components/tour/tour.meta.md +116 -0
  250. package/src/components/tour/tour.stories.tsx +66 -0
  251. package/src/components/tour/tour.tsx +242 -0
  252. package/src/components/transfer/transfer.meta.md +90 -0
  253. package/src/components/transfer/transfer.stories.tsx +68 -0
  254. package/src/components/transfer/transfer.tsx +251 -0
  255. package/src/components/tree/tree.meta.md +111 -0
  256. package/src/components/tree/tree.stories.tsx +109 -0
  257. package/src/components/tree/tree.tsx +367 -0
  258. package/src/components/tree-select/tree-select.meta.md +100 -0
  259. package/src/components/tree-select/tree-select.stories.tsx +80 -0
  260. package/src/components/tree-select/tree-select.tsx +171 -0
  261. package/src/components/typography/typography.meta.md +102 -0
  262. package/src/components/typography/typography.stories.tsx +115 -0
  263. package/src/components/typography/typography.tsx +245 -0
  264. package/src/components/upload/upload.meta.md +111 -0
  265. package/src/components/upload/upload.stories.tsx +75 -0
  266. package/src/components/upload/upload.tsx +265 -0
  267. package/src/components/watermark/watermark.meta.md +95 -0
  268. package/src/components/watermark/watermark.stories.tsx +78 -0
  269. package/src/components/watermark/watermark.tsx +165 -0
  270. package/src/utils/cn.ts +6 -0
@@ -0,0 +1,351 @@
1
+ import * as React from 'react';
2
+ import { Slot } from '@radix-ui/react-slot';
3
+ import { cva, type VariantProps } from 'class-variance-authority';
4
+ import { PanelLeft } from 'lucide-react';
5
+
6
+ import { cn } from '@/utils/cn';
7
+ import { Button } from '@/components/button/button';
8
+ import { Separator } from '@/components/separator/separator';
9
+
10
+ // ─── Context + Provider ──────────────────────────────────────────────────────
11
+
12
+ interface SidebarContextValue {
13
+ open: boolean;
14
+ setOpen: (open: boolean) => void;
15
+ toggle: () => void;
16
+ }
17
+
18
+ const SidebarContext = React.createContext<SidebarContextValue | null>(null);
19
+
20
+ export function useSidebar() {
21
+ const ctx = React.useContext(SidebarContext);
22
+ if (!ctx) {
23
+ throw new Error('useSidebar must be used within <SidebarProvider>');
24
+ }
25
+ return ctx;
26
+ }
27
+
28
+ export interface SidebarProviderProps
29
+ extends React.HTMLAttributes<HTMLDivElement> {
30
+ /** 受控 open 状态。 */
31
+ open?: boolean;
32
+ /** uncontrolled 初始状态。 @default true */
33
+ defaultOpen?: boolean;
34
+ /** open 状态变化回调。 */
35
+ onOpenChange?: (open: boolean) => void;
36
+ }
37
+
38
+ const SidebarProvider = React.forwardRef<HTMLDivElement, SidebarProviderProps>(
39
+ (
40
+ {
41
+ defaultOpen = true,
42
+ open: openProp,
43
+ onOpenChange,
44
+ className,
45
+ children,
46
+ ...props
47
+ },
48
+ ref,
49
+ ) => {
50
+ const [internal, setInternal] = React.useState(defaultOpen);
51
+ const isControlled = openProp !== undefined;
52
+ const open = isControlled ? (openProp as boolean) : internal;
53
+
54
+ const setOpen = React.useCallback(
55
+ (next: boolean) => {
56
+ if (!isControlled) setInternal(next);
57
+ onOpenChange?.(next);
58
+ },
59
+ [isControlled, onOpenChange],
60
+ );
61
+ const toggle = React.useCallback(() => setOpen(!open), [open, setOpen]);
62
+
63
+ const value = React.useMemo<SidebarContextValue>(
64
+ () => ({ open, setOpen, toggle }),
65
+ [open, setOpen, toggle],
66
+ );
67
+
68
+ return (
69
+ <SidebarContext.Provider value={value}>
70
+ <div
71
+ ref={ref}
72
+ data-state={open ? 'expanded' : 'collapsed'}
73
+ className={cn(
74
+ 'group/sidebar-wrapper flex min-h-svh w-full',
75
+ className,
76
+ )}
77
+ {...props}
78
+ >
79
+ {children}
80
+ </div>
81
+ </SidebarContext.Provider>
82
+ );
83
+ },
84
+ );
85
+ SidebarProvider.displayName = 'SidebarProvider';
86
+
87
+ // ─── Main Sidebar container ───────────────────────────────────────────────────
88
+
89
+ const sidebarVariants = cva(
90
+ 'flex h-svh shrink-0 flex-col border-r bg-background transition-[width] duration-200 ease-linear',
91
+ {
92
+ variants: {
93
+ side: {
94
+ left: 'border-r',
95
+ right: 'border-l border-r-0 order-last',
96
+ },
97
+ },
98
+ defaultVariants: { side: 'left' },
99
+ },
100
+ );
101
+
102
+ export interface SidebarProps
103
+ extends React.HTMLAttributes<HTMLElement>,
104
+ VariantProps<typeof sidebarVariants> {
105
+ /**
106
+ * 侧边方向。
107
+ * @default "left"
108
+ */
109
+ side?: 'left' | 'right';
110
+ /**
111
+ * 折叠模式 — `icon` 折叠后只剩图标列(64px);`offcanvas` 完全收起(0px)。
112
+ * @default "icon"
113
+ */
114
+ collapsible?: 'icon' | 'offcanvas' | 'none';
115
+ /** 展开宽度。 @default "16rem" */
116
+ width?: string;
117
+ /** 折叠后宽度(`collapsible="icon"` 时生效)。 @default "3rem" */
118
+ collapsedWidth?: string;
119
+ }
120
+
121
+ const Sidebar = React.forwardRef<HTMLElement, SidebarProps>(
122
+ (
123
+ {
124
+ side = 'left',
125
+ collapsible = 'icon',
126
+ width = '16rem',
127
+ collapsedWidth = '3rem',
128
+ className,
129
+ style,
130
+ children,
131
+ ...props
132
+ },
133
+ ref,
134
+ ) => {
135
+ const { open } = useSidebar();
136
+ const w =
137
+ collapsible === 'none'
138
+ ? width
139
+ : open
140
+ ? width
141
+ : collapsible === 'icon'
142
+ ? collapsedWidth
143
+ : '0px';
144
+ return (
145
+ <aside
146
+ ref={ref}
147
+ data-state={open ? 'expanded' : 'collapsed'}
148
+ data-collapsible={collapsible}
149
+ data-side={side}
150
+ style={{ width: w, ...style }}
151
+ className={cn(
152
+ sidebarVariants({ side }),
153
+ collapsible === 'offcanvas' && !open && 'overflow-hidden',
154
+ className,
155
+ )}
156
+ {...props}
157
+ >
158
+ {children}
159
+ </aside>
160
+ );
161
+ },
162
+ );
163
+ Sidebar.displayName = 'Sidebar';
164
+
165
+ // ─── Sub-components ──────────────────────────────────────────────────────────
166
+
167
+ const SidebarHeader = React.forwardRef<
168
+ HTMLDivElement,
169
+ React.HTMLAttributes<HTMLDivElement>
170
+ >(({ className, ...props }, ref) => (
171
+ <div
172
+ ref={ref}
173
+ className={cn('flex flex-col gap-2 p-2', className)}
174
+ {...props}
175
+ />
176
+ ));
177
+ SidebarHeader.displayName = 'SidebarHeader';
178
+
179
+ const SidebarFooter = React.forwardRef<
180
+ HTMLDivElement,
181
+ React.HTMLAttributes<HTMLDivElement>
182
+ >(({ className, ...props }, ref) => (
183
+ <div
184
+ ref={ref}
185
+ className={cn('mt-auto flex flex-col gap-2 p-2', className)}
186
+ {...props}
187
+ />
188
+ ));
189
+ SidebarFooter.displayName = 'SidebarFooter';
190
+
191
+ const SidebarSeparator = React.forwardRef<
192
+ React.ElementRef<typeof Separator>,
193
+ React.ComponentPropsWithoutRef<typeof Separator>
194
+ >(({ className, ...props }, ref) => (
195
+ <Separator
196
+ ref={ref}
197
+ className={cn('mx-2 w-auto bg-border', className)}
198
+ {...props}
199
+ />
200
+ ));
201
+ SidebarSeparator.displayName = 'SidebarSeparator';
202
+
203
+ const SidebarContent = React.forwardRef<
204
+ HTMLDivElement,
205
+ React.HTMLAttributes<HTMLDivElement>
206
+ >(({ className, ...props }, ref) => (
207
+ <div
208
+ ref={ref}
209
+ className={cn(
210
+ 'flex min-h-0 flex-1 flex-col gap-2 overflow-auto p-2',
211
+ className,
212
+ )}
213
+ {...props}
214
+ />
215
+ ));
216
+ SidebarContent.displayName = 'SidebarContent';
217
+
218
+ const SidebarGroup = React.forwardRef<
219
+ HTMLDivElement,
220
+ React.HTMLAttributes<HTMLDivElement>
221
+ >(({ className, ...props }, ref) => (
222
+ <div
223
+ ref={ref}
224
+ className={cn('flex w-full min-w-0 flex-col gap-1', className)}
225
+ {...props}
226
+ />
227
+ ));
228
+ SidebarGroup.displayName = 'SidebarGroup';
229
+
230
+ const SidebarGroupLabel = React.forwardRef<
231
+ HTMLDivElement,
232
+ React.HTMLAttributes<HTMLDivElement>
233
+ >(({ className, ...props }, ref) => {
234
+ const { open } = useSidebar();
235
+ return (
236
+ <div
237
+ ref={ref}
238
+ className={cn(
239
+ 'flex h-8 shrink-0 items-center px-2 text-xs font-medium text-muted-foreground',
240
+ !open && 'opacity-0',
241
+ className,
242
+ )}
243
+ {...props}
244
+ />
245
+ );
246
+ });
247
+ SidebarGroupLabel.displayName = 'SidebarGroupLabel';
248
+
249
+ const SidebarMenu = React.forwardRef<
250
+ HTMLUListElement,
251
+ React.HTMLAttributes<HTMLUListElement>
252
+ >(({ className, ...props }, ref) => (
253
+ <ul
254
+ ref={ref}
255
+ className={cn('flex w-full min-w-0 flex-col gap-1', className)}
256
+ {...props}
257
+ />
258
+ ));
259
+ SidebarMenu.displayName = 'SidebarMenu';
260
+
261
+ const SidebarMenuItem = React.forwardRef<
262
+ HTMLLIElement,
263
+ React.HTMLAttributes<HTMLLIElement>
264
+ >(({ className, ...props }, ref) => (
265
+ <li ref={ref} className={cn('group/menu-item', className)} {...props} />
266
+ ));
267
+ SidebarMenuItem.displayName = 'SidebarMenuItem';
268
+
269
+ export interface SidebarMenuButtonProps
270
+ extends React.ButtonHTMLAttributes<HTMLButtonElement> {
271
+ /** 当前激活态(高亮显示)。 */
272
+ isActive?: boolean;
273
+ /** 用 Slot 渲染为子元素(配合 router Link)。 */
274
+ asChild?: boolean;
275
+ }
276
+
277
+ const SidebarMenuButton = React.forwardRef<
278
+ HTMLButtonElement,
279
+ SidebarMenuButtonProps
280
+ >(({ className, isActive, asChild, ...props }, ref) => {
281
+ const Comp = asChild ? Slot : 'button';
282
+ return (
283
+ <Comp
284
+ ref={ref}
285
+ data-active={isActive ? 'true' : undefined}
286
+ className={cn(
287
+ 'flex h-8 w-full items-center gap-2 overflow-hidden rounded-md px-2 text-left text-sm outline-none ring-ring transition hover:bg-accent hover:text-accent-foreground focus-visible:ring-2 disabled:pointer-events-none disabled:opacity-50 data-[active=true]:bg-accent data-[active=true]:font-medium data-[active=true]:text-accent-foreground [&_svg]:size-4 [&_svg]:shrink-0',
288
+ className,
289
+ )}
290
+ {...props}
291
+ />
292
+ );
293
+ });
294
+ SidebarMenuButton.displayName = 'SidebarMenuButton';
295
+
296
+ // ─── Trigger ─────────────────────────────────────────────────────────────────
297
+
298
+ const SidebarTrigger = React.forwardRef<
299
+ HTMLButtonElement,
300
+ React.ComponentPropsWithoutRef<typeof Button>
301
+ >(({ className, onClick, ...props }, ref) => {
302
+ const { toggle } = useSidebar();
303
+ return (
304
+ <Button
305
+ ref={ref}
306
+ variant="ghost"
307
+ size="icon"
308
+ className={cn('size-7', className)}
309
+ onClick={(e) => {
310
+ onClick?.(e);
311
+ toggle();
312
+ }}
313
+ aria-label="Toggle Sidebar"
314
+ {...props}
315
+ >
316
+ <PanelLeft />
317
+ <span className="sr-only">Toggle Sidebar</span>
318
+ </Button>
319
+ );
320
+ });
321
+ SidebarTrigger.displayName = 'SidebarTrigger';
322
+
323
+ // ─── Inset(主内容区,与 Sidebar 配合)─────────────────────────────────────
324
+
325
+ const SidebarInset = React.forwardRef<
326
+ HTMLDivElement,
327
+ React.HTMLAttributes<HTMLDivElement>
328
+ >(({ className, ...props }, ref) => (
329
+ <main
330
+ ref={ref}
331
+ className={cn('relative flex min-h-svh flex-1 flex-col bg-background', className)}
332
+ {...(props as React.HTMLAttributes<HTMLElement>)}
333
+ />
334
+ ));
335
+ SidebarInset.displayName = 'SidebarInset';
336
+
337
+ export {
338
+ SidebarProvider,
339
+ Sidebar,
340
+ SidebarHeader,
341
+ SidebarFooter,
342
+ SidebarSeparator,
343
+ SidebarContent,
344
+ SidebarGroup,
345
+ SidebarGroupLabel,
346
+ SidebarMenu,
347
+ SidebarMenuItem,
348
+ SidebarMenuButton,
349
+ SidebarTrigger,
350
+ SidebarInset,
351
+ };
@@ -0,0 +1,95 @@
1
+ ---
2
+ id: skeleton
3
+ name: Skeleton
4
+ type: component
5
+ category: foundation
6
+ since: 0.1.0
7
+ package: "@teamix-evo/ui"
8
+ ---
9
+
10
+ # Skeleton
11
+
12
+ 骨架屏占位 — shadcn 单元素 pulse + antd 子组件家族(`SkeletonAvatar / SkeletonImage / SkeletonButton / SkeletonInput / SkeletonParagraph`)。
13
+
14
+ `motion-reduce:animate-none` 自动尊重用户的"减少动画"系统设置。
15
+
16
+ ## When to use
17
+
18
+ - 首屏 / 路由切换数据加载中
19
+ - 列表 / 卡片网格的预占位(避免布局抖动)
20
+ - 替代 Spinner 用于"内容形状已知"的等待场景
21
+
22
+ ## When NOT to use
23
+
24
+ - 加载时间已知 < 300ms → 直接渲染最终态
25
+ - 真正失败 / 空数据 → 用 `Empty` 或 `Alert`(v0.x)
26
+
27
+ ## Props
28
+
29
+ > 以下表格由 `pnpm --filter @teamix-evo/ui gen:meta` 自动生成。
30
+
31
+ <!-- auto:props:begin -->
32
+ | 名称 | 类型 | 默认值 | 必填 | 说明 |
33
+ | --- | --- | --- | --- | --- |
34
+ | `shape` | `'rect' \| 'circle' \| 'line'` | `"rect"` | – | 形状变体。`rect` 默认矩形;`circle` 圆形(用于 Avatar 占位);`line` 单行文本占位。 |
35
+ <!-- auto:props:end -->
36
+
37
+ ## 依赖
38
+
39
+ > 以下表格由 `pnpm --filter @teamix-evo/ui gen:meta` 自动生成,数据源是 [`manifest.json`](../../../manifest.json)。**手工编辑 marker 之间的内容会在下次生成时被覆盖**。
40
+
41
+ <!-- auto:deps:begin -->
42
+ ### 同库依赖
43
+
44
+ > `teamix-evo ui add skeleton` 时,以下 entry 会被自动连带安装(无需手动 add)。
45
+
46
+ | Entry | 类型 | 描述 |
47
+ | --- | --- | --- |
48
+ | `cn` | util | Tailwind className 合并工具(clsx + tailwind-merge) |
49
+
50
+ ### npm 依赖
51
+
52
+ > 业务侧需要先 `pnpm add` / `npm install` 这些包。CLI 在 `ui add` 完成后会列出此提示。
53
+
54
+ ```bash
55
+ pnpm add class-variance-authority@^0.7.0
56
+ ```
57
+ <!-- auto:deps:end -->
58
+
59
+ > 子组件 `SkeletonAvatar / SkeletonImage / SkeletonButton / SkeletonInput / SkeletonParagraph` 各自的 Props 可在 [`skeleton.tsx`](./skeleton.tsx) 查看。
60
+
61
+ ## AI 生成纪律
62
+
63
+ - **形状要与最终内容匹配**:文本用 `shape="line"`,头像用 `<SkeletonAvatar />`,图片用 `<SkeletonImage />`,**不要**全用矩形糊弄
64
+ - **段落用 SkeletonParagraph**:不要手写多个 `<Skeleton shape="line" />`,失去尾行收窄的自然观感
65
+ - **不要嵌套到 disabled state**:loading 状态优先用 Skeleton 替代整块,而非 Skeleton 叠加在 disabled 内容上
66
+ - **`aria-hidden` 已自动**:不要给 Skeleton 加文字 children 或 aria-label,屏幕阅读器应跳过
67
+
68
+ ## Examples
69
+
70
+ ```tsx
71
+ import {
72
+ Skeleton,
73
+ SkeletonAvatar,
74
+ SkeletonImage,
75
+ SkeletonParagraph,
76
+ } from '@/components/ui/skeleton';
77
+
78
+ // 基础
79
+ <Skeleton className="h-8 w-32" />
80
+
81
+ // 头像 + 双行
82
+ <div className="flex items-center gap-3">
83
+ <SkeletonAvatar />
84
+ <div className="flex flex-col gap-2">
85
+ <Skeleton shape="line" className="w-32" />
86
+ <Skeleton shape="line" className="w-24" />
87
+ </div>
88
+ </div>
89
+
90
+ // 卡片占位
91
+ <div className="rounded-lg border p-4">
92
+ <SkeletonImage />
93
+ <SkeletonParagraph rows={3} className="mt-3" />
94
+ </div>
95
+ ```
@@ -0,0 +1,79 @@
1
+ import type { Meta, StoryObj } from '@storybook/react';
2
+ import {
3
+ Skeleton,
4
+ SkeletonAvatar,
5
+ SkeletonImage,
6
+ SkeletonButton,
7
+ SkeletonInput,
8
+ SkeletonParagraph,
9
+ } from './skeleton';
10
+
11
+ const meta: Meta<typeof Skeleton> = {
12
+ title: '基础原语 · Foundation/Skeleton',
13
+ component: Skeleton,
14
+ tags: ['autodocs'],
15
+ parameters: {
16
+ docs: {
17
+ description: {
18
+ component:
19
+ '骨架屏 — 在内容加载过程中呈现占位图形,减少加载期间的视觉跳动。shadcn `Skeleton` 原子 + antd `Skeleton` 的能力并集:除原子 `Skeleton`(支持 `shape: rect | circle | line`)外,同时提供预设组合 `SkeletonAvatar` / `SkeletonImage` / `SkeletonButton` / `SkeletonInput` / `SkeletonParagraph`。视觉走 OpenTrek semantic tokens,所有样式来自 `@teamix-evo/design`,无 mock。',
20
+ },
21
+ },
22
+ },
23
+ argTypes: {
24
+ shape: { control: 'inline-radio', options: ['rect', 'circle', 'line'] },
25
+ },
26
+ args: { shape: 'rect', className: 'h-8 w-32' },
27
+ };
28
+
29
+ export default meta;
30
+ type Story = StoryObj<typeof Skeleton>;
31
+
32
+ export const Playground: Story = {};
33
+
34
+ export const Family: Story = {
35
+ parameters: { controls: { disable: true } },
36
+ render: () => (
37
+ <div className="flex flex-col gap-6">
38
+ <div className="flex items-center gap-3">
39
+ <SkeletonAvatar size="sm" />
40
+ <SkeletonAvatar />
41
+ <SkeletonAvatar size="lg" />
42
+ </div>
43
+ <div className="flex items-center gap-3">
44
+ <SkeletonButton size="sm" />
45
+ <SkeletonButton />
46
+ <SkeletonButton size="lg" />
47
+ </div>
48
+ <div className="w-72">
49
+ <SkeletonInput />
50
+ </div>
51
+ <div className="w-72">
52
+ <SkeletonImage />
53
+ </div>
54
+ </div>
55
+ ),
56
+ };
57
+
58
+ export const Card: Story = {
59
+ parameters: { controls: { disable: true } },
60
+ render: () => (
61
+ <div className="w-80 rounded-lg border p-4">
62
+ <SkeletonImage />
63
+ <SkeletonParagraph rows={3} className="mt-3" />
64
+ </div>
65
+ ),
66
+ };
67
+
68
+ export const ListItem: Story = {
69
+ parameters: { controls: { disable: true } },
70
+ render: () => (
71
+ <div className="flex w-72 items-center gap-3">
72
+ <SkeletonAvatar />
73
+ <div className="flex flex-1 flex-col gap-2">
74
+ <Skeleton shape="line" className="w-32" />
75
+ <Skeleton shape="line" className="w-24" />
76
+ </div>
77
+ </div>
78
+ ),
79
+ };
@@ -0,0 +1,144 @@
1
+ import * as React from 'react';
2
+ import { cva, type VariantProps } from 'class-variance-authority';
3
+
4
+ import { cn } from '@/utils/cn';
5
+
6
+ const skeletonVariants = cva(
7
+ 'animate-pulse rounded-md bg-muted motion-reduce:animate-none',
8
+ {
9
+ variants: {
10
+ shape: {
11
+ rect: '',
12
+ circle: 'rounded-full',
13
+ line: 'h-3',
14
+ },
15
+ },
16
+ defaultVariants: { shape: 'rect' },
17
+ },
18
+ );
19
+
20
+ export interface SkeletonProps
21
+ extends Omit<React.HTMLAttributes<HTMLDivElement>, 'children'>,
22
+ VariantProps<typeof skeletonVariants> {
23
+ /**
24
+ * 形状变体。`rect` 默认矩形;`circle` 圆形(用于 Avatar 占位);`line` 单行文本占位。
25
+ * @default "rect"
26
+ */
27
+ shape?: 'rect' | 'circle' | 'line';
28
+ }
29
+
30
+ const Skeleton = React.forwardRef<HTMLDivElement, SkeletonProps>(
31
+ ({ className, shape, ...props }, ref) => (
32
+ <div
33
+ ref={ref}
34
+ aria-hidden="true"
35
+ className={cn(skeletonVariants({ shape }), className)}
36
+ {...props}
37
+ />
38
+ ),
39
+ );
40
+ Skeleton.displayName = 'Skeleton';
41
+
42
+ // ─── antd-style sub-components(Avatar / Image / Button / Input / Paragraph)─
43
+
44
+ export interface SkeletonAvatarProps extends Omit<SkeletonProps, 'shape'> {
45
+ /** 尺寸:小 32 / 默认 40 / 大 64。 @default "default" */
46
+ size?: 'sm' | 'default' | 'lg';
47
+ }
48
+ const SkeletonAvatar = React.forwardRef<HTMLDivElement, SkeletonAvatarProps>(
49
+ ({ size = 'default', className, ...props }, ref) => {
50
+ const dim = size === 'sm' ? 'size-8' : size === 'lg' ? 'size-16' : 'size-10';
51
+ return (
52
+ <Skeleton
53
+ ref={ref}
54
+ shape="circle"
55
+ className={cn(dim, className)}
56
+ {...props}
57
+ />
58
+ );
59
+ },
60
+ );
61
+ SkeletonAvatar.displayName = 'SkeletonAvatar';
62
+
63
+ export interface SkeletonImageProps extends Omit<SkeletonProps, 'shape'> {}
64
+ const SkeletonImage = React.forwardRef<HTMLDivElement, SkeletonImageProps>(
65
+ ({ className, ...props }, ref) => (
66
+ <Skeleton
67
+ ref={ref}
68
+ shape="rect"
69
+ className={cn('aspect-video w-full', className)}
70
+ {...props}
71
+ />
72
+ ),
73
+ );
74
+ SkeletonImage.displayName = 'SkeletonImage';
75
+
76
+ export interface SkeletonButtonProps extends Omit<SkeletonProps, 'shape'> {
77
+ size?: 'sm' | 'default' | 'lg';
78
+ }
79
+ const SkeletonButton = React.forwardRef<HTMLDivElement, SkeletonButtonProps>(
80
+ ({ size = 'default', className, ...props }, ref) => {
81
+ const h = size === 'sm' ? 'h-8' : size === 'lg' ? 'h-10' : 'h-9';
82
+ return (
83
+ <Skeleton
84
+ ref={ref}
85
+ shape="rect"
86
+ className={cn(h, 'w-24', className)}
87
+ {...props}
88
+ />
89
+ );
90
+ },
91
+ );
92
+ SkeletonButton.displayName = 'SkeletonButton';
93
+
94
+ export interface SkeletonInputProps extends Omit<SkeletonProps, 'shape'> {}
95
+ const SkeletonInput = React.forwardRef<HTMLDivElement, SkeletonInputProps>(
96
+ ({ className, ...props }, ref) => (
97
+ <Skeleton
98
+ ref={ref}
99
+ shape="rect"
100
+ className={cn('h-9 w-full', className)}
101
+ {...props}
102
+ />
103
+ ),
104
+ );
105
+ SkeletonInput.displayName = 'SkeletonInput';
106
+
107
+ export interface SkeletonParagraphProps
108
+ extends Omit<SkeletonProps, 'shape'> {
109
+ /**
110
+ * 段落行数。最后一行宽度自动收窄至 60%,模拟自然段尾。
111
+ * @default 3
112
+ */
113
+ rows?: number;
114
+ }
115
+ const SkeletonParagraph = React.forwardRef<
116
+ HTMLDivElement,
117
+ SkeletonParagraphProps
118
+ >(({ rows = 3, className, ...props }, ref) => (
119
+ <div
120
+ ref={ref}
121
+ className={cn('flex flex-col gap-2', className)}
122
+ aria-hidden="true"
123
+ {...props}
124
+ >
125
+ {Array.from({ length: rows }).map((_, i) => (
126
+ <Skeleton
127
+ key={i}
128
+ shape="line"
129
+ className={i === rows - 1 ? 'w-3/5' : 'w-full'}
130
+ />
131
+ ))}
132
+ </div>
133
+ ));
134
+ SkeletonParagraph.displayName = 'SkeletonParagraph';
135
+
136
+ export {
137
+ Skeleton,
138
+ SkeletonAvatar,
139
+ SkeletonImage,
140
+ SkeletonButton,
141
+ SkeletonInput,
142
+ SkeletonParagraph,
143
+ skeletonVariants,
144
+ };