@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.
- package/manifest.json +20 -0
- package/package.json +3 -3
- package/src/components/button/button.tsx +18 -14
- package/src/components/card/card.tsx +7 -6
- package/src/components/cascader/cascader.tsx +12 -5
- package/src/components/checkbox/checkbox.tsx +4 -2
- package/src/components/date-picker/date-picker.tsx +2 -2
- package/src/components/dialog/dialog.tsx +1 -1
- package/src/components/filter-bar/filter-bar.stories.tsx +4 -1
- package/src/components/form/form.stories.tsx +7 -4
- package/src/components/input/input.tsx +2 -2
- package/src/components/input-group/input-group.tsx +2 -2
- package/src/components/input-number/input-number.tsx +2 -2
- package/src/components/native-select/native-select.tsx +2 -2
- package/src/components/page-header/page-header.meta.md +3 -1
- package/src/components/page-header/page-header.stories.tsx +8 -1
- package/src/components/page-header/page-header.tsx +7 -4
- package/src/components/page-shell/page-shell.meta.md +116 -0
- package/src/components/page-shell/page-shell.stories.tsx +149 -0
- package/src/components/page-shell/page-shell.tsx +115 -0
- package/src/components/pagination/pagination.tsx +24 -34
- package/src/components/segmented/segmented.tsx +1 -1
- package/src/components/select/select.tsx +2 -2
- package/src/components/sidebar/sidebar.meta.md +1 -0
- package/src/components/sidebar/sidebar.tsx +46 -17
- package/src/components/slider/slider.tsx +1 -1
- package/src/components/table/table.tsx +4 -2
- package/src/components/textarea/textarea.tsx +1 -1
- package/src/components/time-picker/time-picker.tsx +2 -2
- package/src/utils/trigger-input.ts +10 -6
- package/src/components/button/demo/as-child.tsx +0 -24
- package/src/components/button/demo/basic.tsx +0 -8
- package/src/components/button/demo/block.tsx +0 -16
- package/src/components/button/demo/loading.tsx +0 -19
- package/src/components/button/demo/shapes.tsx +0 -18
- package/src/components/button/demo/sizes.tsx +0 -19
- package/src/components/button/demo/variants.tsx +0 -19
- package/src/components/button/demo/with-icon.tsx +0 -20
- package/src/components/input/demo/basic.tsx +0 -12
- package/src/components/input/demo/clearable.tsx +0 -21
- package/src/components/input/demo/show-count.tsx +0 -18
- 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
|
|
27
|
-
|
|
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 = (
|
|
300
|
-
const
|
|
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
|
-
<
|
|
512
|
+
<Select
|
|
511
513
|
aria-label="每页显示"
|
|
512
|
-
|
|
514
|
+
size={selectSize}
|
|
515
|
+
value={String(finalPageSize)}
|
|
513
516
|
onChange={handlePageSizeChange}
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
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
|
-
<
|
|
532
|
-
|
|
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=
|
|
538
|
-
|
|
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>
|
|
@@ -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-
|
|
358
|
-
'focus:outline-none focus: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 } =
|
|
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
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
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
|
-
|
|
272
|
-
|
|
273
|
-
|
|
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-
|
|
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-
|
|
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
|
-
|
|
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
|
-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
15
|
-
|
|
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
|
|
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-
|
|
27
|
-
md: 'h-8 text-
|
|
28
|
-
lg: 'h-9 text-
|
|
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,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
|
-
}
|