@teamix-evo/ui 0.3.0 → 0.4.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.
Files changed (42) hide show
  1. package/manifest.json +20 -0
  2. package/package.json +3 -3
  3. package/src/components/button/button.tsx +18 -14
  4. package/src/components/card/card.tsx +7 -6
  5. package/src/components/cascader/cascader.tsx +12 -5
  6. package/src/components/checkbox/checkbox.tsx +4 -2
  7. package/src/components/date-picker/date-picker.tsx +2 -2
  8. package/src/components/dialog/dialog.tsx +1 -1
  9. package/src/components/filter-bar/filter-bar.stories.tsx +4 -1
  10. package/src/components/form/form.stories.tsx +7 -4
  11. package/src/components/input/input.tsx +2 -2
  12. package/src/components/input-group/input-group.tsx +2 -2
  13. package/src/components/input-number/input-number.tsx +2 -2
  14. package/src/components/native-select/native-select.tsx +2 -2
  15. package/src/components/page-header/page-header.meta.md +3 -1
  16. package/src/components/page-header/page-header.stories.tsx +8 -1
  17. package/src/components/page-header/page-header.tsx +7 -4
  18. package/src/components/page-shell/page-shell.meta.md +116 -0
  19. package/src/components/page-shell/page-shell.stories.tsx +149 -0
  20. package/src/components/page-shell/page-shell.tsx +115 -0
  21. package/src/components/pagination/pagination.tsx +24 -34
  22. package/src/components/segmented/segmented.tsx +1 -1
  23. package/src/components/select/select.tsx +2 -2
  24. package/src/components/sidebar/sidebar.meta.md +1 -0
  25. package/src/components/sidebar/sidebar.tsx +46 -17
  26. package/src/components/slider/slider.tsx +1 -1
  27. package/src/components/table/table.tsx +4 -2
  28. package/src/components/textarea/textarea.tsx +1 -1
  29. package/src/components/time-picker/time-picker.tsx +2 -2
  30. package/src/utils/trigger-input.ts +10 -6
  31. package/src/components/button/demo/as-child.tsx +0 -24
  32. package/src/components/button/demo/basic.tsx +0 -8
  33. package/src/components/button/demo/block.tsx +0 -16
  34. package/src/components/button/demo/loading.tsx +0 -19
  35. package/src/components/button/demo/shapes.tsx +0 -18
  36. package/src/components/button/demo/sizes.tsx +0 -19
  37. package/src/components/button/demo/variants.tsx +0 -19
  38. package/src/components/button/demo/with-icon.tsx +0 -20
  39. package/src/components/input/demo/basic.tsx +0 -12
  40. package/src/components/input/demo/clearable.tsx +0 -21
  41. package/src/components/input/demo/show-count.tsx +0 -18
  42. package/src/components/input/demo/sizes.tsx +0 -15
@@ -0,0 +1,149 @@
1
+ import * as React from 'react';
2
+ import type { Meta, StoryObj } from '@storybook/react-vite';
3
+ import { Bell, Home, Inbox, Settings, Users } from 'lucide-react';
4
+
5
+ import { PageShell } from './page-shell';
6
+ import {
7
+ Sidebar,
8
+ SidebarContent,
9
+ SidebarGroup,
10
+ SidebarHeader,
11
+ SidebarMenu,
12
+ SidebarMenuButton,
13
+ SidebarMenuItem,
14
+ SidebarTrigger,
15
+ } from '@/components/sidebar/sidebar';
16
+ import { Button } from '@/components/button/button';
17
+
18
+ const meta: Meta<typeof PageShell> = {
19
+ title: '布局 · Layout/PageShell',
20
+ component: PageShell,
21
+ tags: ['autodocs'],
22
+ parameters: {
23
+ layout: 'fullscreen',
24
+ docs: {
25
+ description: {
26
+ component:
27
+ 'PageShell — 页面三明治壳。`header` + `sidebar` + `children` 三 slot 任意组合;`background` prop 走 shadcn 语义槽枚举(`background` / `muted` / `card` / `sidebar` / `accent`),亮暗模式由 token 文件自动管。内部为传入的 ui `<Sidebar>` 注入 scoped CSS 覆盖默认 `position: fixed`,让 sidebar 跟 header 共存而不被撑到 viewport 顶端。',
28
+ },
29
+ },
30
+ },
31
+ argTypes: {
32
+ background: {
33
+ control: 'inline-radio',
34
+ options: ['background', 'muted', 'card', 'sidebar', 'accent'],
35
+ },
36
+ sidebarWidth: { control: 'text' },
37
+ },
38
+ };
39
+
40
+ export default meta;
41
+ type Story = StoryObj<typeof PageShell>;
42
+
43
+ // ─── 共享 demo 内容 ──────────────────────────────────────────────────────
44
+
45
+ function DemoHeader() {
46
+ return (
47
+ <header className="flex h-14 items-center justify-between border-b border-border bg-background px-6">
48
+ <span className="text-sm font-semibold">TopBar</span>
49
+ <Button variant="ghost" size="sm">
50
+ <Bell className="size-4" /> 通知
51
+ </Button>
52
+ </header>
53
+ );
54
+ }
55
+
56
+ function DemoSidebar() {
57
+ return (
58
+ <Sidebar collapsible="icon">
59
+ <SidebarHeader>
60
+ <div className="flex items-center justify-between px-2 py-2">
61
+ <span className="text-sm font-semibold group-data-[collapsible=icon]:hidden">
62
+ Logo
63
+ </span>
64
+ <SidebarTrigger />
65
+ </div>
66
+ </SidebarHeader>
67
+ <SidebarContent>
68
+ <SidebarGroup>
69
+ <SidebarMenu>
70
+ {[
71
+ { id: 'home', title: '首页', icon: Home },
72
+ { id: 'inbox', title: '收件箱', icon: Inbox },
73
+ { id: 'team', title: '团队', icon: Users },
74
+ { id: 'settings', title: '设置', icon: Settings },
75
+ ].map((it) => (
76
+ <SidebarMenuItem key={it.id}>
77
+ <SidebarMenuButton tooltip={it.title}>
78
+ <it.icon />
79
+ <span>{it.title}</span>
80
+ </SidebarMenuButton>
81
+ </SidebarMenuItem>
82
+ ))}
83
+ </SidebarMenu>
84
+ </SidebarGroup>
85
+ </SidebarContent>
86
+ </Sidebar>
87
+ );
88
+ }
89
+
90
+ function DemoMain({ label = '主区内容' }: { label?: string }) {
91
+ return (
92
+ <div className="px-6 py-4">
93
+ <h1 className="text-2xl font-semibold text-foreground">{label}</h1>
94
+ <p className="mt-2 text-sm text-muted-foreground">
95
+ PageShell 提供布局壳,具体内容由 PageContainer + 业务组件填充。
96
+ </p>
97
+ </div>
98
+ );
99
+ }
100
+
101
+ // ─── Stories ─────────────────────────────────────────────────────────────
102
+
103
+ /**
104
+ * Fullscreen — 不传 `header` 与 `sidebar`,只渲染 children。适合登录页 / 错误页 / 落地页。
105
+ */
106
+ export const Fullscreen: Story = {
107
+ args: { background: 'background' },
108
+ render: (args) => (
109
+ <PageShell {...args}>
110
+ <DemoMain label="全屏单区" />
111
+ </PageShell>
112
+ ),
113
+ };
114
+
115
+ /**
116
+ * WithHeader — 仅传 `header`,顶部条 + 全宽主区。
117
+ */
118
+ export const WithHeader: Story = {
119
+ args: { background: 'background' },
120
+ render: (args) => (
121
+ <PageShell {...args} header={<DemoHeader />}>
122
+ <DemoMain label="顶部 + 全宽主区" />
123
+ </PageShell>
124
+ ),
125
+ };
126
+
127
+ /**
128
+ * WithSidebar — 仅传 `sidebar`,左导 + 主区。无吊顶布局(opentrek 风格,主区背景常用 `muted`)。
129
+ */
130
+ export const WithSidebar: Story = {
131
+ args: { background: 'muted' },
132
+ render: (args) => (
133
+ <PageShell {...args} sidebar={<DemoSidebar />}>
134
+ <DemoMain label="左导 + 主区(muted 背景)" />
135
+ </PageShell>
136
+ ),
137
+ };
138
+
139
+ /**
140
+ * Complete — header + sidebar + children 完整三明治。切 controls 看不同 `background`。
141
+ */
142
+ export const Complete: Story = {
143
+ args: { background: 'background' },
144
+ render: (args) => (
145
+ <PageShell {...args} header={<DemoHeader />} sidebar={<DemoSidebar />}>
146
+ <DemoMain label="吊顶 + 左导 + 主区" />
147
+ </PageShell>
148
+ ),
149
+ };
@@ -0,0 +1,115 @@
1
+ /**
2
+ * PageShell — 页面三明治壳(layout-level composed component)
3
+ *
4
+ * 用 shadcn Sidebar primitives 组合一个"顶部 header(可选) + 左侧 sidebar(可选) + 主区"
5
+ * 的页面骨架。三个 slot 都可空,组合出四种页面形态:
6
+ *
7
+ * 1. 不传 header & sidebar → 全屏单区(登录页 / 错误页)
8
+ * 2. 仅 header → 顶部条 + 全宽主区
9
+ * 3. 仅 sidebar → 左导 + 主区(opentrek 那种无吊顶 layout)
10
+ * 4. header + sidebar → 吊顶 + 左导 + 主区(uni-manager 那种)
11
+ *
12
+ * 主区背景由 `background` prop 切换 — 用 shadcn 语义槽枚举,亮暗由 token 文件自动管。
13
+ *
14
+ * 传 `sidebar` 时,PageShell 内部用 `<SidebarProvider embedded>` —— ui Sidebar 的嵌入模式,
15
+ * sidebar-container 自动走 `position: relative + h-full`(替代默认 `fixed inset-y-0 h-svh`),
16
+ * 与 header 共存而不被撑到 viewport 顶端。
17
+ */
18
+ import * as React from 'react';
19
+
20
+ import { cn } from '@/utils/cn';
21
+ import { SidebarInset, SidebarProvider } from '@/components/sidebar/sidebar';
22
+
23
+ export type PageShellBackground =
24
+ | 'background'
25
+ | 'muted'
26
+ | 'card'
27
+ | 'sidebar'
28
+ | 'accent';
29
+
30
+ const BG_CLASS: Record<PageShellBackground, string> = {
31
+ background: 'bg-background',
32
+ muted: 'bg-muted',
33
+ card: 'bg-card',
34
+ sidebar: 'bg-sidebar',
35
+ accent: 'bg-accent',
36
+ };
37
+
38
+ export interface PageShellProps extends React.HTMLAttributes<HTMLDivElement> {
39
+ /** 顶部条 slot — 通常是 UmTopbar / 自建 navbar。不传则不渲染顶部。 */
40
+ header?: React.ReactNode;
41
+ /** 左侧 slot — 通常是 ui `<Sidebar>` 或基于它组合的整装件(如 OpSidebar)。不传则不渲染 sidebar 与 SidebarProvider。 */
42
+ sidebar?: React.ReactNode;
43
+ /** 主区内容。 */
44
+ children: React.ReactNode;
45
+ /**
46
+ * 主区背景 — 走 shadcn 语义槽枚举,亮暗模式由 token 文件自动切换。
47
+ * @default "background"
48
+ */
49
+ background?: PageShellBackground;
50
+ /**
51
+ * sidebar 宽度 — 透传给 SidebarProvider 的 `--sidebar-width` CSS 变量。仅在传 `sidebar` 时生效。
52
+ * @default "14rem"
53
+ */
54
+ sidebarWidth?: string;
55
+ }
56
+
57
+ const PageShell = React.forwardRef<HTMLDivElement, PageShellProps>(
58
+ function PageShell(
59
+ {
60
+ header,
61
+ sidebar,
62
+ children,
63
+ background = 'background',
64
+ sidebarWidth = '14rem',
65
+ className,
66
+ style,
67
+ ...rest
68
+ },
69
+ ref,
70
+ ) {
71
+ const bgClass = BG_CLASS[background];
72
+
73
+ if (!sidebar) {
74
+ return (
75
+ <div
76
+ ref={ref}
77
+ className={cn('flex min-h-svh w-full flex-col', className)}
78
+ style={style}
79
+ {...rest}
80
+ >
81
+ {header}
82
+ <main className={cn('flex-1 overflow-y-auto', bgClass)}>
83
+ {children}
84
+ </main>
85
+ </div>
86
+ );
87
+ }
88
+
89
+ return (
90
+ <SidebarProvider
91
+ ref={ref}
92
+ embedded
93
+ className={cn('!flex-col', className)}
94
+ style={
95
+ {
96
+ '--sidebar-width': sidebarWidth,
97
+ ...style,
98
+ } as React.CSSProperties
99
+ }
100
+ {...rest}
101
+ >
102
+ {header}
103
+ <div className="flex w-full flex-1 min-h-0">
104
+ {sidebar}
105
+ <SidebarInset className={cn('!min-h-0 overflow-y-auto', bgClass)}>
106
+ {children}
107
+ </SidebarInset>
108
+ </div>
109
+ </SidebarProvider>
110
+ );
111
+ },
112
+ );
113
+ PageShell.displayName = 'PageShell';
114
+
115
+ export { PageShell };
@@ -10,6 +10,8 @@ import { cva, type VariantProps } from 'class-variance-authority';
10
10
 
11
11
  import { cn } from '@/utils/cn';
12
12
  import { Button, type ButtonProps } from '@/components/button/button';
13
+ import { Input } from '@/components/input/input';
14
+ import { Select } from '@/components/select/select';
13
15
 
14
16
  // ─── 尺寸映射 — 对齐 Button/Input 的 h-7 / h-8 / h-9 三档 ──────────────────────
15
17
 
@@ -23,17 +25,14 @@ const sizeClass: Record<PaginationSize, string> = {
23
25
  };
24
26
 
25
27
  const minWClass: Record<PaginationSize, string> = {
26
- sm: 'min-w-7',
27
- md: 'min-w-8',
28
+ // sm 28px — 对齐 --button-sm-height
29
+ sm: 'min-w-[var(--button-sm-height)]',
30
+ // md 32px — 对齐 OpenTrek v4.1 --pagination-button-size
31
+ md: 'min-w-[var(--pagination-button-size)]',
32
+ // lg 36px — 仅 lg 档使用,保留 utility(无业务 token 对应)
28
33
  lg: 'min-w-9',
29
34
  };
30
35
 
31
- const inputHeightClass: Record<PaginationSize, string> = {
32
- sm: 'h-7 text-xs px-2',
33
- md: 'h-8 text-xs px-2.5',
34
- lg: 'h-9 text-sm px-3',
35
- };
36
-
37
36
  // ─── shadcn-style primitives ──────────────────────────────────────────────────
38
37
 
39
38
  const Pagination = ({ className, ...props }: React.ComponentProps<'nav'>) => (
@@ -296,8 +295,9 @@ const SimplePagination = ({
296
295
  onChange?.(n, finalPageSize);
297
296
  };
298
297
 
299
- const handlePageSizeChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
300
- const v = Number(e.target.value);
298
+ const handlePageSizeChange = (value: string | string[]) => {
299
+ const raw = Array.isArray(value) ? value[0] : value;
300
+ const v = Number(raw);
301
301
  if (Number.isNaN(v)) return;
302
302
  if (pageSize === undefined) setInnerPageSize(v);
303
303
  if (current === undefined) setInnerCurrent(1);
@@ -504,43 +504,33 @@ const SimplePagination = ({
504
504
  // ─── 附加 — pageSize 选择器 / 跳转(仅 normal)─────────────────────────────
505
505
  const showAddons = type === 'normal' && (pageSizeSelector || showJump);
506
506
 
507
+ const selectSize = size === 'lg' ? 'lg' : size === 'sm' ? 'sm' : 'md';
508
+
507
509
  const addons = showAddons ? (
508
510
  <div className={cn('flex items-center gap-2', sizeClass[size])}>
509
511
  {pageSizeSelector ? (
510
- <select
512
+ <Select
511
513
  aria-label="每页显示"
512
- value={finalPageSize}
514
+ size={selectSize}
515
+ value={String(finalPageSize)}
513
516
  onChange={handlePageSizeChange}
514
- className={cn(
515
- 'cursor-pointer rounded-lg border border-input bg-background shadow-sm',
516
- 'transition-colors hover:border-ring focus:outline-none focus:ring-1 focus:ring-ring',
517
- 'tabular-nums text-foreground',
518
- inputHeightClass[size],
519
- )}
520
- >
521
- {pageSizeList.map((opt) => (
522
- <option key={opt} value={opt}>
523
- {opt} 条/页
524
- </option>
525
- ))}
526
- </select>
517
+ options={pageSizeList.map((opt) => ({
518
+ label: `${opt} 条/页`,
519
+ value: String(opt),
520
+ }))}
521
+ />
527
522
  ) : null}
528
523
  {showJump ? (
529
524
  <span className="inline-flex items-center gap-1 text-muted-foreground">
530
525
  前往
531
- <input
532
- type="text"
526
+ <Input
527
+ size={selectSize}
533
528
  inputMode="numeric"
534
529
  value={jumperVal}
535
530
  onChange={(e) => setJumperVal(e.target.value)}
536
531
  onKeyDown={handleJumpKey}
537
- className={cn(
538
- 'w-12 rounded-lg border border-input bg-background text-center shadow-sm',
539
- 'transition-colors placeholder:text-muted-foreground',
540
- 'focus:outline-none focus:ring-1 focus:ring-ring',
541
- 'tabular-nums text-foreground',
542
- inputHeightClass[size],
543
- )}
532
+ className="w-12 text-center tabular-nums"
533
+ aria-label="跳转到页"
544
534
  />
545
535
 
546
536
  </span>
@@ -24,7 +24,7 @@ const itemVariants = cva(
24
24
  {
25
25
  variants: {
26
26
  active: {
27
- true: 'bg-background text-foreground shadow-sm',
27
+ true: 'bg-card text-foreground shadow-sm',
28
28
  false: 'text-muted-foreground hover:text-foreground',
29
29
  },
30
30
  disabled: {
@@ -354,8 +354,8 @@ const Select = React.forwardRef<HTMLButtonElement, SelectProps>(
354
354
  aria-invalid={error || undefined}
355
355
  disabled={disabled}
356
356
  className={cn(
357
- 'group flex w-panel-sm cursor-pointer items-center justify-between gap-2 rounded-md border border-input bg-background py-1 shadow-sm ring-offset-background transition-colors',
358
- 'focus:outline-none focus:ring-1 focus:ring-ring',
357
+ 'group flex w-panel-sm cursor-pointer items-center justify-between gap-2 rounded-md border border-input bg-card py-1 shadow-sm ring-offset-background transition-colors',
358
+ 'focus:outline-none hover:border-ring focus:border-ring focus:ring-2 focus:ring-ring/10',
359
359
  'disabled:cursor-not-allowed disabled:opacity-50',
360
360
  triggerSizeCls[size],
361
361
  error && 'border-destructive focus:ring-destructive',
@@ -70,6 +70,7 @@ pnpm add @radix-ui/react-slot@^1.1.0 class-variance-authority@^0.7.0 lucide-reac
70
70
  ## AI 生成纪律
71
71
 
72
72
  - **必装 SidebarProvider**:Sidebar 上下层任何子组件都依赖 Context;通常在 Layout 顶层包裹。Provider 内置 `TooltipProvider`,无需额外挂
73
+ - **`embedded` 嵌入模式**:Provider 加 `embedded` prop(默认 `false`),下游 Sidebar 的 sidebar-container 从默认 `fixed inset-y-0 h-svh`(贴 viewport 全屏)切到 `relative h-full`(嵌入外层布局)。常用于 PageShell / 任何需要 sidebar 与 header 共存的 layout。**消费方一般不直接传 `embedded`**,而是用 [`<PageShell>`](../page-shell/page-shell.meta.md) —— PageShell 内部自动开启。embedded 模式下 `collapsible="offcanvas"` 折叠改用宽度收 0,无滑出动画
73
74
  - **`SidebarMenuButton` 配 `asChild`**:wrap React Router / Next.js Link
74
75
  - **`SidebarMenuButton` 折叠态 tooltip**:`collapsible="icon"` 时务必传 `tooltip="导航名"`,否则折叠后只剩 icon 无法识别
75
76
  - **`isActive` 由路由判断**:不要用 useState 自管;`pathname === href`
@@ -56,6 +56,13 @@ interface SidebarContextValue {
56
56
  setOpenMobile: (open: boolean) => void;
57
57
  isMobile: boolean;
58
58
  toggleSidebar: () => void;
59
+ /**
60
+ * 嵌入模式 — true 时 Sidebar 的 sidebar-container 用 `position: relative; height: 100%`
61
+ * 替代默认的 `position: fixed; inset-y-0; height: 100svh`,让 sidebar 跟外层 header / 横向 flex
62
+ * 容器共存(常用于 PageShell 内嵌)。
63
+ * @default false
64
+ */
65
+ embedded: boolean;
59
66
  }
60
67
 
61
68
  const SidebarContext = React.createContext<SidebarContextValue | null>(null);
@@ -75,6 +82,13 @@ export interface SidebarProviderProps
75
82
  defaultOpen?: boolean;
76
83
  open?: boolean;
77
84
  onOpenChange?: (open: boolean) => void;
85
+ /**
86
+ * 嵌入模式 — true 时下游 `<Sidebar>` 的 sidebar-container 走 `position: relative + h-full`,
87
+ * 让 sidebar 嵌入到外层布局(如 PageShell 的 header 下 + 横向 flex 容器内),而不是默认贴
88
+ * viewport 全屏。默认 false(保留原版"贴 viewport 边" shadcn 行为)。
89
+ * @default false
90
+ */
91
+ embedded?: boolean;
78
92
  }
79
93
 
80
94
  const SidebarProvider = React.forwardRef<HTMLDivElement, SidebarProviderProps>(
@@ -83,6 +97,7 @@ const SidebarProvider = React.forwardRef<HTMLDivElement, SidebarProviderProps>(
83
97
  defaultOpen = true,
84
98
  open: openProp,
85
99
  onOpenChange: setOpenProp,
100
+ embedded = false,
86
101
  className,
87
102
  style,
88
103
  children,
@@ -139,6 +154,7 @@ const SidebarProvider = React.forwardRef<HTMLDivElement, SidebarProviderProps>(
139
154
  openMobile,
140
155
  setOpenMobile,
141
156
  toggleSidebar,
157
+ embedded,
142
158
  }),
143
159
  [
144
160
  state,
@@ -148,6 +164,7 @@ const SidebarProvider = React.forwardRef<HTMLDivElement, SidebarProviderProps>(
148
164
  openMobile,
149
165
  setOpenMobile,
150
166
  toggleSidebar,
167
+ embedded,
151
168
  ],
152
169
  );
153
170
 
@@ -199,7 +216,8 @@ const Sidebar = React.forwardRef<HTMLDivElement, SidebarProps>(
199
216
  },
200
217
  ref,
201
218
  ) => {
202
- const { isMobile, state, openMobile, setOpenMobile } = useSidebar();
219
+ const { isMobile, state, openMobile, setOpenMobile, embedded } =
220
+ useSidebar();
203
221
 
204
222
  if (collapsible === 'none') {
205
223
  return (
@@ -252,25 +270,36 @@ const Sidebar = React.forwardRef<HTMLDivElement, SidebarProps>(
252
270
  data-side={side}
253
271
  data-slot="sidebar"
254
272
  >
255
- {/* sidebar gap on desktop */}
256
- <div
257
- data-slot="sidebar-gap"
258
- className={cn(
259
- 'relative w-(--sidebar-width) bg-transparent transition-[width] duration-200 ease-linear',
260
- 'group-data-[collapsible=offcanvas]:w-0',
261
- 'group-data-[side=right]:rotate-180',
262
- variant === 'floating' || variant === 'inset'
263
- ? 'group-data-[collapsible=icon]:w-[calc(var(--sidebar-width-icon)+(--spacing(4)))]'
264
- : 'group-data-[collapsible=icon]:w-(--sidebar-width-icon)',
265
- )}
266
- />
273
+ {/* sidebar gap on desktop — 仅 fixed 模式需要;embedded 模式下 sidebar-container
274
+ 自身参与横向 flex,无需占位 */}
275
+ {!embedded && (
276
+ <div
277
+ data-slot="sidebar-gap"
278
+ className={cn(
279
+ 'relative w-(--sidebar-width) bg-transparent transition-[width] duration-200 ease-linear',
280
+ 'group-data-[collapsible=offcanvas]:w-0',
281
+ 'group-data-[side=right]:rotate-180',
282
+ variant === 'floating' || variant === 'inset'
283
+ ? 'group-data-[collapsible=icon]:w-[calc(var(--sidebar-width-icon)+(--spacing(4)))]'
284
+ : 'group-data-[collapsible=icon]:w-(--sidebar-width-icon)',
285
+ )}
286
+ />
287
+ )}
267
288
  <div
268
289
  data-slot="sidebar-container"
269
290
  data-side={side}
270
291
  className={cn(
271
- 'fixed inset-y-0 z-10 hidden h-svh w-(--sidebar-width) transition-[left,right,width] duration-200 ease-linear md:flex',
272
- 'data-[side=left]:left-0 data-[side=left]:group-data-[collapsible=offcanvas]:left-[calc(var(--sidebar-width)*-1)]',
273
- 'data-[side=right]:right-0 data-[side=right]:group-data-[collapsible=offcanvas]:right-[calc(var(--sidebar-width)*-1)]',
292
+ embedded
293
+ ? [
294
+ 'relative hidden h-full w-(--sidebar-width) transition-[width] duration-200 ease-linear md:flex',
295
+ // embedded 模式无 fixed,offcanvas 折叠改用宽度收 0(fixed 版本是滑走)
296
+ 'group-data-[collapsible=offcanvas]:w-0 overflow-hidden',
297
+ ]
298
+ : [
299
+ 'fixed inset-y-0 z-10 hidden h-svh w-(--sidebar-width) transition-[left,right,width] duration-200 ease-linear md:flex',
300
+ 'data-[side=left]:left-0 data-[side=left]:group-data-[collapsible=offcanvas]:left-[calc(var(--sidebar-width)*-1)]',
301
+ 'data-[side=right]:right-0 data-[side=right]:group-data-[collapsible=offcanvas]:right-[calc(var(--sidebar-width)*-1)]',
302
+ ],
274
303
  variant === 'floating' || variant === 'inset'
275
304
  ? 'p-2 group-data-[collapsible=icon]:w-[calc(var(--sidebar-width-icon)+(--spacing(4))+2px)]'
276
305
  : 'group-data-[collapsible=icon]:w-(--sidebar-width-icon) group-data-[side=left]:border-r group-data-[side=right]:border-l border-sidebar-border',
@@ -535,7 +564,7 @@ const SidebarMenu = React.forwardRef<
535
564
  ref={ref}
536
565
  data-slot="sidebar-menu"
537
566
  data-sidebar="menu"
538
- className={cn('flex w-full min-w-0 flex-col gap-0', className)}
567
+ className={cn('flex w-full min-w-0 flex-col gap-1', className)}
539
568
  {...props}
540
569
  />
541
570
  ));
@@ -168,7 +168,7 @@ const Slider = React.forwardRef<
168
168
  return (
169
169
  <SliderPrimitive.Thumb
170
170
  key={i}
171
- className="group relative z-10 block size-4 rounded-full border-2 border-primary bg-background shadow transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50"
171
+ className="group relative z-10 block size-4 rounded-full border-2 border-primary bg-card shadow transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50"
172
172
  >
173
173
  {tooltipVisible ? (
174
174
  <span
@@ -73,7 +73,8 @@ const TableHead = React.forwardRef<
73
73
  <th
74
74
  ref={ref}
75
75
  className={cn(
76
- 'h-10 px-3 text-left align-middle text-xs font-medium text-muted-foreground [&:has([role=checkbox])]:pr-0',
76
+ // OpenTrek v4.1 表格单元格水平 padding 走业务 token --table-cell-padding-x
77
+ 'h-10 px-[var(--table-cell-padding-x)] text-left align-middle text-xs font-medium text-muted-foreground [&:has([role=checkbox])]:pr-0',
77
78
  className,
78
79
  )}
79
80
  {...props}
@@ -88,7 +89,8 @@ const TableCell = React.forwardRef<
88
89
  <td
89
90
  ref={ref}
90
91
  className={cn(
91
- 'p-3 align-middle [&:has([role=checkbox])]:pr-0',
92
+ // 水平 padding 走 token,垂直保持 12px(与 cell-padding-x 同值)
93
+ 'px-[var(--table-cell-padding-x)] py-3 align-middle [&:has([role=checkbox])]:pr-0',
92
94
  className,
93
95
  )}
94
96
  {...props}
@@ -110,7 +110,7 @@ const Textarea = React.forwardRef<HTMLTextAreaElement, TextareaProps>(
110
110
  aria-invalid={ariaInvalid}
111
111
  onChange={handleChange}
112
112
  className={cn(
113
- 'flex w-full rounded-md border border-input bg-background px-3 py-2 shadow-sm placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50 aria-invalid:border-destructive aria-invalid:focus-visible:ring-destructive',
113
+ 'flex w-full rounded-md border border-input bg-card px-3 py-2 shadow-sm placeholder:text-muted-foreground focus-visible:outline-none hover:border-ring focus-visible:border-ring focus-visible:ring-2 focus-visible:ring-ring/10 disabled:cursor-not-allowed disabled:opacity-50 aria-invalid:border-destructive aria-invalid:focus-visible:ring-destructive/10',
114
114
  sizeClass,
115
115
  autoSize ? 'resize-none' : 'min-h-textarea',
116
116
  className,
@@ -545,7 +545,7 @@ const TimePicker = React.forwardRef<HTMLInputElement, TimePickerProps>(
545
545
  <PopoverContent
546
546
  align="start"
547
547
  sideOffset={6}
548
- className="w-auto rounded-lg border-border bg-popover p-0 shadow-md"
548
+ className="w-auto rounded-md border-border bg-popover p-0 shadow-sm"
549
549
  onOpenAutoFocus={(e) => e.preventDefault()}
550
550
  onInteractOutside={(e) => {
551
551
  const target = e.detail.originalEvent.target as Node | null;
@@ -890,7 +890,7 @@ const TimeRangePicker = React.forwardRef<
890
890
  <PopoverContent
891
891
  align="start"
892
892
  sideOffset={6}
893
- className="w-auto rounded-lg border-border bg-popover p-0 shadow-md"
893
+ className="w-auto rounded-md border-border bg-popover p-0 shadow-sm"
894
894
  onOpenAutoFocus={(e) => e.preventDefault()}
895
895
  onInteractOutside={(e) => {
896
896
  const target = e.detail.originalEvent.target as Node | null;
@@ -11,8 +11,12 @@ import { cn } from '@/utils/cn';
11
11
  * 确保 4 个 picker 的 trigger 视觉完全一致。
12
12
  */
13
13
  export const triggerWrapperClass = cn(
14
- 'flex items-center gap-2 rounded-lg border border-input bg-background px-3 shadow-sm transition-colors',
15
- 'hover:border-foreground/30 focus-within:border-foreground/60 focus-within:outline-none',
14
+ 'flex items-center gap-2 rounded-md border border-input bg-card px-3 shadow-sm transition-colors',
15
+ // hover / focus 视觉:border 变 ring 色;focus 时再加 2px 10% 半透明 halo
16
+ // - opentrek `--color-ring: #2878fa` → hover 蓝边,focus 蓝边 + 2px 10% 蓝 halo(对齐 op 设计规范)
17
+ // - uni-manager `--color-ring: transparent` → 全透明,由 uni-manager scoped CSS
18
+ // `[class*="border-input"]:focus-within` 接管 border 加深(灰色,antd 风格)
19
+ 'hover:border-ring focus-within:outline-none focus-within:border-ring focus-within:ring-2 focus-within:ring-ring/10',
16
20
  '[&:has(input:disabled)]:cursor-not-allowed [&:has(input:disabled)]:opacity-50',
17
21
  );
18
22
 
@@ -20,12 +24,12 @@ export const triggerWrapperClass = cn(
20
24
  export const inputElementClass =
21
25
  'min-w-0 grow bg-transparent text-foreground outline-none tabular-nums placeholder:text-muted-foreground disabled:cursor-not-allowed';
22
26
 
23
- /** Trigger 高度尺寸(对齐 Input / Button 三档)。 */
27
+ /** Trigger 高度 + 字号(对齐 Input / Button 三档,ADR 0027 form-element-medium = h-8 + text-xs 12px)。 */
24
28
  export const triggerSizeClass = {
25
29
  sm: 'h-7 text-xs',
26
- default: 'h-8 text-sm',
27
- md: 'h-8 text-sm',
28
- lg: 'h-9 text-base',
30
+ default: 'h-8 text-xs',
31
+ md: 'h-8 text-xs',
32
+ lg: 'h-9 text-sm',
29
33
  } as const;
30
34
 
31
35
  export type TriggerSize = keyof typeof triggerSizeClass;
@@ -1,24 +0,0 @@
1
- import { Button } from '@/components/ui/button';
2
-
3
- /**
4
- * asChild:用 Radix Slot 渲染为子元素(如 `<a>` / 路由 Link)。
5
- * 此时 `loading` / `icon` prop 会被忽略,因为底层不再是 `<button>`。
6
- */
7
- export default function Demo() {
8
- return (
9
- <div className="flex flex-wrap items-center gap-3">
10
- <Button asChild variant="link">
11
- <a href="/ui/components/button">查看文档</a>
12
- </Button>
13
- <Button asChild>
14
- <a
15
- href="https://github.com/teamix-evo/teamix-evo"
16
- target="_blank"
17
- rel="noreferrer"
18
- >
19
- GitHub 仓库
20
- </a>
21
- </Button>
22
- </div>
23
- );
24
- }
@@ -1,8 +0,0 @@
1
- import { Button } from '@/components/ui/button';
2
-
3
- /**
4
- * 最简用法:默认 variant + 默认 size。
5
- */
6
- export default function Demo() {
7
- return <Button>提交</Button>;
8
- }
@@ -1,16 +0,0 @@
1
- import { Button } from '@/components/ui/button';
2
-
3
- /**
4
- * 块级按钮:撑满父容器宽度。
5
- * 不要再额外加 `className="w-full"` 重复声明。
6
- */
7
- export default function Demo() {
8
- return (
9
- <div className="flex w-80 flex-col gap-3">
10
- <Button block>登录</Button>
11
- <Button block variant="outline">
12
- 注册
13
- </Button>
14
- </div>
15
- );
16
- }