@qijenchen/design-system 0.1.0-beta.77 → 0.1.0-beta.79

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/CLAUDE.md CHANGED
@@ -32,7 +32,7 @@
32
32
 
33
33
  ## 行數預算(Anthropic 對齊)
34
34
 
35
- CLAUDE.md target ≤ 200(Anthropic best-practice)/ transition ≤ 400 / hard cap 800。SKILL ≤ 250 / spec ≤ 300(foundational SSOT 例外 ≤ 800-1200)/ memory **per-file ≤ 100 lines** + **MEMORY.md index ≤ 20 entries**(soft 18 / hard 20,session-start hook 攔)。Hooks **26 soft / 60 hard**(SSOT = `session_start_governance_check.sh` Check 7 threshold logic,2026-05-27 升 50→55→60 per codex M31 P0 hooks + baseline + primitive-misuse 3 new hooks)。動態值見 `scripts/sync-governance-counters.mjs` 跑出為準(snapshot 2026-06-11:**31 M-rules / 88 audit dims / 52 hooks** — 數字僅供 sanity check,真值以 script 輸出為準避 drift)。
35
+ CLAUDE.md target ≤ 200(Anthropic best-practice)/ transition ≤ 400 / hard cap 800。SKILL ≤ 250 / spec ≤ 300(foundational SSOT 例外 ≤ 800-1200)/ memory **per-file ≤ 100 lines** + **MEMORY.md index ≤ 20 entries**(soft 18 / hard 20,session-start hook 攔)。Hooks **26 soft / 60 hard**(SSOT = `session_start_governance_check.sh` Check 7 threshold logic,2026-05-27 升 50→55→60 per codex M31 P0 hooks + baseline + primitive-misuse 3 new hooks)。動態值見 `scripts/sync-governance-counters.mjs` 跑出為準(snapshot 2026-07-02:**31 M-rules / 90 audit dims / 53 hooks** — 數字僅供 sanity check,真值以 script 輸出為準避 drift)。
36
36
 
37
37
  ## Anti-bloat L1-L3
38
38
 
@@ -18,7 +18,7 @@ const Command = React.forwardRef(({ className, ...props }, ref) => /* @__PURE__
18
18
  ));
19
19
  Command.displayName = Command$1.displayName;
20
20
  const CommandDialog = ({ children, ...props }) => {
21
- return /* @__PURE__ */ jsx(Dialog, { ...props, children: /* @__PURE__ */ jsx(DialogContent, { className: "overflow-hidden p-0 shadow-[var(--elevation-200)]", children: /* @__PURE__ */ jsx(Command, { className: "[&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-fg-muted [&_[cmdk-group]:not([hidden])_~[cmdk-group]]:pt-0 [&_[cmdk-group]]:px-2 [&_[cmdk-input-wrapper]_svg]:h-5 [&_[cmdk-input-wrapper]_svg]:w-5 [&_[cmdk-input]]:h-12 [&_[cmdk-item]]:px-2 [&_[cmdk-item]]:py-3 [&_[cmdk-item]_svg]:h-5 [&_[cmdk-item]_svg]:w-5", children }) }) });
21
+ return /* @__PURE__ */ jsx(Dialog, { ...props, children: /* @__PURE__ */ jsx(DialogContent, { className: "overflow-hidden p-0 shadow-[var(--elevation-200)]", children: /* @__PURE__ */ jsx(Command, { className: "[&_[cmdk-group-heading]]:px-3 [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-fg-muted [&_[cmdk-group]:not([hidden])_~[cmdk-group]]:pt-0 [&_[cmdk-group]]:px-3 [&_[cmdk-input-wrapper]_svg]:h-5 [&_[cmdk-input-wrapper]_svg]:w-5 [&_[cmdk-input]]:h-12 [&_[cmdk-item]]:px-3 [&_[cmdk-item]]:py-3 [&_[cmdk-item]_svg]:h-5 [&_[cmdk-item]_svg]:w-5", children }) }) });
22
22
  };
23
23
  const CommandInput = React.forwardRef(({ className, ...props }, ref) => /* @__PURE__ */ jsxs("div", { className: "flex items-center border-b border-divider px-3", "cmdk-input-wrapper": "", children: [
24
24
  /* @__PURE__ */ jsx(Search, { className: "mr-2 h-4 w-4 shrink-0 text-fg-muted" }),
@@ -51,7 +51,7 @@ const CommandGroup = React.forwardRef(({ className, ...props }, ref) => /* @__PU
51
51
  {
52
52
  ref,
53
53
  className: cn(
54
- "overflow-hidden p-1 text-foreground [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:py-1.5 [&_[cmdk-group-heading]]:text-caption [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-fg-muted",
54
+ "overflow-hidden p-1 text-foreground [&_[cmdk-group-heading]]:px-3 [&_[cmdk-group-heading]]:py-1.5 [&_[cmdk-group-heading]]:text-caption [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-fg-muted",
55
55
  className
56
56
  ),
57
57
  ...props
@@ -72,7 +72,7 @@ const CommandItem = React.forwardRef(({ className, ...props }, ref) => /* @__PUR
72
72
  {
73
73
  ref,
74
74
  className: cn(
75
- "relative flex cursor-default gap-2 select-none items-center rounded-md px-2 py-1.5 text-body outline-none data-[disabled=true]:pointer-events-none data-[selected=true]:bg-neutral-hover data-[selected=true]:text-foreground data-[disabled=true]:text-fg-disabled [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
75
+ "relative flex cursor-default gap-2 select-none items-center rounded-md px-3 py-1.5 text-body outline-none data-[disabled=true]:pointer-events-none data-[selected=true]:bg-neutral-hover data-[selected=true]:text-foreground data-[disabled=true]:text-fg-disabled [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
76
76
  className
77
77
  ),
78
78
  ...props
@@ -1 +1 @@
1
- {"version":3,"file":"command.js","sources":["../../../src/components/Command/command.tsx"],"sourcesContent":["\"use client\"\n/**\n * @internal — DS-internal 單元(per `.claude/rules/ui-development.md` Public vs Internal canonical;spec frontmatter `isInternal`)。\n * 不進 root barrel front-door;由 SelectMenu(搜尋式選單引擎)等 DS 元件 wrap 消費,end-user app 請用 wrapper 元件。\n */\n\nimport * as React from \"react\"\nimport { type DialogProps } from \"@radix-ui/react-dialog\"\nimport { Command as CommandPrimitive } from \"cmdk\"\nimport { Search } from \"lucide-react\"\n\nimport { cn } from \"@/lib/utils\"\nimport { Dialog, DialogContent } from \"@/design-system/components/Dialog/dialog\"\nimport { ScrollArea } from \"@/design-system/components/ScrollArea/scroll-area\"\n\nconst Command = React.forwardRef<\n React.ElementRef<typeof CommandPrimitive>,\n React.ComponentPropsWithoutRef<typeof CommandPrimitive>\n>(({ className, ...props }, ref) => (\n <CommandPrimitive\n ref={ref}\n className={cn(\n \"flex h-full w-full flex-col overflow-hidden rounded-md bg-surface-raised text-foreground\",\n className\n )}\n {...props}\n />\n))\nCommand.displayName = CommandPrimitive.displayName\n\nconst CommandDialog = ({ children, ...props }: DialogProps) => {\n // M2 verified 2026-04-25 / 2026-06-11 更正歸因(cmdk/dist source):cmdk 於 DOM 上 emit\n // `cmdk-group-heading=\"\"` / `cmdk-group=\"\"` / `cmdk-input=\"\"` / `cmdk-item=\"\"` attributes;\n // `cmdk-input-wrapper=\"\"` 非 cmdk emit — 是本檔 CommandInput 自設的 wrapper div attribute(shadcn 慣例)。\n // 下列 `[&_[cmdk-*]]:` attribute selectors 皆有對應真實 DOM。\n return (\n <Dialog {...props}>\n <DialogContent className=\"overflow-hidden p-0 shadow-[var(--elevation-200)]\">\n <Command className=\"[&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-fg-muted [&_[cmdk-group]:not([hidden])_~[cmdk-group]]:pt-0 [&_[cmdk-group]]:px-2 [&_[cmdk-input-wrapper]_svg]:h-5 [&_[cmdk-input-wrapper]_svg]:w-5 [&_[cmdk-input]]:h-12 [&_[cmdk-item]]:px-2 [&_[cmdk-item]]:py-3 [&_[cmdk-item]_svg]:h-5 [&_[cmdk-item]_svg]:w-5\">\n {children}\n </Command>\n </DialogContent>\n </Dialog>\n )\n}\n\nconst CommandInput = React.forwardRef<\n React.ElementRef<typeof CommandPrimitive.Input>,\n React.ComponentPropsWithoutRef<typeof CommandPrimitive.Input>\n>(({ className, ...props }, ref) => (\n <div className=\"flex items-center border-b border-divider px-3\" cmdk-input-wrapper=\"\">\n <Search className=\"mr-2 h-4 w-4 shrink-0 text-fg-muted\" />\n <CommandPrimitive.Input\n ref={ref}\n className={cn(\n \"flex h-11 w-full rounded-md bg-transparent py-3 text-body outline-none placeholder:text-fg-muted disabled:cursor-not-allowed disabled:text-fg-disabled disabled:placeholder:text-fg-disabled\",\n className\n )}\n {...props}\n />\n </div>\n))\n\nCommandInput.displayName = CommandPrimitive.Input.displayName\n\n/**\n * CommandList — cmdk primitive 外包 ScrollArea 跨 OS scrollbar 一致。\n *\n * Verified against cmdk/dist/index.js(2026-04-25):cmdk selected-item auto-scroll\n * 用標準 `Element.scrollIntoView({block:\"nearest\"})`,browser 向上找 nearest\n * scrollable ancestor → 命中 ScrollArea.Viewport(`overflow:hidden scroll`)→ 自動\n * 捲入 selected ✓。不需 MutationObserver sync。\n *\n * `cmdk-list-sizer` ResizeObserver 只量 offsetHeight 設 CSS var `--cmdk-list-height`\n * (純測量,非 scroll logic),wrap 不影響。\n *\n * 跨 DS 一致:DataTable / Sheet / Sidebar / DropdownMenu / Command 皆走 ScrollArea。\n */\nconst CommandList = React.forwardRef<\n React.ElementRef<typeof CommandPrimitive.List>,\n React.ComponentPropsWithoutRef<typeof CommandPrimitive.List>\n>(({ className, ...props }, ref) => (\n <ScrollArea className=\"max-h-[var(--menu-max-height,300px)]\">\n <CommandPrimitive.List ref={ref} className={cn(\"overflow-x-hidden\", className)} {...props} />\n </ScrollArea>\n))\n\nCommandList.displayName = CommandPrimitive.List.displayName\n\nconst CommandEmpty = React.forwardRef<\n React.ElementRef<typeof CommandPrimitive.Empty>,\n React.ComponentPropsWithoutRef<typeof CommandPrimitive.Empty>\n>(({ className, ...props }, ref) => (\n <CommandPrimitive.Empty\n ref={ref}\n className={className}\n {...props}\n />\n))\n\nCommandEmpty.displayName = CommandPrimitive.Empty.displayName\n\nconst CommandGroup = React.forwardRef<\n React.ElementRef<typeof CommandPrimitive.Group>,\n React.ComponentPropsWithoutRef<typeof CommandPrimitive.Group>\n>(({ className, ...props }, ref) => (\n <CommandPrimitive.Group\n ref={ref}\n className={cn(\n \"overflow-hidden p-1 text-foreground [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:py-1.5 [&_[cmdk-group-heading]]:text-caption [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-fg-muted\",\n className\n )}\n {...props}\n />\n))\n\nCommandGroup.displayName = CommandPrimitive.Group.displayName\n\nconst CommandSeparator = React.forwardRef<\n React.ElementRef<typeof CommandPrimitive.Separator>,\n React.ComponentPropsWithoutRef<typeof CommandPrimitive.Separator>\n>(({ className, ...props }, ref) => (\n <CommandPrimitive.Separator\n ref={ref}\n className={cn(\"h-px bg-divider\", className)}\n {...props}\n />\n))\nCommandSeparator.displayName = CommandPrimitive.Separator.displayName\n\nconst CommandItem = React.forwardRef<\n React.ElementRef<typeof CommandPrimitive.Item>,\n React.ComponentPropsWithoutRef<typeof CommandPrimitive.Item>\n>(({ className, ...props }, ref) => (\n <CommandPrimitive.Item\n ref={ref}\n className={cn(\n \"relative flex cursor-default gap-2 select-none items-center rounded-md px-2 py-1.5 text-body outline-none data-[disabled=true]:pointer-events-none data-[selected=true]:bg-neutral-hover data-[selected=true]:text-foreground data-[disabled=true]:text-fg-disabled [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0\",\n className\n )}\n {...props}\n />\n))\n\nCommandItem.displayName = CommandPrimitive.Item.displayName\n\nconst CommandShortcut = ({\n className,\n ...props\n}: React.HTMLAttributes<HTMLSpanElement>) => {\n return (\n <span\n className={cn(\n \"ml-auto text-caption tracking-shortcut text-fg-muted\",\n className\n )}\n {...props}\n />\n )\n}\nCommandShortcut.displayName = \"CommandShortcut\"\n\n// Story auto-compile metadata — Phase 1 mechanical migration(2026-04-24)\n// Phase 2 fill needed: purpose descriptions + when rationale + world-class refs\nexport const commandMeta = {\n component: 'Command',\n family: 'composite', // 對齊 command.spec.md frontmatter family: composite(SSOT)\n variants: {\n\n },\n sizes: {\n\n },\n states: ['default', 'hover', 'active', 'focus-visible', 'disabled'],\n tokens: {\n bg: ['bg-neutral-hover', 'bg-surface-raised', 'bg-transparent'],\n fg: ['text-fg-disabled', 'text-fg-muted', 'text-foreground'],\n ring: [],\n },\n} as const\n\nexport {\n Command,\n CommandDialog,\n CommandInput,\n CommandList,\n CommandEmpty,\n CommandGroup,\n CommandItem,\n CommandShortcut,\n CommandSeparator,\n}\n"],"names":["CommandPrimitive"],"mappings":"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"}
1
+ {"version":3,"file":"command.js","sources":["../../../src/components/Command/command.tsx"],"sourcesContent":["\"use client\"\n/**\n * @internal — DS-internal 單元(per `.claude/rules/ui-development.md` Public vs Internal canonical;spec frontmatter `isInternal`)。\n * 不進 root barrel front-door;由 SelectMenu(搜尋式選單引擎)等 DS 元件 wrap 消費,end-user app 請用 wrapper 元件。\n */\n\nimport * as React from \"react\"\nimport { type DialogProps } from \"@radix-ui/react-dialog\"\nimport { Command as CommandPrimitive } from \"cmdk\"\nimport { Search } from \"lucide-react\"\n\nimport { cn } from \"@/lib/utils\"\nimport { Dialog, DialogContent } from \"@/design-system/components/Dialog/dialog\"\nimport { ScrollArea } from \"@/design-system/components/ScrollArea/scroll-area\"\n\nconst Command = React.forwardRef<\n React.ElementRef<typeof CommandPrimitive>,\n React.ComponentPropsWithoutRef<typeof CommandPrimitive>\n>(({ className, ...props }, ref) => (\n <CommandPrimitive\n ref={ref}\n className={cn(\n \"flex h-full w-full flex-col overflow-hidden rounded-md bg-surface-raised text-foreground\",\n className\n )}\n {...props}\n />\n))\nCommand.displayName = CommandPrimitive.displayName\n\nconst CommandDialog = ({ children, ...props }: DialogProps) => {\n // M2 verified 2026-04-25 / 2026-06-11 更正歸因(cmdk/dist source):cmdk 於 DOM 上 emit\n // `cmdk-group-heading=\"\"` / `cmdk-group=\"\"` / `cmdk-input=\"\"` / `cmdk-item=\"\"` attributes;\n // `cmdk-input-wrapper=\"\"` 非 cmdk emit — 是本檔 CommandInput 自設的 wrapper div attribute(shadcn 慣例)。\n // 下列 `[&_[cmdk-*]]:` attribute selectors 皆有對應真實 DOM。\n return (\n <Dialog {...props}>\n <DialogContent className=\"overflow-hidden p-0 shadow-[var(--elevation-200)]\">\n <Command className=\"[&_[cmdk-group-heading]]:px-3 [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-fg-muted [&_[cmdk-group]:not([hidden])_~[cmdk-group]]:pt-0 [&_[cmdk-group]]:px-3 [&_[cmdk-input-wrapper]_svg]:h-5 [&_[cmdk-input-wrapper]_svg]:w-5 [&_[cmdk-input]]:h-12 [&_[cmdk-item]]:px-3 [&_[cmdk-item]]:py-3 [&_[cmdk-item]_svg]:h-5 [&_[cmdk-item]_svg]:w-5\">\n {children}\n </Command>\n </DialogContent>\n </Dialog>\n )\n}\n\nconst CommandInput = React.forwardRef<\n React.ElementRef<typeof CommandPrimitive.Input>,\n React.ComponentPropsWithoutRef<typeof CommandPrimitive.Input>\n>(({ className, ...props }, ref) => (\n <div className=\"flex items-center border-b border-divider px-3\" cmdk-input-wrapper=\"\">\n <Search className=\"mr-2 h-4 w-4 shrink-0 text-fg-muted\" />\n <CommandPrimitive.Input\n ref={ref}\n className={cn(\n \"flex h-11 w-full rounded-md bg-transparent py-3 text-body outline-none placeholder:text-fg-muted disabled:cursor-not-allowed disabled:text-fg-disabled disabled:placeholder:text-fg-disabled\",\n className\n )}\n {...props}\n />\n </div>\n))\n\nCommandInput.displayName = CommandPrimitive.Input.displayName\n\n/**\n * CommandList — cmdk primitive 外包 ScrollArea 跨 OS scrollbar 一致。\n *\n * Verified against cmdk/dist/index.js(2026-04-25):cmdk selected-item auto-scroll\n * 用標準 `Element.scrollIntoView({block:\"nearest\"})`,browser 向上找 nearest\n * scrollable ancestor → 命中 ScrollArea.Viewport(`overflow:hidden scroll`)→ 自動\n * 捲入 selected ✓。不需 MutationObserver sync。\n *\n * `cmdk-list-sizer` ResizeObserver 只量 offsetHeight 設 CSS var `--cmdk-list-height`\n * (純測量,非 scroll logic),wrap 不影響。\n *\n * 跨 DS 一致:DataTable / Sheet / Sidebar / DropdownMenu / Command 皆走 ScrollArea。\n */\nconst CommandList = React.forwardRef<\n React.ElementRef<typeof CommandPrimitive.List>,\n React.ComponentPropsWithoutRef<typeof CommandPrimitive.List>\n>(({ className, ...props }, ref) => (\n <ScrollArea className=\"max-h-[var(--menu-max-height,300px)]\">\n <CommandPrimitive.List ref={ref} className={cn(\"overflow-x-hidden\", className)} {...props} />\n </ScrollArea>\n))\n\nCommandList.displayName = CommandPrimitive.List.displayName\n\nconst CommandEmpty = React.forwardRef<\n React.ElementRef<typeof CommandPrimitive.Empty>,\n React.ComponentPropsWithoutRef<typeof CommandPrimitive.Empty>\n>(({ className, ...props }, ref) => (\n <CommandPrimitive.Empty\n ref={ref}\n className={className}\n {...props}\n />\n))\n\nCommandEmpty.displayName = CommandPrimitive.Empty.displayName\n\nconst CommandGroup = React.forwardRef<\n React.ElementRef<typeof CommandPrimitive.Group>,\n React.ComponentPropsWithoutRef<typeof CommandPrimitive.Group>\n>(({ className, ...props }, ref) => (\n <CommandPrimitive.Group\n ref={ref}\n className={cn(\n \"overflow-hidden p-1 text-foreground [&_[cmdk-group-heading]]:px-3 [&_[cmdk-group-heading]]:py-1.5 [&_[cmdk-group-heading]]:text-caption [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-fg-muted\",\n className\n )}\n {...props}\n />\n))\n\nCommandGroup.displayName = CommandPrimitive.Group.displayName\n\nconst CommandSeparator = React.forwardRef<\n React.ElementRef<typeof CommandPrimitive.Separator>,\n React.ComponentPropsWithoutRef<typeof CommandPrimitive.Separator>\n>(({ className, ...props }, ref) => (\n <CommandPrimitive.Separator\n ref={ref}\n className={cn(\"h-px bg-divider\", className)}\n {...props}\n />\n))\nCommandSeparator.displayName = CommandPrimitive.Separator.displayName\n\nconst CommandItem = React.forwardRef<\n React.ElementRef<typeof CommandPrimitive.Item>,\n React.ComponentPropsWithoutRef<typeof CommandPrimitive.Item>\n>(({ className, ...props }, ref) => (\n <CommandPrimitive.Item\n ref={ref}\n className={cn(\n \"relative flex cursor-default gap-2 select-none items-center rounded-md px-3 py-1.5 text-body outline-none data-[disabled=true]:pointer-events-none data-[selected=true]:bg-neutral-hover data-[selected=true]:text-foreground data-[disabled=true]:text-fg-disabled [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0\",\n className\n )}\n {...props}\n />\n))\n\nCommandItem.displayName = CommandPrimitive.Item.displayName\n\nconst CommandShortcut = ({\n className,\n ...props\n}: React.HTMLAttributes<HTMLSpanElement>) => {\n return (\n <span\n className={cn(\n \"ml-auto text-caption tracking-shortcut text-fg-muted\",\n className\n )}\n {...props}\n />\n )\n}\nCommandShortcut.displayName = \"CommandShortcut\"\n\n// Story auto-compile metadata — Phase 1 mechanical migration(2026-04-24)\n// Phase 2 fill needed: purpose descriptions + when rationale + world-class refs\nexport const commandMeta = {\n component: 'Command',\n family: 'composite', // 對齊 command.spec.md frontmatter family: composite(SSOT)\n variants: {\n\n },\n sizes: {\n\n },\n states: ['default', 'hover', 'active', 'focus-visible', 'disabled'],\n tokens: {\n bg: ['bg-neutral-hover', 'bg-surface-raised', 'bg-transparent'],\n fg: ['text-fg-disabled', 'text-fg-muted', 'text-foreground'],\n ring: [],\n },\n} as const\n\nexport {\n Command,\n CommandDialog,\n CommandInput,\n CommandList,\n CommandEmpty,\n CommandGroup,\n CommandItem,\n CommandShortcut,\n CommandSeparator,\n}\n"],"names":["CommandPrimitive"],"mappings":"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"}
@@ -73,6 +73,12 @@
73
73
  "sourceHook": "check_sidebar_menu_button_implicit_wrap.sh",
74
74
  "bucket": "SHIP_AS_IS"
75
75
  },
76
+ {
77
+ "file": "hooks/check_tabs_content_chrome_body_double_gap.sh",
78
+ "sha256": "656e854fc8c3e61afd7da3705f87d169a2ea04979e446ed453114b42b9bdcfb1",
79
+ "sourceHook": "check_tabs_content_chrome_body_double_gap.sh",
80
+ "bucket": "SHIP_AS_IS"
81
+ },
76
82
  {
77
83
  "file": "hooks/check_tailwind_wildcard_in_docs.sh",
78
84
  "sha256": "51fcf04d3adc2a15b6c06312753e51412ed8ff88d088a5e8fe4ec0c784c7aa96",
@@ -102,7 +108,7 @@
102
108
  },
103
109
  {
104
110
  "file": "manifest.json",
105
- "sha256": "627ff28351c0af0baca271bbcfa9812389979218ffa64c4d23b4abee7606429a"
111
+ "sha256": "588eb31f9ad3c302217e7642b053cb8b4b81dcd2dd1c69959826f6d2317102ed"
106
112
  },
107
113
  {
108
114
  "file": "preamble.md",
@@ -0,0 +1,183 @@
1
+ #!/bin/bash
2
+ # check_tabs_content_chrome_body_double_gap.sh — P0 BLOCKER(overlay-body layout-space guardian,2 checks)
3
+ #
4
+ # **Check 1**(tabs double-gap):`<TabsContent>` 被放進 chrome scroll body(`<DialogBody>` /
5
+ # `<SheetBody>` / `<SurfaceBody>`)內卻沒 `mt-0` override → 雙重 gap owner。
6
+ # **Check 2**(overlay-body fixed macro gap,2026-07-01 Sheet gap-4 錨例):浮層 body 自身 className
7
+ # 用固定 `gap-[4-9]`(macro)而非 layout-space token → lg density 不縮放,drift。詳見下方 Check 2 段。
8
+ #
9
+ # 兩者同屬「overlay-body layout-space 合規」domain,折進一 hook(不增 hook 數)。
10
+ #
11
+ # Root invariant(item-anatomy.spec.md「垂直 padding 歸屬 / 禁雙重 padding」+ layoutSpace.spec.md
12
+ # 規則 2「Header → element = 單一 tight」):兩元素之間的 gap 只能有一個 owner。
13
+ # - DialogBody/SheetBody/SurfaceBody 內層 padded div 已用 `pt-/py-[var(--layout-space-tight)]`
14
+ # 擁有 header→content 的 chrome gap。
15
+ # - TabsContent 預設帶 `mt-[var(--layout-space-tight)]`(tabs.tsx,為「緊接 tab bar 下方」而設)。
16
+ # - tabsSlot composition 把 tab bar 抬進 header,TabsContent 留在 body → 兩 owner 疊加 =
17
+ # 雙重 tight(md 24 / lg 32)。修法 = chrome body 內的 TabsContent 加 `className="mt-0"`
18
+ # 把 ownership 轉移給 body(對齊 AppShell 前例 app-shell.stories.tsx + Carbon/Radix「panel
19
+ # 單一 owner」benchmark)。
20
+ #
21
+ # Anchor:2026-07-01 user 在「專案設定」Dialog(dialog.stories.tsx WithTabsInHeader)DevTools
22
+ # 抓到「專案名稱」上方兩個 --layout-space-tight。歷次稽核漏抓,因既有
23
+ # check_layout_space_magic_numbers.sh **明確 skip packages/design-system/src/**(該 hook L34)。
24
+ # 本 hook **反轉 scope** 專掃 DS src + apps composition,補該盲區。
25
+ #
26
+ # Scope:只掃 packages/design-system/src/** + apps/** 的 .tsx(含 .stories.tsx)。
27
+ # Level:P0 BLOCKER exit 2(layout-space SSOT 一律 P0,非 P1 WARN — per
28
+ # feedback_ssot_mechanical_p0_not_p1_warn_2026_05_27)。窄 gate(必同時含 chrome body + TabsContent)
29
+ # 控 false-positive。Escape:`// @tabs-content-gap-ok: <rationale>`(同行或前一註解行)。
30
+
31
+ source "$(dirname "$0")/_log-fire.sh" 2>/dev/null && log_hook_fire
32
+
33
+ set -uo pipefail
34
+
35
+ INPUT=$(cat 2>/dev/null || echo "{}")
36
+ TOOL=$(echo "$INPUT" | jq -r '.tool_name // ""' 2>/dev/null)
37
+
38
+ case "${TOOL:-}" in
39
+ Edit|Write|MultiEdit) ;;
40
+ *) exit 0 ;;
41
+ esac
42
+
43
+ FILE=$(echo "$INPUT" | jq -r '.tool_input.file_path // .tool_input.notebook_path // ""' 2>/dev/null)
44
+
45
+ # Scope:DS source + apps,.tsx only(INVERSE of check_layout_space_magic_numbers.sh 的 DS-src skip)
46
+ if ! echo "$FILE" | grep -qE '\.tsx$'; then exit 0; fi
47
+ if ! echo "$FILE" | grep -qE '(packages/design-system/src/|/apps/|(^|/)apps/)'; then exit 0; fi
48
+
49
+ # PostToolUse:file 已寫入磁碟,讀「完整檔」做 containment 分析(避免 hunk-only partial-edit 盲區 —
50
+ # 部分 edit 的 new_string 可能只含 TabsContent 行不含外層 DialogBody,window 分析會漏)。
51
+ [ -f "$FILE" ] || exit 0
52
+ CONTENT=$(cat "$FILE" 2>/dev/null)
53
+ [ -z "$CONTENT" ] && exit 0
54
+
55
+ # ══ Check 2:overlay body 自身用固定 macro gap-N → 該用 layout-space token(2026-07-01 Sheet gap-4 錨例)══
56
+ # Root(layoutSpace.spec.md 規則 2/3):浮層 body 直接堆疊內容的垂直 gap 屬 macro layout-space,該用
57
+ # density-scaling token(並列 = loose / functional 交互 = tight),非固定 gap-N(lg 不縮放 → 跟 DS
58
+ # 自己 dialog.stories 的 gap-[var(--layout-space-loose)] 不一致)。窄 gate:只抓 overlay body 自身
59
+ # className 的固定 gap-[4-9](macro range;排除 gap-1/2/3 micro + 已 tokenize 的 gap-[var)。
60
+ # 全庫掃證實零現存假陽性(純防未來 drift)。Escape:同行或前一註解行 @overlay-body-gap-ok:。
61
+ GAP_MARKER='@overlay-body-gap-ok:'
62
+ GAP_HITS=$(printf '%s\n' "$CONTENT" | grep -nE '<(DialogBody|SheetBody|PopoverBody|SurfaceBody)[^>]*gap-[4-9]([^0-9]|$)' 2>/dev/null | grep -v 'gap-\[var')
63
+ GAP_UNJUSTIFIED=""
64
+ if [ -n "$GAP_HITS" ]; then
65
+ while IFS= read -r hit; do
66
+ [ -z "$hit" ] && continue
67
+ ln="${hit%%:*}"
68
+ cur="${hit#*:}"
69
+ if echo "$cur" | grep -qF "$GAP_MARKER"; then continue; fi
70
+ if [ "$ln" -gt 1 ] 2>/dev/null; then
71
+ prev=$(printf '%s\n' "$CONTENT" | sed -n "$((ln-1))p")
72
+ if echo "$prev" | grep -qF "$GAP_MARKER" && echo "$prev" | grep -qE '^[[:space:]]*(//|\{?/\*|\*)'; then continue; fi
73
+ fi
74
+ GAP_UNJUSTIFIED="${GAP_UNJUSTIFIED}${ln}: $(echo "$cur" | sed 's/^[[:space:]]*//')\n"
75
+ done <<< "$GAP_HITS"
76
+ fi
77
+ if [ -n "$GAP_UNJUSTIFIED" ]; then
78
+ cat >&2 << EOF
79
+ 🚨 OVERLAY-BODY FIXED-GAP BLOCKER(P0,2026-07-01 Sheet demo gap-4 錨例)
80
+
81
+ 在 $FILE 偵測到浮層 body(Dialog/Sheet/Popover/Surface Body)自身用固定 macro gap-N:
82
+ $(echo -e "$GAP_UNJUSTIFIED" | sed 's/^/ /' | head -10)
83
+
84
+ ── 為什麼 ──
85
+ 浮層 body 直接堆疊內容(並列表單欄位 / 區塊)的垂直 gap 屬 macro layout-space,該用
86
+ density-scaling token(並列 = loose / functional 交互 = tight),非固定 gap-N(lg 不縮放 →
87
+ 跟 DS 自己 dialog.stories 的 gap-[var(--layout-space-loose)] 不一致)。
88
+
89
+ ── 修法 2 選 1 ──
90
+ (a) gap-N → gap-[var(--layout-space-loose)](並列)/ gap-[var(--layout-space-tight)](functional 交互)
91
+ (b) Escape:該行或前一註解行加 \`// @overlay-body-gap-ok: <rationale>\`(刻意 list 間距 / 視覺平衡)
92
+
93
+ canonical → tokens/layoutSpace/layoutSpace.spec.md 規則 2/3
94
+ EOF
95
+ exit 2
96
+ fi
97
+
98
+ # ══ Check 1:<TabsContent> 在 chrome body 內雙重 gap(原 beta.78 邏輯)══
99
+ # Gate:必同時含 chrome body open tag 且 TabsContent 才啟動(narrow surface,絕大多數 edit 靜默)
100
+ if ! echo "$CONTENT" | grep -qE '<(DialogBody|SheetBody|SurfaceBody)[ />]'; then exit 0; fi
101
+ if ! echo "$CONTENT" | grep -q '<TabsContent'; then exit 0; fi
102
+
103
+ ESCAPE_MARKER='@tabs-content-gap-ok:'
104
+
105
+ # 逐行 depth-counter(awk):進 chrome body +1、出 -1;TabsContent 在 depth>0 = 在 chrome body 內。
106
+ # - 註解行(JSDoc `*` / `//` / `{/*` / `/*` / `*/`)整行跳過,不計 depth 也不判 TabsContent
107
+ # (避免 JSDoc 內 `<DialogBody>` 範例污染 depth / 內含的 mt-0 example 被誤判)。
108
+ # - 同行 open 先加 depth 再判 TabsContent 再減 close → 同行 `<DialogBody><TabsContent>` 與
109
+ # `<TabsContent>...</DialogBody>` 都正確歸屬「inside」。
110
+ # - TabsContent opening tag 可能跨行:accumulate 到 `>` 再驗 className 有無 canonical `mt-0`
111
+ # (class-boundary anchored;非-0 / arbitrary 值需 escape marker)。
112
+ # 輸出:每個「chrome body 內且無 mt-0 override」的 TabsContent 起始行號(供 bash escape 過濾)。
113
+ CANDIDATES=$(printf '%s\n' "$CONTENT" | awk '
114
+ function check_tag(tag, ln) {
115
+ # 只認 canonical `mt-0`(class-boundary anchored):排除 `mt-0.5` / `mt-04` 等 substring 假通過
116
+ # (mt-0.5 是本 DS 真實 class,item-anatomy.stories.tsx:766;2px 不抵銷 pt-tight → 仍雙重)。
117
+ # arbitrary `mt-[...]` / 非-0 值 = 非 canonical → 需 escape marker 明示 documented(非靜默通過)。
118
+ if (tag ~ /(^|[^a-zA-Z0-9])mt-0([^a-zA-Z0-9.]|$)/) return
119
+ print ln
120
+ }
121
+ BEGIN { depth=0; collecting=0; buf=""; startline=0 }
122
+ {
123
+ raw=$0
124
+ if (collecting) {
125
+ buf = buf " " raw
126
+ if (raw ~ />/) { collecting=0; check_tag(buf, startline); buf="" }
127
+ next
128
+ }
129
+ t=raw; sub(/^[ \t]+/,"",t)
130
+ if (t ~ /^(\*|\/\/|\{?\/\*|\*\/)/) next # skip comment-only lines
131
+ tmp=raw; o=gsub(/<(DialogBody|SheetBody|SurfaceBody)[ \t\/>]/,"X",tmp)
132
+ tmpc=raw; c=gsub(/<\/(DialogBody|SheetBody|SurfaceBody)>/,"X",tmpc)
133
+ depth += o
134
+ if (raw ~ /<TabsContent/ && depth > 0) {
135
+ idx = index(raw, "<TabsContent")
136
+ rest = substr(raw, idx)
137
+ if (rest ~ />/) { gt = index(rest, ">"); check_tag(substr(rest, 1, gt), NR) }
138
+ else { collecting=1; buf=rest; startline=NR }
139
+ }
140
+ depth -= c
141
+ }
142
+ ')
143
+
144
+ if [ -z "$CANDIDATES" ]; then exit 0; fi
145
+
146
+ # Escape 過濾:同行 OR 前一「註解專用行」含 @tabs-content-gap-ok: → 豁免(對齊
147
+ # check_layout_space_magic_numbers.sh L57-69 已 debug 過的 preceding-line marker 邏輯)。
148
+ UNJUSTIFIED=""
149
+ while IFS= read -r ln; do
150
+ [ -z "$ln" ] && continue
151
+ cur=$(echo "$CONTENT" | sed -n "${ln}p")
152
+ if echo "$cur" | grep -qF "$ESCAPE_MARKER"; then continue; fi
153
+ if [ "$ln" -gt 1 ] 2>/dev/null; then
154
+ prev=$(echo "$CONTENT" | sed -n "$((ln-1))p")
155
+ if echo "$prev" | grep -qF "$ESCAPE_MARKER" && echo "$prev" | grep -qE '^[[:space:]]*(//|\{?/\*|\*)'; then continue; fi
156
+ fi
157
+ UNJUSTIFIED="${UNJUSTIFIED}${ln}: $(echo "$cur" | sed 's/^[[:space:]]*//')\n"
158
+ done <<< "$CANDIDATES"
159
+
160
+ if [ -z "$UNJUSTIFIED" ]; then exit 0; fi
161
+
162
+ cat >&2 << EOF
163
+ 🚨 TABS-CONTENT-CHROME-BODY DOUBLE-GAP BLOCKER(P0,2026-07-01 user「專案名稱上方兩個 space-tight」)
164
+
165
+ 在 $FILE 偵測到 <TabsContent> 放在 chrome scroll body(DialogBody/SheetBody/SurfaceBody)內
166
+ 但未 override margin-top:
167
+ $(echo -e "$UNJUSTIFIED" | sed 's/^/ /' | head -10)
168
+
169
+ ── 為什麼是 bug ──
170
+ DialogBody/SheetBody/SurfaceBody 內層已用 pt-/py-[var(--layout-space-tight)] 擁有 header→content
171
+ 的 chrome gap;TabsContent 預設又帶 mt-[var(--layout-space-tight)](tabs.tsx,為緊接 tab bar 而設)。
172
+ tabsSlot composition 把 tab bar 抬進 header 後,兩個 owner 疊加 = 雙重 tight(md 24 / lg 32)。
173
+ 一軸只能有一個 spacing owner(item-anatomy.spec.md「垂直 padding 歸屬 / 禁雙重 padding」)。
174
+
175
+ ── 修法 2 選 1 ──
176
+ (a) chrome body 內的 TabsContent 加 \`className="mt-0"\` 把 ownership 轉移給 body
177
+ (對齊 AppShell 前例 app-shell.stories.tsx + Carbon/Radix「panel 單一 owner」benchmark)
178
+ (b) Escape:該行或前一註解行加 \`// @tabs-content-gap-ok: <rationale>\`(顯式 documented 例外)
179
+
180
+ 完整 canonical → components/Tabs/tabs.spec.md「出現在 Dialog」段
181
+ + tokens/layoutSpace/layoutSpace.spec.md 規則 2
182
+ EOF
183
+ exit 2
@@ -22,6 +22,11 @@
22
22
  "sourceHook": "check_tailwind_wildcard_in_docs.sh",
23
23
  "bucket": "SHIP_AS_IS"
24
24
  },
25
+ {
26
+ "file": "hooks/check_tabs_content_chrome_body_double_gap.sh",
27
+ "sourceHook": "check_tabs_content_chrome_body_double_gap.sh",
28
+ "bucket": "SHIP_AS_IS"
29
+ },
25
30
  {
26
31
  "file": "hooks/check_escape_marker_abuse.sh",
27
32
  "sourceHook": "check_escape_marker_abuse.sh",
@@ -0,0 +1,183 @@
1
+ #!/bin/bash
2
+ # check_tabs_content_chrome_body_double_gap.sh — P0 BLOCKER(overlay-body layout-space guardian,2 checks)
3
+ #
4
+ # **Check 1**(tabs double-gap):`<TabsContent>` 被放進 chrome scroll body(`<DialogBody>` /
5
+ # `<SheetBody>` / `<SurfaceBody>`)內卻沒 `mt-0` override → 雙重 gap owner。
6
+ # **Check 2**(overlay-body fixed macro gap,2026-07-01 Sheet gap-4 錨例):浮層 body 自身 className
7
+ # 用固定 `gap-[4-9]`(macro)而非 layout-space token → lg density 不縮放,drift。詳見下方 Check 2 段。
8
+ #
9
+ # 兩者同屬「overlay-body layout-space 合規」domain,折進一 hook(不增 hook 數)。
10
+ #
11
+ # Root invariant(item-anatomy.spec.md「垂直 padding 歸屬 / 禁雙重 padding」+ layoutSpace.spec.md
12
+ # 規則 2「Header → element = 單一 tight」):兩元素之間的 gap 只能有一個 owner。
13
+ # - DialogBody/SheetBody/SurfaceBody 內層 padded div 已用 `pt-/py-[var(--layout-space-tight)]`
14
+ # 擁有 header→content 的 chrome gap。
15
+ # - TabsContent 預設帶 `mt-[var(--layout-space-tight)]`(tabs.tsx,為「緊接 tab bar 下方」而設)。
16
+ # - tabsSlot composition 把 tab bar 抬進 header,TabsContent 留在 body → 兩 owner 疊加 =
17
+ # 雙重 tight(md 24 / lg 32)。修法 = chrome body 內的 TabsContent 加 `className="mt-0"`
18
+ # 把 ownership 轉移給 body(對齊 AppShell 前例 app-shell.stories.tsx + Carbon/Radix「panel
19
+ # 單一 owner」benchmark)。
20
+ #
21
+ # Anchor:2026-07-01 user 在「專案設定」Dialog(dialog.stories.tsx WithTabsInHeader)DevTools
22
+ # 抓到「專案名稱」上方兩個 --layout-space-tight。歷次稽核漏抓,因既有
23
+ # check_layout_space_magic_numbers.sh **明確 skip packages/design-system/src/**(該 hook L34)。
24
+ # 本 hook **反轉 scope** 專掃 DS src + apps composition,補該盲區。
25
+ #
26
+ # Scope:只掃 packages/design-system/src/** + apps/** 的 .tsx(含 .stories.tsx)。
27
+ # Level:P0 BLOCKER exit 2(layout-space SSOT 一律 P0,非 P1 WARN — per
28
+ # feedback_ssot_mechanical_p0_not_p1_warn_2026_05_27)。窄 gate(必同時含 chrome body + TabsContent)
29
+ # 控 false-positive。Escape:`// @tabs-content-gap-ok: <rationale>`(同行或前一註解行)。
30
+
31
+ source "$(dirname "$0")/_log-fire.sh" 2>/dev/null && log_hook_fire
32
+
33
+ set -uo pipefail
34
+
35
+ INPUT=$(cat 2>/dev/null || echo "{}")
36
+ TOOL=$(echo "$INPUT" | jq -r '.tool_name // ""' 2>/dev/null)
37
+
38
+ case "${TOOL:-}" in
39
+ Edit|Write|MultiEdit) ;;
40
+ *) exit 0 ;;
41
+ esac
42
+
43
+ FILE=$(echo "$INPUT" | jq -r '.tool_input.file_path // .tool_input.notebook_path // ""' 2>/dev/null)
44
+
45
+ # Scope:DS source + apps,.tsx only(INVERSE of check_layout_space_magic_numbers.sh 的 DS-src skip)
46
+ if ! echo "$FILE" | grep -qE '\.tsx$'; then exit 0; fi
47
+ if ! echo "$FILE" | grep -qE '(packages/design-system/src/|/apps/|(^|/)apps/)'; then exit 0; fi
48
+
49
+ # PostToolUse:file 已寫入磁碟,讀「完整檔」做 containment 分析(避免 hunk-only partial-edit 盲區 —
50
+ # 部分 edit 的 new_string 可能只含 TabsContent 行不含外層 DialogBody,window 分析會漏)。
51
+ [ -f "$FILE" ] || exit 0
52
+ CONTENT=$(cat "$FILE" 2>/dev/null)
53
+ [ -z "$CONTENT" ] && exit 0
54
+
55
+ # ══ Check 2:overlay body 自身用固定 macro gap-N → 該用 layout-space token(2026-07-01 Sheet gap-4 錨例)══
56
+ # Root(layoutSpace.spec.md 規則 2/3):浮層 body 直接堆疊內容的垂直 gap 屬 macro layout-space,該用
57
+ # density-scaling token(並列 = loose / functional 交互 = tight),非固定 gap-N(lg 不縮放 → 跟 DS
58
+ # 自己 dialog.stories 的 gap-[var(--layout-space-loose)] 不一致)。窄 gate:只抓 overlay body 自身
59
+ # className 的固定 gap-[4-9](macro range;排除 gap-1/2/3 micro + 已 tokenize 的 gap-[var)。
60
+ # 全庫掃證實零現存假陽性(純防未來 drift)。Escape:同行或前一註解行 @overlay-body-gap-ok:。
61
+ GAP_MARKER='@overlay-body-gap-ok:'
62
+ GAP_HITS=$(printf '%s\n' "$CONTENT" | grep -nE '<(DialogBody|SheetBody|PopoverBody|SurfaceBody)[^>]*gap-[4-9]([^0-9]|$)' 2>/dev/null | grep -v 'gap-\[var')
63
+ GAP_UNJUSTIFIED=""
64
+ if [ -n "$GAP_HITS" ]; then
65
+ while IFS= read -r hit; do
66
+ [ -z "$hit" ] && continue
67
+ ln="${hit%%:*}"
68
+ cur="${hit#*:}"
69
+ if echo "$cur" | grep -qF "$GAP_MARKER"; then continue; fi
70
+ if [ "$ln" -gt 1 ] 2>/dev/null; then
71
+ prev=$(printf '%s\n' "$CONTENT" | sed -n "$((ln-1))p")
72
+ if echo "$prev" | grep -qF "$GAP_MARKER" && echo "$prev" | grep -qE '^[[:space:]]*(//|\{?/\*|\*)'; then continue; fi
73
+ fi
74
+ GAP_UNJUSTIFIED="${GAP_UNJUSTIFIED}${ln}: $(echo "$cur" | sed 's/^[[:space:]]*//')\n"
75
+ done <<< "$GAP_HITS"
76
+ fi
77
+ if [ -n "$GAP_UNJUSTIFIED" ]; then
78
+ cat >&2 << EOF
79
+ 🚨 OVERLAY-BODY FIXED-GAP BLOCKER(P0,2026-07-01 Sheet demo gap-4 錨例)
80
+
81
+ 在 $FILE 偵測到浮層 body(Dialog/Sheet/Popover/Surface Body)自身用固定 macro gap-N:
82
+ $(echo -e "$GAP_UNJUSTIFIED" | sed 's/^/ /' | head -10)
83
+
84
+ ── 為什麼 ──
85
+ 浮層 body 直接堆疊內容(並列表單欄位 / 區塊)的垂直 gap 屬 macro layout-space,該用
86
+ density-scaling token(並列 = loose / functional 交互 = tight),非固定 gap-N(lg 不縮放 →
87
+ 跟 DS 自己 dialog.stories 的 gap-[var(--layout-space-loose)] 不一致)。
88
+
89
+ ── 修法 2 選 1 ──
90
+ (a) gap-N → gap-[var(--layout-space-loose)](並列)/ gap-[var(--layout-space-tight)](functional 交互)
91
+ (b) Escape:該行或前一註解行加 \`// @overlay-body-gap-ok: <rationale>\`(刻意 list 間距 / 視覺平衡)
92
+
93
+ canonical → tokens/layoutSpace/layoutSpace.spec.md 規則 2/3
94
+ EOF
95
+ exit 2
96
+ fi
97
+
98
+ # ══ Check 1:<TabsContent> 在 chrome body 內雙重 gap(原 beta.78 邏輯)══
99
+ # Gate:必同時含 chrome body open tag 且 TabsContent 才啟動(narrow surface,絕大多數 edit 靜默)
100
+ if ! echo "$CONTENT" | grep -qE '<(DialogBody|SheetBody|SurfaceBody)[ />]'; then exit 0; fi
101
+ if ! echo "$CONTENT" | grep -q '<TabsContent'; then exit 0; fi
102
+
103
+ ESCAPE_MARKER='@tabs-content-gap-ok:'
104
+
105
+ # 逐行 depth-counter(awk):進 chrome body +1、出 -1;TabsContent 在 depth>0 = 在 chrome body 內。
106
+ # - 註解行(JSDoc `*` / `//` / `{/*` / `/*` / `*/`)整行跳過,不計 depth 也不判 TabsContent
107
+ # (避免 JSDoc 內 `<DialogBody>` 範例污染 depth / 內含的 mt-0 example 被誤判)。
108
+ # - 同行 open 先加 depth 再判 TabsContent 再減 close → 同行 `<DialogBody><TabsContent>` 與
109
+ # `<TabsContent>...</DialogBody>` 都正確歸屬「inside」。
110
+ # - TabsContent opening tag 可能跨行:accumulate 到 `>` 再驗 className 有無 canonical `mt-0`
111
+ # (class-boundary anchored;非-0 / arbitrary 值需 escape marker)。
112
+ # 輸出:每個「chrome body 內且無 mt-0 override」的 TabsContent 起始行號(供 bash escape 過濾)。
113
+ CANDIDATES=$(printf '%s\n' "$CONTENT" | awk '
114
+ function check_tag(tag, ln) {
115
+ # 只認 canonical `mt-0`(class-boundary anchored):排除 `mt-0.5` / `mt-04` 等 substring 假通過
116
+ # (mt-0.5 是本 DS 真實 class,item-anatomy.stories.tsx:766;2px 不抵銷 pt-tight → 仍雙重)。
117
+ # arbitrary `mt-[...]` / 非-0 值 = 非 canonical → 需 escape marker 明示 documented(非靜默通過)。
118
+ if (tag ~ /(^|[^a-zA-Z0-9])mt-0([^a-zA-Z0-9.]|$)/) return
119
+ print ln
120
+ }
121
+ BEGIN { depth=0; collecting=0; buf=""; startline=0 }
122
+ {
123
+ raw=$0
124
+ if (collecting) {
125
+ buf = buf " " raw
126
+ if (raw ~ />/) { collecting=0; check_tag(buf, startline); buf="" }
127
+ next
128
+ }
129
+ t=raw; sub(/^[ \t]+/,"",t)
130
+ if (t ~ /^(\*|\/\/|\{?\/\*|\*\/)/) next # skip comment-only lines
131
+ tmp=raw; o=gsub(/<(DialogBody|SheetBody|SurfaceBody)[ \t\/>]/,"X",tmp)
132
+ tmpc=raw; c=gsub(/<\/(DialogBody|SheetBody|SurfaceBody)>/,"X",tmpc)
133
+ depth += o
134
+ if (raw ~ /<TabsContent/ && depth > 0) {
135
+ idx = index(raw, "<TabsContent")
136
+ rest = substr(raw, idx)
137
+ if (rest ~ />/) { gt = index(rest, ">"); check_tag(substr(rest, 1, gt), NR) }
138
+ else { collecting=1; buf=rest; startline=NR }
139
+ }
140
+ depth -= c
141
+ }
142
+ ')
143
+
144
+ if [ -z "$CANDIDATES" ]; then exit 0; fi
145
+
146
+ # Escape 過濾:同行 OR 前一「註解專用行」含 @tabs-content-gap-ok: → 豁免(對齊
147
+ # check_layout_space_magic_numbers.sh L57-69 已 debug 過的 preceding-line marker 邏輯)。
148
+ UNJUSTIFIED=""
149
+ while IFS= read -r ln; do
150
+ [ -z "$ln" ] && continue
151
+ cur=$(echo "$CONTENT" | sed -n "${ln}p")
152
+ if echo "$cur" | grep -qF "$ESCAPE_MARKER"; then continue; fi
153
+ if [ "$ln" -gt 1 ] 2>/dev/null; then
154
+ prev=$(echo "$CONTENT" | sed -n "$((ln-1))p")
155
+ if echo "$prev" | grep -qF "$ESCAPE_MARKER" && echo "$prev" | grep -qE '^[[:space:]]*(//|\{?/\*|\*)'; then continue; fi
156
+ fi
157
+ UNJUSTIFIED="${UNJUSTIFIED}${ln}: $(echo "$cur" | sed 's/^[[:space:]]*//')\n"
158
+ done <<< "$CANDIDATES"
159
+
160
+ if [ -z "$UNJUSTIFIED" ]; then exit 0; fi
161
+
162
+ cat >&2 << EOF
163
+ 🚨 TABS-CONTENT-CHROME-BODY DOUBLE-GAP BLOCKER(P0,2026-07-01 user「專案名稱上方兩個 space-tight」)
164
+
165
+ 在 $FILE 偵測到 <TabsContent> 放在 chrome scroll body(DialogBody/SheetBody/SurfaceBody)內
166
+ 但未 override margin-top:
167
+ $(echo -e "$UNJUSTIFIED" | sed 's/^/ /' | head -10)
168
+
169
+ ── 為什麼是 bug ──
170
+ DialogBody/SheetBody/SurfaceBody 內層已用 pt-/py-[var(--layout-space-tight)] 擁有 header→content
171
+ 的 chrome gap;TabsContent 預設又帶 mt-[var(--layout-space-tight)](tabs.tsx,為緊接 tab bar 而設)。
172
+ tabsSlot composition 把 tab bar 抬進 header 後,兩個 owner 疊加 = 雙重 tight(md 24 / lg 32)。
173
+ 一軸只能有一個 spacing owner(item-anatomy.spec.md「垂直 padding 歸屬 / 禁雙重 padding」)。
174
+
175
+ ── 修法 2 選 1 ──
176
+ (a) chrome body 內的 TabsContent 加 \`className="mt-0"\` 把 ownership 轉移給 body
177
+ (對齊 AppShell 前例 app-shell.stories.tsx + Carbon/Radix「panel 單一 owner」benchmark)
178
+ (b) Escape:該行或前一註解行加 \`// @tabs-content-gap-ok: <rationale>\`(顯式 documented 例外)
179
+
180
+ 完整 canonical → components/Tabs/tabs.spec.md「出現在 Dialog」段
181
+ + tokens/layoutSpace/layoutSpace.spec.md 規則 2
182
+ EOF
183
+ exit 2
@@ -38,7 +38,7 @@ N. <SSOT 是否該抽到更上層 primitive?>
38
38
  - 必產 3-column cite table(claim / path:line / reasoning)
39
39
 
40
40
  ## 🚨 三 invariant — 2026-05-23 user 永久 directive(缺一 = brief 不合格)
41
- 1. **全盤閱讀全部 source**(CLAUDE.md / 31 active M-rules / 5 rules/*.md / 88 audit dims SKILL.md / 全部 spec.md / 全部 stories / 全部 components / tokens / patterns / hooks / 全部 memory files / planning),**禁憑記憶**(spec/story/component/memory 數量隨時變動,故不硬編數字 — 一律「全部」;M-rule / audit-dim 數由 sync-governance-counters 機械守住)
41
+ 1. **全盤閱讀全部 source**(CLAUDE.md / 31 active M-rules / 5 rules/*.md / 90 audit dims SKILL.md / 全部 spec.md / 全部 stories / 全部 components / tokens / patterns / hooks / 全部 memory files / planning),**禁憑記憶**(spec/story/component/memory 數量隨時變動,故不硬編數字 — 一律「全部」;M-rule / audit-dim 數由 sync-governance-counters 機械守住)
42
42
  2. **Triple-verify per finding**:(a) grep DS-wide 確認 pattern 存在 (b) Read 對應 spec.md / tsx 確認 problem 真存在 (c) 對照既有 canonical 確認非 documented exception。任一 NO → 自動撤回,**禁無病呻吟**
43
43
  3. **禁抽樣** — DS-wide ALL files 全掃,context 不夠拆 stage 全跑完;sub-agent admission「I sampled / spot-check / representative」= audit incomplete reject
44
44
  ```
@@ -213,8 +213,10 @@ User 2026-05-15 verbatim 抓「DS 深度稽核漏 storybook content quality」+
213
213
  | 86 | **Plugin manifest + symlink-vs-source 一致**(ds-repo-mode-only)| `node scripts/plugin-structure-validate.mjs`:`.claude-plugin/{plugin,marketplace}.json` valid + 5-manifest version 對齊 + plugin symlink set(skills / hooks / rules)== source(fork `/plugin install` 後拿到完整 governance 不缺漏)。3-allowlist 軸 2:plugin symlink == source;軸 3(mirror 涵蓋 apps/template + 排除 .claude/skills)走 dim 84 |
214
214
  | 87 | **Scaffold 可建性 dogfood prepublish gate**(ds-repo-mode-only)| `node scripts/dogfood-prepublish-verify.mjs`:published scaffold 真能 `npm install` + `build-storybook`(consumer 視角,非僅 byte-mirror 比對),prepublish 攔「ship 出去裝不起來」|
215
215
  | 88 | **Infra-self ref integrity**(ds-repo-mode-only;2026-05-30 加,根因防線)| `node scripts/check-dangling-infra-ref.mjs --check`(governance docs 引用的 hook/script 必存在磁碟 OR 同行 annotate retired/未實作;認 `check_X.sh`→`_X.sh` lib-consolidation form)+ `node scripts/check-skill-deadref.mjs --check`(禁 `CLAUDE.md line N` brittle ref + 已移除 section deny-list)。錨例 2026-05-30:抓 40 處死 hook ref + env-smoke set-e bug,前無機械防線。M7/M34 broad-vs-narrow gap closed。**2026-05-31 infra-audit 修**:SCAN_DIRS 補 `.claude/memory`(原漏=memory infra ref 零防線)。**已知 P3 限制**:annotation-bypass — 同行含 retired/folded/未實作 字樣即 Bucket A,未驗該 hook 真曾存在(fabricated「(retired)」ref 可繞);需 git-history verify 才全閉,暫接受(adversarial-only 風險,queued #9)|
216
+ | 89 | **Composition-boundary 雙重 spacing(tabsSlot-in-chrome-body)**(2026-07-01,item-anatomy 雙重 padding invariant)| `<TabsContent>` 放進 `DialogBody`/`SheetBody`/`SurfaceBody`(自帶 `pt-/py-tight`)內必 `mt-0` — chrome body 已擁有 header→content gap(規則 2),TabsContent 預設 `mt-tight` 雙重疊加(md 24 / lg 32)。一軸單一 spacing owner(`item-anatomy.spec.md`「垂直 padding 歸屬」)。Hook `check_tabs_content_chrome_body_double_gap.sh` 是 pre-write 層(P0 BLOCKER),本 dim 是 DS-wide sweep。Escape `// @tabs-content-gap-ok:`。SSOT `components/Tabs/tabs.spec.md`「出現在 Dialog」段 |
217
+ | 90 | **Layout-space macro 合規 sweep(judgment 層,DS-src 盲區)**(2026-07-01,user「確保都遵循沒偏移」)| **LLM adversarial per-component**:並列 **區塊/卡片/表單欄位** 之間的 macro 垂直/水平 gap、chrome 水平內距、header→content、content→action button —— 該用 density-scaling **layout-space token** 卻硬寫固定 `gap-N`/`space-y-N`/`p-N`/`px-N`(規則 1-6)。**判準邊界**見 `tokens/layoutSpace/layoutSpace.spec.md`「該用 token vs 刻意固定」段:**只抓 macro 結構違規,禁 flag 合法 magic number**(同質 list 列間距 / micro-icon / 刻意視覺平衡 / 元件內部固定如 FieldGroup)。**每 finding 必對抗二次驗證殺假陽性**(2026-07-01 錨例:sweep 35 raw → 8 confirmed → 逐項覆核剩真違規 Sheet gap-4;over-flag ProfileCard/apps-scaffold/list-gap 被判非違規)。既有 magic-number hook skip DS-src → 本 dim 是唯一 DS-src layout-space judgment 防線(regex 分不出 macro/micro,故靠 LLM judgment 而非硬 hook)。SSOT `layoutSpace.spec.md` 6 規則 + 邊界段 |
216
218
 
217
- **Heavy dim(`--deep` mode 各必獨立 sub-agent 跑,不可 batch)**:12 / 24 / 25 / 40 / 41 / 42 / 43 / 45 / 49 / 50 / 51 / 52 / 53。Sub-agent prompt 嚴禁含「SKIP」keyword(per Phase 1 NO-SKIP invariant)。
219
+ **Heavy dim(`--deep` mode 各必獨立 sub-agent 跑,不可 batch)**:12 / 24 / 25 / 40 / 41 / 42 / 43 / 45 / 49 / 50 / 51 / 52 / 53 / 90。Sub-agent prompt 嚴禁含「SKIP」keyword(per Phase 1 NO-SKIP invariant)。
218
220
 
219
221
  ---
220
222
 
@@ -116,7 +116,7 @@ function PageHeader({ title, rightSlot }: { title: string; rightSlot?: ReactElem
116
116
 
117
117
  function DashboardPage() {
118
118
  return (
119
- <div className="px-[var(--layout-space-loose)] py-[var(--layout-space-tight)] space-y-6">
119
+ <div className="px-[var(--layout-space-loose)] py-[var(--layout-space-tight)] space-y-[var(--layout-space-loose)]">
120
120
  <section>
121
121
  <h2 className="text-h5 mb-2">Today</h2>
122
122
  <p className="text-body text-fg-secondary">
@@ -124,9 +124,9 @@ function DashboardPage() {
124
124
  (DataTable / Chart / Card / Stat 等),never modify DS source。
125
125
  </p>
126
126
  </section>
127
- <section className="grid grid-cols-3 gap-4">
127
+ <section className="grid grid-cols-3 gap-[var(--layout-space-loose)]">
128
128
  {['Revenue', 'Active customers', 'Pending orders'].map((label) => (
129
- <div key={label} className="rounded-lg border border-divider bg-surface p-4">
129
+ <div key={label} className="rounded-lg border border-divider bg-surface p-[var(--layout-space-loose)]">
130
130
  <div className="text-caption text-fg-secondary">{label}</div>
131
131
  <div className="text-h3 mt-1">—</div>
132
132
  </div>
package/llms-full.txt CHANGED
@@ -1,6 +1,6 @@
1
1
  # @qijenchen/design-system — 完整設計參考(llms-full)
2
2
 
3
- > 全 component / pattern 的 variants / sizes / 禁止事項。build-time 從 spec.md frontmatter 生成,禁手改。v0.1.0-beta.77
3
+ > 全 component / pattern 的 variants / sizes / 禁止事項。build-time 從 spec.md frontmatter 生成,禁手改。v0.1.0-beta.79
4
4
 
5
5
  # Components
6
6
 
package/llms.txt CHANGED
@@ -1,7 +1,7 @@
1
1
  # @qijenchen/design-system
2
2
 
3
3
  > World-class React design system(Radix/shadcn + Tailwind v4 + 自訂 design token)。
4
- > 54 components + 4 public patterns + design tokens。v0.1.0-beta.77
4
+ > 54 components + 4 public patterns + design tokens。v0.1.0-beta.79
5
5
 
6
6
  本檔由 source(spec.md frontmatter + Storybook index)build-time 自動生成,**禁手改**(CI --check drift gate 守)。
7
7
  每元件 / pattern 的完整 variants / sizes / 禁止事項 全文見 [llms-full.txt](./llms-full.txt)。
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@qijenchen/design-system",
3
- "version": "0.1.0-beta.77",
3
+ "version": "0.1.0-beta.79",
4
4
  "private": false,
5
5
  "description": "World-class design system — components, patterns, tokens, hooks (single source of truth for team distribution).",
6
6
  "type": "module",
@@ -36,7 +36,7 @@ const CommandDialog = ({ children, ...props }: DialogProps) => {
36
36
  return (
37
37
  <Dialog {...props}>
38
38
  <DialogContent className="overflow-hidden p-0 shadow-[var(--elevation-200)]">
39
- <Command className="[&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-fg-muted [&_[cmdk-group]:not([hidden])_~[cmdk-group]]:pt-0 [&_[cmdk-group]]:px-2 [&_[cmdk-input-wrapper]_svg]:h-5 [&_[cmdk-input-wrapper]_svg]:w-5 [&_[cmdk-input]]:h-12 [&_[cmdk-item]]:px-2 [&_[cmdk-item]]:py-3 [&_[cmdk-item]_svg]:h-5 [&_[cmdk-item]_svg]:w-5">
39
+ <Command className="[&_[cmdk-group-heading]]:px-3 [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-fg-muted [&_[cmdk-group]:not([hidden])_~[cmdk-group]]:pt-0 [&_[cmdk-group]]:px-3 [&_[cmdk-input-wrapper]_svg]:h-5 [&_[cmdk-input-wrapper]_svg]:w-5 [&_[cmdk-input]]:h-12 [&_[cmdk-item]]:px-3 [&_[cmdk-item]]:py-3 [&_[cmdk-item]_svg]:h-5 [&_[cmdk-item]_svg]:w-5">
40
40
  {children}
41
41
  </Command>
42
42
  </DialogContent>
@@ -107,7 +107,7 @@ const CommandGroup = React.forwardRef<
107
107
  <CommandPrimitive.Group
108
108
  ref={ref}
109
109
  className={cn(
110
- "overflow-hidden p-1 text-foreground [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:py-1.5 [&_[cmdk-group-heading]]:text-caption [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-fg-muted",
110
+ "overflow-hidden p-1 text-foreground [&_[cmdk-group-heading]]:px-3 [&_[cmdk-group-heading]]:py-1.5 [&_[cmdk-group-heading]]:text-caption [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-fg-muted",
111
111
  className
112
112
  )}
113
113
  {...props}
@@ -135,7 +135,7 @@ const CommandItem = React.forwardRef<
135
135
  <CommandPrimitive.Item
136
136
  ref={ref}
137
137
  className={cn(
138
- "relative flex cursor-default gap-2 select-none items-center rounded-md px-2 py-1.5 text-body outline-none data-[disabled=true]:pointer-events-none data-[selected=true]:bg-neutral-hover data-[selected=true]:text-foreground data-[disabled=true]:text-fg-disabled [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
138
+ "relative flex cursor-default gap-2 select-none items-center rounded-md px-3 py-1.5 text-body outline-none data-[disabled=true]:pointer-events-none data-[selected=true]:bg-neutral-hover data-[selected=true]:text-foreground data-[disabled=true]:text-fg-disabled [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
139
139
  className
140
140
  )}
141
141
  {...props}
@@ -365,9 +365,16 @@ export const ListBody = {
365
365
  * W4 header content row + tabs row flush stack(gap = 0)
366
366
  *
367
367
  * Anatomy(consumer 寫法):`<Tabs>` wrap 整 DialogContent → `<DialogHeader tabsSlot={<TabsList>}>` →
368
- * `<DialogBody><TabsContent>...</TabsContent></DialogBody>`
368
+ * `<DialogBody><TabsContent className="mt-0">...</TabsContent></DialogBody>`
369
369
  *
370
370
  * 注意:TabsContent 必放 DialogBody 內(scroll + chrome padding 由 DialogBody 自管),不要自加 padding。
371
+ * ⚠️ 一軸單一 spacing owner(item-anatomy.spec.md「垂直 padding 歸屬 / 禁雙重 padding」):DialogBody
372
+ * 內層已用 `pt-[var(--layout-space-tight)]` 擁有 header→content 的 chrome gap;而 TabsContent 預設
373
+ * 帶 `mt-[var(--layout-space-tight)]`(tabs.tsx:489,為「緊接 tab bar 下方」而設)。tabsSlot 這條
374
+ * composition 把 tab bar 抬進 header,兩 owner 疊加 = 雙重 tight(md 24 / lg 32,user 2026-07-01 抓)。
375
+ * 故 chrome body 內的 TabsContent **必 `className="mt-0"`** 把 ownership 轉移給 DialogBody(對齊
376
+ * AppShell 前例 app-shell.stories.tsx:337 + tabs.spec.md「出現在 Dialog」段)。Hook
377
+ * `check_tabs_content_chrome_body_double_gap.sh` 機械強制。
371
378
  */
372
379
  export const WithTabsInHeader = {
373
380
  name: '標頭內含分頁',
@@ -390,7 +397,8 @@ export const WithTabsInHeader = {
390
397
  <DialogTitle>專案設定</DialogTitle>
391
398
  </DialogHeader>
392
399
  <DialogBody>
393
- <TabsContent value="general">
400
+ {/* mt-0:DialogBody pt-tight 已擁有 header→content gap,TabsContent 預設 mt-tight 會雙重(見上方 JSDoc)*/}
401
+ <TabsContent value="general" className="mt-0">
394
402
  <div className="flex flex-col gap-[var(--layout-space-loose)]">
395
403
  <Field>
396
404
  <FieldLabel>專案名稱</FieldLabel>
@@ -402,7 +410,7 @@ export const WithTabsInHeader = {
402
410
  </Field>
403
411
  </div>
404
412
  </TabsContent>
405
- <TabsContent value="members">
413
+ <TabsContent value="members" className="mt-0">
406
414
  <div className="flex flex-col gap-3">
407
415
  {[
408
416
  ['Alan Chen', '管理員'],
@@ -416,7 +424,7 @@ export const WithTabsInHeader = {
416
424
  ))}
417
425
  </div>
418
426
  </TabsContent>
419
- <TabsContent value="integrations">
427
+ <TabsContent value="integrations" className="mt-0">
420
428
  <div className="flex flex-col gap-3">
421
429
  {[
422
430
  { key: 'slack', label: 'Slack 通知', on: true },
@@ -363,7 +363,7 @@ Field 內的資料輸入控件(Input / NumberInput / DatePicker / Select / Com
363
363
  - **表單欄位的 label + control + description + error 標準佈局**:登入表單、設定頁、建立對話框
364
364
  - **需要 required 星號、disabled 狀態、invalid 驗證的統一行為**
365
365
  - **需要 `horizontal` / `vertical` 排版切換**:設定頁常用 horizontal(label 左 / control 右)
366
- - **多欄位垂直堆疊**:搭配 `FieldGroup` 管理 density-aware 間距
366
+ - **多欄位垂直堆疊**:搭配 `FieldGroup` 統一管理欄位垂直間距(三級固定值 `compact` / `normal` / `loose`,刻意不隨 density 縮放;具體 gap map 見上方 gap 表)
367
367
 
368
368
  ## 何時不用
369
369
 
@@ -102,7 +102,7 @@ export const UsageGuidance: Story = {
102
102
  </SheetHeader>
103
103
  {/* Body 必用 SheetBody(內建 ScrollArea + 浮層 padding SSOT px-loose/pt-tight/pb-bottom);
104
104
  自組 ScrollArea + py-4 = 水平 0 padding 歷史 bug,詳 sheet.tsx SheetBody comment。 */}
105
- <SheetBody className="flex flex-col gap-4">
105
+ <SheetBody className="flex flex-col gap-[var(--layout-space-loose)]">
106
106
  <Field>
107
107
  <FieldLabel>名稱</FieldLabel>
108
108
  <Input defaultValue="產品路線圖" />
@@ -317,7 +317,7 @@ export const HeaderFooterStructureRule: Story = {
317
317
  <SheetDescription>填寫基本資料後可進行付款設定</SheetDescription>
318
318
  </SheetHeader>
319
319
  {/* Body 必用 SheetBody(內建 ScrollArea + 浮層 padding SSOT) */}
320
- <SheetBody className="flex flex-col gap-4">
320
+ <SheetBody className="flex flex-col gap-[var(--layout-space-loose)]">
321
321
  <Field>
322
322
  <FieldLabel>公司名稱</FieldLabel>
323
323
  <Input placeholder="輸入公司名稱" />
@@ -185,6 +185,8 @@ Tabs 是 **navigation anchor**,不是 compact control:
185
185
 
186
186
  **TabsContent 與 TabsList 間距(2026-06-12 user 拍板)**:`--layout-space-tight`(md 12px,density 連動),**內建於 TabsContent**(`mt-[var(--layout-space-tight)]`,className 可覆寫;full-height 佈局用 `mt-0`)。依 `layoutSpace.spec`「親疏 3 級」第一級(同 bundle,元件 spec own)+ 規則 3「直接功能依賴 = tight」精神;**不**取 Ant 的固定 16px(M23 自家 token 優先)。內容本身的排版歸 item-anatomy / 各內容元件管,本條只管 list↔content gap。
187
187
 
188
+ > ⚠️ **此預設只在 TabsContent 緊接 tab bar 當直接 sibling 時成立**(standalone 頁面 tabs、ChromeHeader-as-sibling)。當 `tabsSlot` composition 把 tab bar 抬進 chrome header、TabsContent 留在 `DialogBody`/`SheetBody`/`SurfaceBody`(chrome scroll body,已自帶 `pt-/py-[var(--layout-space-tight)]`)內時,body 已擁有 header→content gap,TabsContent 的預設 `mt-tight` 會**雙重疊加**(md 24 / lg 32)。此時 TabsContent **必 `className="mt-0"`** 把 ownership 轉移給 body — 詳見下方「出現在 Dialog」段。一軸單一 spacing owner,per `item-anatomy.spec.md`「垂直 padding 歸屬 / 禁雙重 padding」。Hook `check_tabs_content_chrome_body_double_gap.sh` 機械強制。
189
+
188
190
  ---
189
191
 
190
192
  ## Underline 與 TabsList border 的視覺關係
@@ -219,6 +221,8 @@ Tabs 常與容器 header 的底邊 border 合併——**視覺上只有一條線
219
221
 
220
222
  Size 建議:overlay / chrome header 內用 `sm`(32/40)— 對應 close X 也是 sm,視覺一致;**獨立取代 chrome header** 的 page-level workspace tabs 用 `lg`(48/56,= chrome-header-height)。
221
223
 
224
+ **間距 ownership**:tabsSlot 模式下 `TabsContent` 放 `DialogBody`/`SheetBody`/`SurfaceBody`(自帶 `pt-/py-tight`)內時**必 `className="mt-0"`** — body 已擁有 header→content gap,TabsContent 預設 `mt-tight` 會雙重(md 24 / lg 32,2026-07-01 user 抓)。詳「TabsContent 與 TabsList 間距」段;hook `check_tabs_content_chrome_body_double_gap.sh` 強制。
225
+
222
226
  ---
223
227
 
224
228
  ## 狀態
@@ -245,6 +245,8 @@ Row 集合是**內容(content)**,不是區段(region)。加 py 到 row 集合會
245
245
 
246
246
  **曾經發生的 bug**:TreeView 原本硬寫 `py-2`,放進 `SidebarGroup`(也有 `py-2`)導致 label 和 first tree item 之間多出 8px 無法解釋的 gap;後來改成「只有 menu context 加 py-2」,結果 story 的 bordered wrapper 不屬於 menu context,items 貼邊。最終解法:TreeView root 一律不加 py,所有外層容器自己負責。
247
247
 
248
+ **同源 invariant(不只 row 集合)**:「一軸單一 spacing owner」也適用元件的**單一 margin**跨 composition boundary。2026-07-01:`TabsContent` 內建 `mt-[var(--layout-space-tight)]`(tab bar 下方間距),`tabsSlot` 模式把 tab bar 抬進 chrome header 後 TabsContent 留在 `DialogBody`(自帶 `pt-tight`)內 → header→content gap 兩個 owner 疊加 = 雙重 tight(「專案名稱」上方 md 24 / lg 32)。解法同上:chrome body 已擁有 gap,TabsContent 加 `mt-0` 轉移 ownership。詳 `components/Tabs/tabs.spec.md`「出現在 Dialog」段 + hook `check_tabs_content_chrome_body_double_gap.sh`。
249
+
248
250
  ### 實作規則
249
251
 
250
252
  - 寫 row 集合元件(新 tree / menu / list)時,**root div 不加 py**
@@ -145,6 +145,23 @@ Pattern C 的視覺邏輯見 `overlay-surface.spec.md`「Hover bg 貼邊 chrome
145
145
 
146
146
  ---
147
147
 
148
+ ## 該用 token(縮放)vs 刻意固定(magic number 合法)的邊界(2026-07-01 codify)
149
+
150
+ **核心**:layout-space token(density-scaling)只管 **macro 結構間距**;**不是所有 px 都該 token 化**。硬把刻意固定值 token 化 = over-tokenize(反 pattern,跟「硬寫該縮放的 gap」同樣是 bug 的反面)。
151
+
152
+ | 該用 layout-space token(density-scaling) | 刻意固定值 OK(magic number 合法) |
153
+ |---|---|
154
+ | 並列獨立 **區塊 / 卡片 / 表單欄位** 之間(規則 3 parallel = loose)| **同質 list 的列間距**(一串同類 rows,規則 3「同範疇 spec-own」)|
155
+ | **Header → 內容**(規則 2 = tight)| **micro / icon / 控件內部** 間距(gap-1/2、icon padding)|
156
+ | **Chrome 水平內距**(規則 6 = loose)| **為視覺平衡 / 對稱 刻意調的值**(設計裁量)|
157
+ | **內容 → action button**(規則 4 = bottom 48)| **元件自身刻意固定**(如 FieldGroup 三級固定 gap,不隨 density)|
158
+
159
+ **判準一句話**:是「並列/區塊/chrome/header→content/content→action 的 macro 結構間距、規則說該縮放」→ 用 token;是「list 列間距 / micro / 刻意固定 / 元件內部決定」→ magic number 合法,**不要 token 化**。
160
+
161
+ **機械強制**:`check_tabs_content_chrome_body_double_gap.sh` Check 2 抓「浮層 body 自身固定 macro gap」(明確 pattern);判斷題(這是並列還是 list?macro 還是 micro?)靠 **週期 LLM sweep**(`/design-system-audit` dim「layout-space macro 合規」)—— 純 regex 分不出,不硬機械化(否則 over-flag 合法 magic number)。
162
+
163
+ ---
164
+
148
165
  ## 心智模型(三步推理 + 具體 self-questions)
149
166
 
150
167
  ### Step 1 — 判角色(對單一元素)
@@ -290,6 +307,7 @@ document.documentElement.setAttribute('data-layout-space', 'lg')
290
307
 
291
308
  - **不抽 universal LayoutBody / FormLayout primitive**:world-class(Material / Polaris / Atlassian / Carbon / Mantine)都「每元件 own variant + 共享 token」;規則 1-6 universal,角色 scope-relative 易誤封裝。獨特 chrome 已在 `overlay-surface.spec.md` + `action-bar.spec.md`
292
309
  - **List-as-region in overlay body**(2026-05-01 canonical):當 Dialog / Sheet / Popover body 內容 = unbounded list-as-region(menu / Cmd+K / nav)時,consumer 用 className override 撤掉 chrome padding(`<DialogBody className="!px-0 !pt-0 !pb-0">`)+ 自管 list outer wrapper(`<div className="py-2">`)+ item 自帶 `px-loose rounded-md`。**不做成 body variant**(`flush?: boolean`)— Material/Atlassian/Mantine/shadcn 都 consumer 自管,Polaris flush API scope 極窄;variant 不解決底層脆弱(加 1 row search/banner 就破功),反把 1 surface decision 拆兩 API。詳 `overlay-surface.spec.md`「List-as-region in overlay body」+ `dialog.tsx:165` JSDoc
310
+ - **Tabs-in-chrome-body 單一 gap owner**(2026-07-01 canonical):`tabsSlot` 模式下 `TabsContent` 放進 `DialogBody`/`SheetBody`/`SurfaceBody`(自帶 `pt-/py-tight`)時**必 `className="mt-0"`** — chrome body 已擁有 header→content gap(規則 2「element → tight」),TabsContent 內建 `mt-tight` 會雙重疊加(md 24 / lg 32)。同「一軸單一 spacing owner」原則(`item-anatomy.spec.md`「垂直 padding 歸屬 / 禁雙重 padding」)。詳 `components/Tabs/tabs.spec.md`「出現在 Dialog」段 + hook `check_tabs_content_chrome_body_double_gap.sh`
293
311
  - **v1 → v6**(2026-04-30):block-adjacent 機械 → 親疏判 + bundled-family 分權 + region 二分(bounded/unbounded)+ 多 region 限制。詳 git + memory `feedback_layout_v6_canonical.md`
294
312
 
295
313
  ---