@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
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.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/tokens": "^0.5.0",
77
+ "@teamix-evo/registry": "0.6.0",
78
78
  "@teamix-evo/eslint-config": "0.2.1",
79
- "@teamix-evo/registry": "0.3.0"
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 hover:shadow-md',
33
+ 'bg-primary text-primary-foreground transition-shadow duration-150 hover:bg-primary/90',
32
34
  secondary:
33
- 'bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80',
35
+ 'bg-secondary text-secondary-foreground transition-shadow duration-150 hover:bg-secondary/80',
34
36
  destructive:
35
- 'bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90',
37
+ 'bg-destructive text-destructive-foreground transition-shadow duration-150 hover:bg-destructive/90',
36
38
  outline:
37
- 'border border-input bg-background shadow-sm hover:bg-muted hover:text-foreground hover:shadow-md',
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-background shadow-sm hover:bg-muted hover:text-foreground hover:shadow-md',
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 — 紧凑场景(表格行内、密集工具条),对齐 cd small
52
- sm: 'h-6 px-3 text-xs',
53
- // 32px — 默认 / 表单基线;`md` 与 `default` 等价(向后兼容旧 API)
54
- // 字号对齐 cd hybridcloud --form-element-medium-font-size: 12px (ADR 0027)
55
- md: 'h-8 px-3 text-xs',
56
- default: 'h-8 px-3 text-xs',
57
- // 36px — 强调主操作 / 落地页
58
- lg: 'h-9 px-3 text-sm',
59
- // 32px 正方形 — 纯图标按钮(配合 shape="circle" / "square")
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: 12pxADR 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
- headerFooter: 'p-6',
24
- content: 'p-6 pt-0',
25
- footer: 'p-6 pt-0',
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-6',
30
- content: 'p-6 pt-0',
31
- footer: 'p-6 pt-0',
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 { Button } from '@/components/button/button';
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
- <Button
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
- </Button>
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: 2px (corner-1) → rounded-md(uni-manager 2px / opentrek 6px)
67
- 'peer size-4 shrink-0 cursor-pointer rounded-md border border-input bg-background shadow-sm transition-colors',
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-lg p-0"
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-lg p-0"
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 p-6 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',
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-background px-3 py-1 shadow-sm transition-colors 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',
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-background px-3 shadow-sm transition-colors focus-within:ring-1 focus-within:ring-ring aria-invalid:border-destructive aria-invalid:focus-within:ring-destructive',
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-background shadow-sm',
37
- 'focus-within:ring-1 focus-within:ring-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-background shadow-sm',
196
- 'focus-within:ring-1 focus-within:ring-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-background py-1 pl-3 pr-8 text-xs shadow-sm',
37
- 'focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-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 强制 `<header>` + 边框 + 内边距,不适合内嵌
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 flex-col gap-4 overflow-hidden bg-background px-6 py-4',
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('flex shrink-0 items-center gap-2', className)}
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 bg-background px-6 py-4', className)}
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 内拼装