@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
package/manifest.json
CHANGED
|
@@ -804,6 +804,23 @@
|
|
|
804
804
|
"updateStrategy": "frozen",
|
|
805
805
|
"category": "layout"
|
|
806
806
|
},
|
|
807
|
+
{
|
|
808
|
+
"id": "page-shell",
|
|
809
|
+
"name": "PageShell",
|
|
810
|
+
"type": "component",
|
|
811
|
+
"description": "页面三明治壳 — header + sidebar + children 三 slot 任意组合,覆盖全屏/仅顶部/仅左导/顶部+左导四种页面形态。background prop 走 shadcn 语义槽枚举(background/muted/card/sidebar/accent),亮暗模式由 token 文件自动管。内部为传入的 ui Sidebar 注入 scoped CSS 覆盖默认 fixed 定位,让 sidebar 跟 header 共存而不被撑到 viewport 顶端。",
|
|
812
|
+
"files": [
|
|
813
|
+
{
|
|
814
|
+
"source": "src/components/page-shell/page-shell.tsx",
|
|
815
|
+
"targetAlias": "components",
|
|
816
|
+
"targetName": "page-shell.tsx"
|
|
817
|
+
}
|
|
818
|
+
],
|
|
819
|
+
"meta": "src/components/page-shell/page-shell.meta.md",
|
|
820
|
+
"registryDependencies": ["cn", "sidebar"],
|
|
821
|
+
"updateStrategy": "frozen",
|
|
822
|
+
"category": "layout"
|
|
823
|
+
},
|
|
807
824
|
{
|
|
808
825
|
"id": "card",
|
|
809
826
|
"name": "Card",
|
|
@@ -1577,6 +1594,9 @@
|
|
|
1577
1594
|
"class-variance-authority": "^0.7.0"
|
|
1578
1595
|
},
|
|
1579
1596
|
"updateStrategy": "frozen",
|
|
1597
|
+
"status": "deprecated",
|
|
1598
|
+
"deprecatedReason": "单选场景由 RadioGroup variant=\"button\" 替代;多选切换场景由 Checkbox variant=\"button\" 替代",
|
|
1599
|
+
"replacedBy": "radio-group",
|
|
1580
1600
|
"category": "deprecated"
|
|
1581
1601
|
},
|
|
1582
1602
|
{
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@teamix-evo/ui",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "Source-injected UI components for Teamix Evo (shadcn-based, antd capabilities)",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"files": [
|
|
@@ -74,9 +74,9 @@
|
|
|
74
74
|
"vite": "^5.4.0",
|
|
75
75
|
"vite-tsconfig-paths": "^6.1.1",
|
|
76
76
|
"zod": "^3",
|
|
77
|
-
"@teamix-evo/
|
|
77
|
+
"@teamix-evo/registry": "0.6.0",
|
|
78
78
|
"@teamix-evo/eslint-config": "0.2.1",
|
|
79
|
-
"@teamix-evo/
|
|
79
|
+
"@teamix-evo/tokens": "^0.6.0"
|
|
80
80
|
},
|
|
81
81
|
"publishConfig": {
|
|
82
82
|
"access": "public",
|
|
@@ -27,16 +27,18 @@ const buttonVariants = cva(
|
|
|
27
27
|
{
|
|
28
28
|
variants: {
|
|
29
29
|
variant: {
|
|
30
|
+
// shadow 由 variant scoped CSS 提供(opentrek 默认无 / uni-manager 有 shadow + hover 浮起,
|
|
31
|
+
// 见 packages/tokens/variants/uni-manager/theme.css 末尾 [data-slot='button'] 规则)
|
|
30
32
|
default:
|
|
31
|
-
'bg-primary text-primary-foreground shadow hover:bg-primary/90
|
|
33
|
+
'bg-primary text-primary-foreground transition-shadow duration-150 hover:bg-primary/90',
|
|
32
34
|
secondary:
|
|
33
|
-
'bg-secondary text-secondary-foreground shadow-
|
|
35
|
+
'bg-secondary text-secondary-foreground transition-shadow duration-150 hover:bg-secondary/80',
|
|
34
36
|
destructive:
|
|
35
|
-
'bg-destructive text-destructive-foreground shadow-
|
|
37
|
+
'bg-destructive text-destructive-foreground transition-shadow duration-150 hover:bg-destructive/90',
|
|
36
38
|
outline:
|
|
37
|
-
'border border-input bg-
|
|
39
|
+
'border border-input bg-card transition-shadow duration-150 hover:bg-muted hover:text-foreground',
|
|
38
40
|
dashed:
|
|
39
|
-
'border border-dashed border-input bg-
|
|
41
|
+
'border border-dashed border-input bg-card transition-shadow duration-150 hover:bg-muted hover:text-foreground',
|
|
40
42
|
ghost: 'hover:bg-accent hover:text-accent-foreground',
|
|
41
43
|
link: 'text-primary underline-offset-4',
|
|
42
44
|
},
|
|
@@ -48,15 +50,15 @@ const buttonVariants = cva(
|
|
|
48
50
|
destructive: '',
|
|
49
51
|
},
|
|
50
52
|
size: {
|
|
51
|
-
// 24px —
|
|
52
|
-
sm: 'h-6 px-
|
|
53
|
-
// 32px — 默认 /
|
|
54
|
-
// 字号对齐 cd hybridcloud --form-element-medium-font-size: 12px
|
|
55
|
-
md: 'h-8 px-
|
|
56
|
-
default: 'h-8 px-
|
|
57
|
-
// 36px — 强调主操作 /
|
|
58
|
-
lg: 'h-9 px-
|
|
59
|
-
// 32px 正方形 —
|
|
53
|
+
// 24px — 紧凑场景(表格行内、密集工具条),对齐 cd small
|
|
54
|
+
sm: 'h-6 px-[var(--btn-padding-x-sm)] text-xs',
|
|
55
|
+
// 32px — 默认 / 表单基线;`md` 与 `default` 等价(向后兼容旧 API)
|
|
56
|
+
// 字号对齐 cd hybridcloud --form-element-medium-font-size: 12px(ADR 0027)
|
|
57
|
+
md: 'h-8 px-[var(--btn-padding-x-sm)] text-xs',
|
|
58
|
+
default: 'h-8 px-[var(--btn-padding-x-sm)] text-xs',
|
|
59
|
+
// 36px — 强调主操作 / 落地页;水平 padding 走 OpenTrek v4.1 标准 16px
|
|
60
|
+
lg: 'h-9 px-[var(--btn-padding-x)] text-sm',
|
|
61
|
+
// 32px 正方形 — 纯图标按钮(配合 shape="circle" / "square")
|
|
60
62
|
icon: 'h-8 w-8',
|
|
61
63
|
},
|
|
62
64
|
shape: {
|
|
@@ -315,6 +317,8 @@ const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
|
|
315
317
|
const buttonEl = (
|
|
316
318
|
<button
|
|
317
319
|
ref={ref}
|
|
320
|
+
data-slot="button"
|
|
321
|
+
data-variant={variant}
|
|
318
322
|
className={composedClassName}
|
|
319
323
|
disabled={disabled || loading}
|
|
320
324
|
aria-busy={loading || undefined}
|
|
@@ -20,15 +20,16 @@ const SIZE_TOKENS = {
|
|
|
20
20
|
title: 'text-sm',
|
|
21
21
|
},
|
|
22
22
|
md: {
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
23
|
+
// p-[var(--card-padding-x)] — op: 20px / um: 20px(OpenTrek v4.1 ContentCard准则)
|
|
24
|
+
headerFooter: 'p-[var(--card-padding-x)]',
|
|
25
|
+
content: 'p-[var(--card-padding-x)] pt-0',
|
|
26
|
+
footer: 'p-[var(--card-padding-x)] pt-0',
|
|
26
27
|
title: 'text-base',
|
|
27
28
|
},
|
|
28
29
|
default: {
|
|
29
|
-
headerFooter: 'p-
|
|
30
|
-
content: 'p-
|
|
31
|
-
footer: 'p-
|
|
30
|
+
headerFooter: 'p-[var(--card-padding-x)]',
|
|
31
|
+
content: 'p-[var(--card-padding-x)] pt-0',
|
|
32
|
+
footer: 'p-[var(--card-padding-x)] pt-0',
|
|
32
33
|
title: 'text-base',
|
|
33
34
|
},
|
|
34
35
|
lg: {
|
|
@@ -9,7 +9,11 @@ import {
|
|
|
9
9
|
} from 'lucide-react';
|
|
10
10
|
|
|
11
11
|
import { cn } from '@/utils/cn';
|
|
12
|
-
import {
|
|
12
|
+
import {
|
|
13
|
+
triggerWrapperClass,
|
|
14
|
+
triggerSizeClass,
|
|
15
|
+
type TriggerSize,
|
|
16
|
+
} from '@/utils/trigger-input';
|
|
13
17
|
import {
|
|
14
18
|
Popover,
|
|
15
19
|
PopoverContent,
|
|
@@ -386,23 +390,26 @@ const Cascader = React.forwardRef<HTMLButtonElement, CascaderProps>(
|
|
|
386
390
|
);
|
|
387
391
|
|
|
388
392
|
// ─── Render ──────────────────────────────────────────
|
|
393
|
+
// trigger 用 form-control 共享 `triggerWrapperClass`,跟 Select / DatePicker /
|
|
394
|
+
// TimePicker 视觉完全一致(hover/focus border 走 token,uni-manager scoped CSS 兜底)。
|
|
389
395
|
return (
|
|
390
396
|
<Popover open={open} onOpenChange={setOpen}>
|
|
391
397
|
<PopoverTrigger asChild>
|
|
392
|
-
<
|
|
398
|
+
<button
|
|
393
399
|
ref={ref}
|
|
394
400
|
type="button"
|
|
395
|
-
variant="outline"
|
|
396
|
-
size={size}
|
|
397
401
|
disabled={disabled}
|
|
398
402
|
className={cn(
|
|
403
|
+
triggerWrapperClass,
|
|
404
|
+
triggerSizeClass[size as TriggerSize],
|
|
399
405
|
'min-w-panel-sm justify-between font-normal',
|
|
406
|
+
'disabled:cursor-not-allowed disabled:opacity-50',
|
|
400
407
|
className,
|
|
401
408
|
)}
|
|
402
409
|
>
|
|
403
410
|
{triggerContent}
|
|
404
411
|
<ChevronDown className="ml-2 size-4 shrink-0 opacity-50" />
|
|
405
|
-
</
|
|
412
|
+
</button>
|
|
406
413
|
</PopoverTrigger>
|
|
407
414
|
<PopoverContent className="w-auto p-0" align="start">
|
|
408
415
|
{/* Search input */}
|
|
@@ -63,8 +63,10 @@ const Checkbox = React.forwardRef<
|
|
|
63
63
|
// checked : bg #0064C8 (primary) / tick white → bg-primary + text-primary-foreground
|
|
64
64
|
// ck-hovered : bg #0057AD (deeper) → data-[state=checked]:hover:bg-primary/90
|
|
65
65
|
// indeterm. : 同 checked,只是 indicator 换成 minus
|
|
66
|
-
// border-radius:
|
|
67
|
-
|
|
66
|
+
// border-radius: 用 rounded-sm 而非 rounded-md — opentrek `--radius-md = 8px`
|
|
67
|
+
// 在 size-4 (16px) 上 = 完美圆形,跟 shadcn 方形复选框相左。`--radius-sm`
|
|
68
|
+
// uni-manager 0px(锐利)/ opentrek 4px(对齐 shadcn Checkbox 默认)。
|
|
69
|
+
'peer size-4 shrink-0 cursor-pointer rounded-sm border border-input bg-card shadow-sm transition-colors',
|
|
68
70
|
// hover 反馈用 `enabled:` 前缀 — disabled 态不触发 hover 样式(border/bg 变化)
|
|
69
71
|
'enabled:hover:border-primary data-[state=unchecked]:enabled:hover:bg-primary/5',
|
|
70
72
|
'focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring',
|
|
@@ -732,7 +732,7 @@ const DatePicker = React.forwardRef<HTMLInputElement, DatePickerProps>(
|
|
|
732
732
|
</div>
|
|
733
733
|
</PopoverAnchor>
|
|
734
734
|
<PopoverContent
|
|
735
|
-
className="w-auto rounded-
|
|
735
|
+
className="w-auto rounded-md p-0 shadow-sm"
|
|
736
736
|
onOpenAutoFocus={(e) => e.preventDefault()}
|
|
737
737
|
onInteractOutside={(e) => {
|
|
738
738
|
// 点击 / focus 落在 anchor(trigger wrapper)内时,popover 不关闭。
|
|
@@ -1450,7 +1450,7 @@ const DateRangePicker = React.forwardRef<
|
|
|
1450
1450
|
</div>
|
|
1451
1451
|
</PopoverAnchor>
|
|
1452
1452
|
<PopoverContent
|
|
1453
|
-
className="w-auto rounded-
|
|
1453
|
+
className="w-auto rounded-md p-0 shadow-sm"
|
|
1454
1454
|
onOpenAutoFocus={(e) => e.preventDefault()}
|
|
1455
1455
|
onInteractOutside={(e) => {
|
|
1456
1456
|
const target = e.detail.originalEvent.target as Node | null;
|
|
@@ -54,7 +54,7 @@ DialogOverlay.displayName = DialogPrimitive.Overlay.displayName;
|
|
|
54
54
|
* - size 绑定 `--layout-dialog-{sm|md|lg|xl}` tokens
|
|
55
55
|
*/
|
|
56
56
|
const dialogContentVariants = cva(
|
|
57
|
-
'fixed left-1/2 top-1/2 z-50 grid w-full translate-x-[-50%] translate-y-[-50%] gap-4 border-0 bg-background
|
|
57
|
+
'fixed left-1/2 top-1/2 z-50 grid w-full translate-x-[-50%] translate-y-[-50%] gap-4 border-0 bg-background px-[var(--dialog-body-padding-x)] py-[var(--dialog-body-padding-y)] shadow-lg duration-200 rounded-[var(--radius-dialog)] data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95',
|
|
58
58
|
{
|
|
59
59
|
variants: {
|
|
60
60
|
size: {
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import * as React from 'react';
|
|
2
2
|
import type { Meta, StoryObj } from '@storybook/react-vite';
|
|
3
3
|
import { useForm, FormProvider } from 'react-hook-form';
|
|
4
|
+
// `as never` cast on zodResolver(...) below: zod 3.25.x ships zod v4
|
|
5
|
+
// internals while @hookform/resolvers@3.x still types Resolver against
|
|
6
|
+
// zod v3 Schema. Remove when upgrading to @hookform/resolvers v4.
|
|
4
7
|
import { zodResolver } from '@hookform/resolvers/zod';
|
|
5
8
|
import { z } from 'zod';
|
|
6
9
|
import {
|
|
@@ -616,7 +619,7 @@ export const WithValidation: Story = {
|
|
|
616
619
|
name: '带 Zod 校验',
|
|
617
620
|
render: () => {
|
|
618
621
|
const form = useForm<z.infer<typeof filterSchema>>({
|
|
619
|
-
resolver: zodResolver(filterSchema),
|
|
622
|
+
resolver: zodResolver(filterSchema as never),
|
|
620
623
|
defaultValues: {
|
|
621
624
|
keyword: '',
|
|
622
625
|
org: '',
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import * as React from 'react';
|
|
2
2
|
import type { Meta, StoryObj } from '@storybook/react-vite';
|
|
3
3
|
import { useForm } from 'react-hook-form';
|
|
4
|
+
// `as never` casts on zodResolver(...) below: zod 3.25.x ships zod v4
|
|
5
|
+
// internals while @hookform/resolvers@3.x still types Resolver against
|
|
6
|
+
// zod v3 Schema. Remove when upgrading to @hookform/resolvers v4.
|
|
4
7
|
import { zodResolver } from '@hookform/resolvers/zod';
|
|
5
8
|
import { z } from 'zod';
|
|
6
9
|
import {
|
|
@@ -44,7 +47,7 @@ export const ZodValidated: Story = {
|
|
|
44
47
|
parameters: { layout: 'centered' },
|
|
45
48
|
render: () => {
|
|
46
49
|
const form = useForm<z.infer<typeof schema>>({
|
|
47
|
-
resolver: zodResolver(schema),
|
|
50
|
+
resolver: zodResolver(schema as never),
|
|
48
51
|
defaultValues: { username: '', email: '', bio: '' },
|
|
49
52
|
});
|
|
50
53
|
const [submitted, setSubmitted] = React.useState<unknown>(null);
|
|
@@ -137,7 +140,7 @@ export const WithSections: Story = {
|
|
|
137
140
|
parameters: { layout: 'centered' },
|
|
138
141
|
render: () => {
|
|
139
142
|
const form = useForm<z.infer<typeof sectionsSchema>>({
|
|
140
|
-
resolver: zodResolver(sectionsSchema),
|
|
143
|
+
resolver: zodResolver(sectionsSchema as never),
|
|
141
144
|
defaultValues: { name: '', email: '', phone: '', company: '', role: '' },
|
|
142
145
|
});
|
|
143
146
|
const [submitted, setSubmitted] = React.useState<unknown>(null);
|
|
@@ -257,7 +260,7 @@ export const MultiColumn: Story = {
|
|
|
257
260
|
parameters: { layout: 'centered' },
|
|
258
261
|
render: () => {
|
|
259
262
|
const form = useForm<z.infer<typeof multiColumnSchema>>({
|
|
260
|
-
resolver: zodResolver(multiColumnSchema),
|
|
263
|
+
resolver: zodResolver(multiColumnSchema as never),
|
|
261
264
|
defaultValues: {
|
|
262
265
|
firstName: '',
|
|
263
266
|
lastName: '',
|
|
@@ -377,7 +380,7 @@ export const StickyActions: Story = {
|
|
|
377
380
|
parameters: { layout: 'centered' },
|
|
378
381
|
render: () => {
|
|
379
382
|
const form = useForm<z.infer<typeof stickySchema>>({
|
|
380
|
-
resolver: zodResolver(stickySchema),
|
|
383
|
+
resolver: zodResolver(stickySchema as never),
|
|
381
384
|
defaultValues: {
|
|
382
385
|
title: '',
|
|
383
386
|
description: '',
|
|
@@ -28,7 +28,7 @@ const SIZE_TO_HEIGHT = {
|
|
|
28
28
|
* 字符计数 `showCount` + 一键清空 `clearable` 仍保留为 Input 的内建逻辑能力。
|
|
29
29
|
*/
|
|
30
30
|
const inputVariants = cva(
|
|
31
|
-
'flex w-full rounded-md border border-input bg-
|
|
31
|
+
'flex w-full rounded-md border border-input bg-card px-[var(--input-padding-x)] py-[var(--input-padding-y)] shadow-sm transition-colors 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',
|
|
32
32
|
{
|
|
33
33
|
variants: { size: SIZE_TO_HEIGHT },
|
|
34
34
|
defaultVariants: { size: 'md' },
|
|
@@ -39,7 +39,7 @@ const inputVariants = cva(
|
|
|
39
39
|
* Input 包壳容器(`clearable` 启用时承载 border + ring,把内部 input 的边框转移到外层)。
|
|
40
40
|
*/
|
|
41
41
|
const inputWrapperVariants = cva(
|
|
42
|
-
'flex w-full items-center gap-2 rounded-md border border-input bg-
|
|
42
|
+
'flex w-full items-center gap-2 rounded-md border border-input bg-card px-[var(--input-padding-x)] shadow-sm transition-colors hover:border-ring focus-within:border-ring focus-within:ring-2 focus-within:ring-ring/10 aria-invalid:border-destructive aria-invalid:focus-within:ring-destructive/10',
|
|
43
43
|
{
|
|
44
44
|
variants: { size: SIZE_TO_HEIGHT },
|
|
45
45
|
defaultVariants: { size: 'md' },
|
|
@@ -33,8 +33,8 @@ const InputGroup = React.forwardRef<HTMLDivElement, InputGroupProps>(
|
|
|
33
33
|
ref={ref}
|
|
34
34
|
data-disabled={disabled ? '' : undefined}
|
|
35
35
|
className={cn(
|
|
36
|
-
'flex w-full items-stretch overflow-hidden rounded-md border border-input bg-
|
|
37
|
-
'focus-within:ring-
|
|
36
|
+
'flex w-full items-stretch overflow-hidden rounded-md border border-input bg-card shadow-sm',
|
|
37
|
+
'hover:border-ring focus-within:border-ring focus-within:ring-2 focus-within:ring-ring/10',
|
|
38
38
|
'data-[disabled]:cursor-not-allowed data-[disabled]:opacity-50',
|
|
39
39
|
className,
|
|
40
40
|
)}
|
|
@@ -192,8 +192,8 @@ const InputNumber = React.forwardRef<HTMLInputElement, InputNumberProps>(
|
|
|
192
192
|
return (
|
|
193
193
|
<div
|
|
194
194
|
className={cn(
|
|
195
|
-
'inline-flex items-stretch overflow-hidden rounded-md border border-input bg-
|
|
196
|
-
'focus-within:ring-
|
|
195
|
+
'inline-flex items-stretch overflow-hidden rounded-md border border-input bg-card shadow-sm',
|
|
196
|
+
'hover:border-ring focus-within:border-ring focus-within:ring-2 focus-within:ring-ring/10',
|
|
197
197
|
disabled && 'cursor-not-allowed opacity-50',
|
|
198
198
|
className,
|
|
199
199
|
)}
|
|
@@ -33,8 +33,8 @@ const NativeSelect = React.forwardRef<HTMLSelectElement, NativeSelectProps>(
|
|
|
33
33
|
ref={ref}
|
|
34
34
|
disabled={disabled}
|
|
35
35
|
className={cn(
|
|
36
|
-
'flex h-9 w-full appearance-none rounded-md border border-input bg-
|
|
37
|
-
'focus-visible:outline-none focus-visible:ring-
|
|
36
|
+
'flex h-9 w-full appearance-none rounded-md border border-input bg-card py-1 pl-3 pr-8 text-xs shadow-sm',
|
|
37
|
+
'focus-visible:outline-none hover:border-ring focus-visible:border-ring focus-visible:ring-2 focus-visible:ring-ring/10',
|
|
38
38
|
'disabled:cursor-not-allowed disabled:opacity-50',
|
|
39
39
|
className,
|
|
40
40
|
)}
|
|
@@ -21,7 +21,8 @@ package: '@teamix-evo/ui'
|
|
|
21
21
|
|
|
22
22
|
## When NOT to use
|
|
23
23
|
|
|
24
|
-
- **页面内子区块标题** → 用 [`Title`](../typography/typography.meta.md) + 自定义容器,PageHeader
|
|
24
|
+
- **页面内子区块标题** → 用 [`Title`](../typography/typography.meta.md) + 自定义容器,PageHeader 是页面级容器(`<header>` + flex 列布局),不适合内嵌
|
|
25
|
+
|
|
25
26
|
- **Modal / Drawer 头部** → 这些组件自带 header 区域(`DialogHeader` / `SheetHeader`),不要再叠 PageHeader
|
|
26
27
|
- **需要随滚动吸顶的特殊容器** → PageHeader 自身不内置 sticky,真有诉求外层包 `<div className="sticky top-0 z-10">`
|
|
27
28
|
|
|
@@ -76,6 +77,7 @@ pnpm add lucide-react@^0.460.0
|
|
|
76
77
|
|
|
77
78
|
### 硬约束
|
|
78
79
|
|
|
80
|
+
- **Padding 由 PageContainer 层提供** — PageHeader 自身只含 `bg-background` 与 flex 列布局,**不带 padding**。实际使用必须放在 `UmPageContainer` / `OpPageContainer` 或消费方自建 wrapper 内,由外层加 `px-6 py-4`(或变体自己的尺寸)。stories 通过 decorator 包 `<div className="px-6 py-4">` 模拟容器内展示。
|
|
79
81
|
- **不接受配置对象** — 不要给 PageHeader 加 `breadcrumbs={[...]}` / `actions={[...]}` / `tags={[...]}` 一类的配置,**所有内容必须用子组件 + 原子组件组合**(P3 一图流原则:看到 jsx 即看到 DOM)
|
|
80
82
|
- **`PageHeaderSkeleton` 与 `PageHeader` 是替换关系** — 不要把 Skeleton 放进 `<PageHeader>...</PageHeader>` 里,加载态条件渲染整体替换
|
|
81
83
|
- **`onBack` 是纯回调** — 不要在 PageHeader 内部 import `react-router` / `next/navigation`,路由由调用方在 `onBack` 里实现(P5 不绑框架)
|
|
@@ -43,10 +43,17 @@ const meta: Meta<typeof PageHeader> = {
|
|
|
43
43
|
docs: {
|
|
44
44
|
description: {
|
|
45
45
|
component:
|
|
46
|
-
'PageHeader 页头容器 —— 承接 teamix-pro `ProPageHeader` 的视觉规范,转为 shadcn 复合组件范式(Nav / Heading / Description / Content / Actions / Data / DataItem / Footer / Skeleton 子组件)。**纯排版骨架,不接受配置对象** — 面包屑 / 标签 / 操作按钮 / 详情面板都由消费方用 `<Breadcrumb>` / `<Tag>` / `<Button>` / `<Descriptions>` 等原子组件自由组合;图标采用 `<Icon variant="circle|square" color="...">{children}</Icon>` 带背景容器范式。背景图通过 `backgroundImage` prop inline `style` 注入。`onBack`
|
|
46
|
+
'PageHeader 页头容器 —— 承接 teamix-pro `ProPageHeader` 的视觉规范,转为 shadcn 复合组件范式(Nav / Heading / Description / Content / Actions / Data / DataItem / Footer / Skeleton 子组件)。**纯排版骨架,不接受配置对象** — 面包屑 / 标签 / 操作按钮 / 详情面板都由消费方用 `<Breadcrumb>` / `<Tag>` / `<Button>` / `<Descriptions>` 等原子组件自由组合;图标采用 `<Icon variant="circle|square" color="...">{children}</Icon>` 带背景容器范式。背景图通过 `backgroundImage` prop inline `style` 注入。`onBack` 纯回调,不绑定路由库。\n\n**Padding**:组件自身不含外边距 — 实际使用时由 PageContainer 层(如 `UmPageContainer` / `OpPageContainer`)提供 padding。下方 Story 通过 decorator 包 `<div className="px-6 py-4">` 模拟容器内的展示效果。',
|
|
47
47
|
},
|
|
48
48
|
},
|
|
49
49
|
},
|
|
50
|
+
decorators: [
|
|
51
|
+
(Story) => (
|
|
52
|
+
<div className="bg-background px-6 py-4">
|
|
53
|
+
<Story />
|
|
54
|
+
</div>
|
|
55
|
+
),
|
|
56
|
+
],
|
|
50
57
|
};
|
|
51
58
|
|
|
52
59
|
export default meta;
|
|
@@ -29,7 +29,7 @@ const PageHeader = React.forwardRef<HTMLElement, PageHeaderProps>(
|
|
|
29
29
|
<header
|
|
30
30
|
ref={ref}
|
|
31
31
|
className={cn(
|
|
32
|
-
'relative flex
|
|
32
|
+
'relative flex min-h-[var(--page-header-height)] flex-col gap-4',
|
|
33
33
|
className,
|
|
34
34
|
)}
|
|
35
35
|
style={bgStyle}
|
|
@@ -125,7 +125,7 @@ const PageHeaderDescription = React.forwardRef<
|
|
|
125
125
|
>(({ className, ...props }, ref) => (
|
|
126
126
|
<p
|
|
127
127
|
ref={ref}
|
|
128
|
-
className={cn('text-xs text-muted-foreground', className)}
|
|
128
|
+
className={cn('my-2 text-xs text-muted-foreground', className)}
|
|
129
129
|
{...props}
|
|
130
130
|
/>
|
|
131
131
|
));
|
|
@@ -139,7 +139,10 @@ const PageHeaderActions = React.forwardRef<
|
|
|
139
139
|
>(({ className, ...props }, ref) => (
|
|
140
140
|
<div
|
|
141
141
|
ref={ref}
|
|
142
|
-
className={cn(
|
|
142
|
+
className={cn(
|
|
143
|
+
'flex shrink-0 items-center gap-[var(--button-gap)]',
|
|
144
|
+
className,
|
|
145
|
+
)}
|
|
143
146
|
{...props}
|
|
144
147
|
/>
|
|
145
148
|
));
|
|
@@ -249,7 +252,7 @@ const PageHeaderSkeleton = React.forwardRef<
|
|
|
249
252
|
) => (
|
|
250
253
|
<div
|
|
251
254
|
ref={ref}
|
|
252
|
-
className={cn('flex flex-col gap-4
|
|
255
|
+
className={cn('flex flex-col gap-4', className)}
|
|
253
256
|
aria-busy="true"
|
|
254
257
|
{...props}
|
|
255
258
|
>
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
---
|
|
2
|
+
id: page-shell
|
|
3
|
+
name: PageShell
|
|
4
|
+
displayName: 页面壳
|
|
5
|
+
type: component
|
|
6
|
+
category: layout
|
|
7
|
+
since: 0.1.0
|
|
8
|
+
package: '@teamix-evo/ui'
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
# PageShell 页面壳
|
|
12
|
+
|
|
13
|
+
页面级三明治壳 — `header` + `sidebar` + `children` 三 slot 任意组合,覆盖"全屏 / 仅顶部 / 仅左导 / 顶部+左导" 四种页面形态。`background` prop 走 shadcn 语义槽枚举(`background` / `muted` / `card` / `sidebar` / `accent`),亮暗模式由 [`@teamix-evo/tokens`](../../../../tokens/) 文件自动管。
|
|
14
|
+
|
|
15
|
+
PageShell 传 `sidebar` 时内部用 [`<SidebarProvider embedded>`](../sidebar/sidebar.meta.md) —— ui Sidebar 的嵌入模式,sidebar-container 自动走 `position: relative + h-full`(替代默认 `fixed inset-y-0 h-svh`),与 header 共存而不被撑到 viewport 顶端。消费方不再需要手写 `!relative !top-0 !h-full` hack。
|
|
16
|
+
|
|
17
|
+
## When to use
|
|
18
|
+
|
|
19
|
+
- **应用主页面骨架** — 控制台 / dashboard / 管理后台等所有需要 sidebar/topbar 的页面
|
|
20
|
+
- **无 sidebar 全屏页** — 登录页 / 错误页 / 落地页(不传 `sidebar` 即可)
|
|
21
|
+
- **opentrek 风格无吊顶 layout** — 仅传 `sidebar`,主区背景设 `muted`
|
|
22
|
+
- **uni-manager 风格吊顶+左导** — 同时传 `header={<UmTopbar />}` 与 `sidebar={<Sidebar />}`
|
|
23
|
+
|
|
24
|
+
## When NOT to use
|
|
25
|
+
|
|
26
|
+
- **页面内子布局**(分屏 / 多面板)→ 直接用 flex / grid,PageShell 是页面级唯一壳
|
|
27
|
+
- **Modal / Drawer 内的布局** → 这些组件自带容器,不要再叠 PageShell
|
|
28
|
+
- **路由级布局共享** — 这是 React Router / Next.js layout 层职责,PageShell 是渲染层
|
|
29
|
+
|
|
30
|
+
## Props
|
|
31
|
+
|
|
32
|
+
<!-- auto:props:begin -->
|
|
33
|
+
| 名称 | 类型 | 默认值 | 必填 | 说明 |
|
|
34
|
+
| --- | --- | --- | --- | --- |
|
|
35
|
+
| `header` | `ReactNode` | – | – | 顶部条 slot — 通常是 UmTopbar / 自建 navbar。不传则不渲染顶部。 |
|
|
36
|
+
| `sidebar` | `ReactNode` | – | – | 左侧 slot — 通常是 ui `<Sidebar>` 或基于它组合的整装件(如 OpSidebar)。不传则不渲染 sidebar 与 SidebarProvider。 |
|
|
37
|
+
| `background` | `'background' \| 'muted' \| 'card' \| 'sidebar' \| 'accent'` | `'background'` | – | 主区背景 — 走 shadcn 语义槽枚举,亮暗模式由 token 文件自动切换。 |
|
|
38
|
+
| `sidebarWidth` | `string` | `'14rem'` | – | sidebar 宽度 — 透传给 SidebarProvider 的 `--sidebar-width` CSS 变量。仅在传 `sidebar` 时生效。 |
|
|
39
|
+
<!-- auto:props:end -->
|
|
40
|
+
|
|
41
|
+
## 依赖
|
|
42
|
+
|
|
43
|
+
<!-- auto:deps:begin -->
|
|
44
|
+
### 同库依赖
|
|
45
|
+
|
|
46
|
+
| Entry | 类型 | 描述 |
|
|
47
|
+
| --- | --- | --- |
|
|
48
|
+
| `cn` | util | Tailwind className 合并工具 |
|
|
49
|
+
| `sidebar` | component | shadcn Sidebar 25 primitives |
|
|
50
|
+
<!-- auto:deps:end -->
|
|
51
|
+
|
|
52
|
+
## AI 生成纪律
|
|
53
|
+
|
|
54
|
+
### 硬约束
|
|
55
|
+
|
|
56
|
+
- **不传 `sidebar` 时不要在 children 内调用 `useSidebar()`** — PageShell 不传 sidebar 即不渲染 SidebarProvider,无 sidebar context
|
|
57
|
+
- **`background` 必须用枚举值,不要传 raw className** — 想要其他背景请扩 `PageShellBackground` 枚举或在 children 内自加 wrapper
|
|
58
|
+
- **不要在 PageShell 外再包 `SidebarProvider`** — PageShell 传 sidebar 时自带,重复包会破坏 cookie 持久化与 Cmd+B 快捷键
|
|
59
|
+
- **sidebar slot 传 ui `<Sidebar>` 时不需要再加 `className="!relative ..."`** — PageShell 已注入 scoped CSS 覆盖
|
|
60
|
+
|
|
61
|
+
### 推荐范式
|
|
62
|
+
|
|
63
|
+
- 全屏:`<PageShell><LoginForm /></PageShell>`
|
|
64
|
+
- opentrek 无吊顶:`<PageShell background="muted" sidebar={<OpSidebar />}><OpPageContainer>...</OpPageContainer></PageShell>`
|
|
65
|
+
- uni-manager 吊顶+左导:`<PageShell header={<UmTopbar ... />} sidebar={<Sidebar>...</Sidebar>}><UmPageContainer>...</UmPageContainer></PageShell>`
|
|
66
|
+
|
|
67
|
+
### 反例
|
|
68
|
+
|
|
69
|
+
- ❌ 在 PageShell 外手动包 `<SidebarProvider>` — 重复 provider
|
|
70
|
+
- ❌ `background="#f7f7f7"` 传颜色值 — 必须用枚举,违反 token discipline
|
|
71
|
+
- ❌ 不传 `sidebar` 但 children 内用 `useSidebar()` — 报错"must be used within a SidebarProvider"
|
|
72
|
+
|
|
73
|
+
## Examples
|
|
74
|
+
|
|
75
|
+
```tsx
|
|
76
|
+
import { PageShell } from '@/components/page-shell/page-shell';
|
|
77
|
+
import { Sidebar, SidebarContent, SidebarMenu, ... } from '@/components/sidebar/sidebar';
|
|
78
|
+
|
|
79
|
+
// 全屏单区 — 登录页
|
|
80
|
+
<PageShell>
|
|
81
|
+
<LoginForm />
|
|
82
|
+
</PageShell>
|
|
83
|
+
|
|
84
|
+
// 仅顶部 — 简单页面
|
|
85
|
+
<PageShell header={<Navbar />}>
|
|
86
|
+
<main>...</main>
|
|
87
|
+
</PageShell>
|
|
88
|
+
|
|
89
|
+
// 仅 sidebar — opentrek 风格(灰底)
|
|
90
|
+
<PageShell background="muted" sidebar={<OpSidebar />}>
|
|
91
|
+
<OpPageContainer header={...}>...</OpPageContainer>
|
|
92
|
+
</PageShell>
|
|
93
|
+
|
|
94
|
+
// 完整三明治 — uni-manager 风格(白底)
|
|
95
|
+
<PageShell
|
|
96
|
+
header={<UmTopbar brand={...} menus={...} ... />}
|
|
97
|
+
sidebar={<Sidebar collapsible="icon">...</Sidebar>}
|
|
98
|
+
sidebarWidth="14rem"
|
|
99
|
+
>
|
|
100
|
+
<UmPageContainer header={<PageHeader>...</PageHeader>}>
|
|
101
|
+
<DataTable ... />
|
|
102
|
+
</UmPageContainer>
|
|
103
|
+
</PageShell>
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
## 实现细节
|
|
107
|
+
|
|
108
|
+
PageShell 传 `sidebar` 时,内部用 `<SidebarProvider embedded ...>` 启用 ui Sidebar 的嵌入模式。该模式下 [Sidebar](../sidebar/sidebar.meta.md) 的 `sidebar-container` 自动从默认 `fixed inset-y-0 h-svh` 切到 `relative h-full`,让 sidebar 跟 header 在同一布局流里垂直对齐 —— 完全声明式,无 scoped CSS、无 `!important` 覆盖。
|
|
109
|
+
|
|
110
|
+
`embedded` 是 ui SidebarProvider 的原生 prop(默认 `false` 保持 shadcn 全屏行为),由 context 透传给所有下游 `<Sidebar>` 自动响应。
|
|
111
|
+
|
|
112
|
+
## 关联
|
|
113
|
+
|
|
114
|
+
- 范本组件:[Sidebar](../sidebar/sidebar.meta.md) — PageShell 内嵌的 SidebarProvider / SidebarInset 来自这里
|
|
115
|
+
- 关联组件:[PageHeader](../page-header/page-header.meta.md) — 页面内 PageContainer 的 header slot 首选
|
|
116
|
+
- 关联 ADR:[ADR 0005](../../../../../docs/adr/0005-ui-no-variant.md) — ui 不感知 variant,具体页面壳由 biz-ui 的 OpPageContainer / UmPageContainer 在 PageShell 内拼装
|