@qijenchen/design-system 0.1.0-beta.64 → 0.1.0-beta.65

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 (61) hide show
  1. package/CLAUDE.md +1 -1
  2. package/dist/components/Checkbox/checkbox.d.ts.map +1 -1
  3. package/dist/components/Checkbox/checkbox.js +28 -3
  4. package/dist/components/Checkbox/checkbox.js.map +1 -1
  5. package/dist/components/PeoplePicker/person-display.d.ts.map +1 -1
  6. package/dist/components/PeoplePicker/person-display.js +1 -1
  7. package/dist/components/PeoplePicker/person-display.js.map +1 -1
  8. package/dist/components/RadioGroup/radio-group.d.ts +1 -1
  9. package/dist/components/RadioGroup/radio-group.d.ts.map +1 -1
  10. package/dist/components/RadioGroup/radio-group.js +46 -14
  11. package/dist/components/RadioGroup/radio-group.js.map +1 -1
  12. package/dist/components/Rating/rating.d.ts.map +1 -1
  13. package/dist/components/Rating/rating.js +5 -3
  14. package/dist/components/Rating/rating.js.map +1 -1
  15. package/dist/components/Slider/slider.d.ts +1 -1
  16. package/dist/components/Slider/slider.d.ts.map +1 -1
  17. package/dist/components/Slider/slider.js +11 -6
  18. package/dist/components/Slider/slider.js.map +1 -1
  19. package/dist/components/Switch/switch.d.ts +9 -7
  20. package/dist/components/Switch/switch.d.ts.map +1 -1
  21. package/dist/components/Switch/switch.js +30 -5
  22. package/dist/components/Switch/switch.js.map +1 -1
  23. package/dist/components/Tabs/tabs.d.ts.map +1 -1
  24. package/dist/components/Tabs/tabs.js +9 -3
  25. package/dist/components/Tabs/tabs.js.map +1 -1
  26. package/ds-canonical/hooks/check_consumer_app_invariants.sh +9 -0
  27. package/ds-canonical/references/story-baseline-registry.json +18 -2
  28. package/ds-canonical/references/ui-dev-rules.md +21 -0
  29. package/llms-full.txt +1 -1
  30. package/llms.txt +1 -1
  31. package/package.json +1 -1
  32. package/src/components/Accordion/accordion.spec.md +1 -1
  33. package/src/components/AppShell/app-shell.stories.tsx +3 -3
  34. package/src/components/Carousel/carousel.principles.stories.tsx +3 -3
  35. package/src/components/Checkbox/checkbox.spec.md +9 -1
  36. package/src/components/Checkbox/checkbox.tsx +45 -3
  37. package/src/components/Field/field-controls.spec.md +3 -1
  38. package/src/components/Field/field.anatomy.stories.tsx +3 -1
  39. package/src/components/Field/field.stories.tsx +14 -1
  40. package/src/components/PeoplePicker/person-display.tsx +4 -3
  41. package/src/components/ProgressBar/progress-bar.anatomy.stories.tsx +2 -2
  42. package/src/components/RadioGroup/radio-group.anatomy.stories.tsx +1 -1
  43. package/src/components/RadioGroup/radio-group.spec.md +2 -0
  44. package/src/components/RadioGroup/radio-group.tsx +59 -15
  45. package/src/components/Rating/rating.tsx +7 -3
  46. package/src/components/Sidebar/sidebar.spec.md +2 -0
  47. package/src/components/Slider/slider.anatomy.stories.tsx +2 -1
  48. package/src/components/Slider/slider.spec.md +8 -7
  49. package/src/components/Slider/slider.tsx +24 -11
  50. package/src/components/Switch/switch.anatomy.stories.tsx +4 -4
  51. package/src/components/Switch/switch.principles.stories.tsx +3 -3
  52. package/src/components/Switch/switch.spec.md +10 -6
  53. package/src/components/Switch/switch.tsx +45 -12
  54. package/src/components/Tabs/tabs.anatomy.stories.tsx +3 -3
  55. package/src/components/Tabs/tabs.principles.stories.tsx +1 -1
  56. package/src/components/Tabs/tabs.spec.md +4 -0
  57. package/src/components/Tabs/tabs.stories.tsx +4 -4
  58. package/src/components/Tabs/tabs.tsx +9 -3
  59. package/src/patterns/header-canonical/header-canonical.spec.md +1 -1
  60. package/src/styles/base.css +9 -2
  61. package/src/tokens/color/semantic.css +5 -1
@@ -1 +1 @@
1
- {"version":3,"file":"tabs.js","sources":["../../../src/components/Tabs/tabs.tsx"],"sourcesContent":["// @benchmark-unverified-blanket: file-level retraction per M22 (d) — claims herein not individually URL-cited; treat as unverified visual/usage rumor unless retrofit per-claim. Hook escape preserved.\n// code-quality-allow: file-size — foundational composite(Tabs + overflow scroll mode + dropdown switcher + inline action slot)在單一 wrapper SSOT 內,拆分會破壞 a11y / focus management chain。當前 515 < cap 800。\nimport * as React from 'react'\nimport * as TabsPrimitive from '@radix-ui/react-tabs'\nimport { cva } from 'class-variance-authority'\nimport type { LucideIcon } from 'lucide-react'\nimport { cn } from '@/lib/utils'\nimport {\n DropdownMenu,\n DropdownMenuTrigger,\n DropdownMenuContent,\n DropdownMenuItem,\n} from '@/design-system/components/DropdownMenu/dropdown-menu'\nimport {\n useScrollEdges,\n useScrollByPage,\n buildFadeMask,\n ARROW_BUTTON_WIDTH,\n OverflowScrollArrow,\n OverflowMenuTriggerButton,\n} from '@/design-system/patterns/horizontal-overflow/horizontal-overflow'\nimport { ICON_SIZE } from '@/design-system/tokens/uiSize/icon-size'\n\n/**\n * Tabs — 基於 Radix Tabs,橋接設計系統 token\n *\n * ── 定位 ──\n * 同一上下文底下切換平行的 view。切 view(不是切 value)。\n * 切 value 用 SegmentedControl;切路由用 navigation。\n *\n * ── Size ──\n * sm h-tab-sm(32/40),★ 預設(2026-05-17 從 md 改),overlay / chrome / dense\n * md h-tab-md(40/48),future tier 無 recommended use case\n * lg h-tab-lg(48/56),page-level hero / 獨立 tabs 取代 chrome header\n *\n * ── 寬度行為 ──\n * Trigger 寬度永遠由內容決定(hug content)。\n * Triggers 之間 gap 為 --layout-space-loose(16px / 24px lg density)。\n *\n * ── Trigger 結構 ──\n * [startIcon?] [label] [suffix: badge? + endIcon?]\n * slot 間 gap-2,suffix 內 gap-1\n *\n * ── Selected underline ──\n * 使用 ::after 絕對定位在 bottom: -1px,2px primary-hover,\n * 蓋住 TabsList 的 1px gray border(單一視覺線條,不雙線)。\n */\n\n// ── Size context ──\ntype TabsSize = 'sm' | 'md' | 'lg'\ntype TabsOverflow = 'none' | 'scroll' | 'menu'\n\ninterface TabsContextValue {\n size: TabsSize\n}\nconst TabsContext = React.createContext<TabsContextValue>({ size: 'md' })\n\n// ── Root ──\n// Wrap Radix Tabs 以支援 value/onValueChange 的 context pass-through\n// (menu 模式的 overflow items 需要能 proxy click 觸發同一個 onValueChange)\ninterface TabsProps extends React.ComponentPropsWithoutRef<typeof TabsPrimitive.Root> {}\n\nconst TabsValueContext = React.createContext<{\n value?: string\n onValueChange?: (value: string) => void\n}>({})\n\nconst Tabs = React.forwardRef<\n React.ElementRef<typeof TabsPrimitive.Root>,\n TabsProps\n>(({ value, onValueChange, defaultValue, children, ...props }, ref) => {\n // 內部維護一份 uncontrolled state,讓 overflow menu 的 proxy 有 onValueChange 可呼叫\n const [internalValue, setInternalValue] = React.useState<string | undefined>(defaultValue)\n const isControlled = value !== undefined\n const currentValue = isControlled ? value : internalValue\n\n const handleValueChange = React.useCallback(\n (next: string) => {\n if (!isControlled) setInternalValue(next)\n onValueChange?.(next)\n },\n [isControlled, onValueChange]\n )\n\n const valueContext = React.useMemo(\n () => ({ value: currentValue, onValueChange: handleValueChange }),\n [currentValue, handleValueChange]\n )\n\n return (\n <TabsValueContext.Provider value={valueContext}>\n <TabsPrimitive.Root\n ref={ref}\n value={currentValue}\n onValueChange={handleValueChange}\n {...props}\n >\n {children}\n </TabsPrimitive.Root>\n </TabsValueContext.Provider>\n )\n})\nTabs.displayName = 'Tabs'\n\n// ── List ──\n// TabsList 基礎 class — inline-flex 單列 + gap-loose + 底部 border-divider\n// 2026-05-18 改 border-border → border-divider(per user verbatim「我認為應該把 tabs 的\n// 下底線統一改成是 divider 色吧?」+「做」approval):\n// - 跟 Dialog / Sheet / Popover / Sidebar header `border-b border-divider`(neutral-4)同色\n// - withTabs scenario 下 tabs underline = chrome separator,跟 dialog 其他 separator 視覺一致\n// - Selected trigger 2px primary 仍 overlay underlying divider(對比 primary >> divider 不弱)\n// - 對齊 `color.spec.md`「T-junction connectivity 原則」段 outer-vs-divider 判準(Dialog 結構,T-junction 思路適用)\nconst TABS_LIST_BASE = [\n 'inline-flex items-stretch',\n 'gap-[var(--layout-space-loose)]',\n 'border-b border-divider',\n].join(' ')\n\ninterface TabsListProps\n extends React.ComponentPropsWithoutRef<typeof TabsPrimitive.List> {\n size?: TabsSize\n /**\n * Overflow 處理模式。詳見 tabs.spec.md 的 overflow 段。\n * 'none' ★ 預設,不處理,triggers 溢出父容器(適用 tabs 數量可控的情境)\n * 'scroll' 單行橫向滾動 + 邊緣 fade mask(Material / Polaris / iOS 作法)\n * 'menu' show-all navigator——全部 triggers 一直顯示在底層 overflow-x-auto 捲動容器內,\n * (Ant Design / Atlassian 作法)\n */\n overflow?: TabsOverflow\n}\n\nconst TabsList = React.forwardRef<\n React.ElementRef<typeof TabsPrimitive.List>,\n TabsListProps\n>(({ className, size = 'sm', overflow = 'none', children, ...props }, ref) => {\n const tabsSizeContext = React.useMemo(() => ({ size }), [size])\n if (overflow === 'scroll') {\n return (\n <TabsContext.Provider value={tabsSizeContext}>\n <ScrollTabsList ref={ref} className={className} {...props}>\n {children}\n </ScrollTabsList>\n </TabsContext.Provider>\n )\n }\n if (overflow === 'menu') {\n return (\n <TabsContext.Provider value={tabsSizeContext}>\n <MenuTabsList ref={ref} className={className} {...props}>\n {children}\n </MenuTabsList>\n </TabsContext.Provider>\n )\n }\n // none(預設)\n return (\n <TabsContext.Provider value={tabsSizeContext}>\n <TabsPrimitive.List\n ref={ref}\n className={cn(TABS_LIST_BASE, 'w-fit', className)}\n {...props}\n >\n {children}\n </TabsPrimitive.List>\n </TabsContext.Provider>\n )\n})\nTabsList.displayName = 'TabsList'\n\n// ── Scroll mode ──\n//\n// 共同策略(對齊 Material 3 / Ant Design / Primer UnderlineNav 世界級作法):\n// - 容器 overflow-x-auto + overflow-y-hidden(真的可滾,鍵盤焦點可 scroll-into-view;\n// 明示 y-hidden 阻 CSS overflow-3 spec「一軸 auto 時另軸 visible compute auto」)\n// - border-b 在 TabsList 內部(TABS_LIST_BASE),不在 outer wrapper,避免\n// active underline `after:bottom:-1px` 跟 outer + overflow-x-auto 觸發 y promote bug\n// - mask / arrow / fade 全部從 horizontal-overflow pattern module 取得——\n// 參見 `patterns/horizontal-overflow/horizontal-overflow.spec.md`\n// - Menu 模式 = scroll 模式 + 額外的 ⌄ quick-jump button,點 menu item 同時\n// 觸發 onValueChange + scrollIntoView,讓使用者在 tab strip 看到選中的 tab\n// 單行水平滾動 + 邊緣 fade mask 延伸到 arrow button 底下 + 左右 always-visible arrows\n// - 鍵盤: Radix 原生方向鍵 + 瀏覽器 scroll-into-view\n// - Trackpad: 兩指橫向滑動\n// - 滑鼠滾輪: 點 arrow buttons (Shift+wheel 太隱晦)\n\nconst ScrollTabsList = React.forwardRef<\n React.ElementRef<typeof TabsPrimitive.List>,\n React.ComponentPropsWithoutRef<typeof TabsPrimitive.List>\n>(({ className, children, ...props }, ref) => {\n const { scrollRef, atStart, atEnd, canScroll } = useScrollEdges<HTMLDivElement>()\n const scrollByPage = useScrollByPage(scrollRef)\n const maskImage = buildFadeMask({\n canScroll,\n atStart,\n atEnd,\n reserveArrowWidth: ARROW_BUTTON_WIDTH,\n })\n\n return (\n // 2026-05-19 fix(scroll-overflow underline clip + y auto-promote):\n // outer 撤 `border-b` → owner 升到 `TabsList` (TABS_LIST_BASE 含 border-b border-divider)\n // 把 trigger `after:bottom-[-1px]` 2px underline 的下半部 1px 收進 list border-box,\n // 再加 `overflow-y-hidden` 明示阻 browser y auto-promote(CSS overflow-3 spec:\n // overflow-x:auto + overflow-y:visible 必 compute auto)。\n // 不加 `pb-px`(outer border 撤後 list border 已接 -1px 部分,加 pb 多 1px 多餘空白)。\n // 對齊 Primer UnderlineNav `overflow-x:auto; overflow-y:hidden` canonical 同步動\n // horizontal-overflow.spec.md(「Hook re-export」+「典型 scroll / menu 模式組裝」段)owner 升 list 內部。\n <div className=\"relative\">\n <div\n ref={scrollRef}\n className=\"overflow-x-auto overflow-y-hidden [scrollbar-width:none] [&::-webkit-scrollbar]:hidden\"\n style={{ maskImage, WebkitMaskImage: maskImage }}\n >\n <TabsPrimitive.List\n ref={ref}\n className={cn(TABS_LIST_BASE, 'w-fit', className)}\n {...props}\n >\n {children}\n </TabsPrimitive.List>\n </div>\n {!atStart && canScroll && (\n <OverflowScrollArrow direction=\"left\" onClick={() => scrollByPage('left')} />\n )}\n {!atEnd && canScroll && (\n <OverflowScrollArrow direction=\"right\" onClick={() => scrollByPage('right')} />\n )}\n </div>\n )\n})\nScrollTabsList.displayName = 'ScrollTabsList'\n\n// ── Menu mode ──\n// Show-all navigator pattern (Chrome tab dropdown / VS Code editor tabs / Discord channel jumper):\n// - Menu 永遠顯示全部 tabs,active 的用 checked 標記 (單選語意)\n// - 點 menu item = onValueChange + scrollIntoView(center),把該 tab 捲到視圖中央\n// - Menu 內容穩定,跟 scroll 位置無關,使用者對「⌄ = navigator」的直覺一致\n//\n// 為什麼底層仍是 overflow-x-auto 而非 overflow-hidden:\n// - scrollIntoView 需要真實 scroll 容器\n// - 鍵盤 focus 也依賴真實 scroll 讓 browser auto scroll-into-view\n// - scrollbar 用 CSS 隱藏,視覺看不出來\n//\n// Fade mask 純視覺,軟化內容硬邊,跟 menu 機制正交 (兩者可並存)。\n// Menu button 出現條件: canScroll (有溢出空間才需要 navigator)。\n\n// code-quality-allow: long-function — foundational composite main body — 拆 sub-fn 會複雜化 local state / ref / context binding\nconst MenuTabsList = React.forwardRef<\n React.ElementRef<typeof TabsPrimitive.List>,\n React.ComponentPropsWithoutRef<typeof TabsPrimitive.List>\n>(({ className, children, ...props }, ref) => {\n const { scrollRef, atStart, atEnd, canScroll } = useScrollEdges<HTMLDivElement>()\n const { onValueChange, value: activeValue } = React.useContext(TabsValueContext)\n\n // Local ref map — 追蹤每個 trigger 的 DOM 元素,供 scrollIntoView 使用。\n // 不用 useOverflowIndices 因為 menu 永遠顯示全部,不需要動態 overflow 計算。\n // code-quality-allow: long-function — helper fn 結構緊密,拆 sub-fn 會跨 fn 傳 state 反而複雜\n const itemRefs = React.useRef<Map<number, HTMLElement>>(new Map())\n // 2026-05-16 audit codex Round 6:capture rAF + cancel on unmount(defensive hygiene)\n const scrollRafIdRef = React.useRef<number>(0)\n React.useEffect(() => () => { if (scrollRafIdRef.current) cancelAnimationFrame(scrollRafIdRef.current) }, [])\n const registerItem = React.useCallback(\n (index: number) => (el: HTMLElement | null) => {\n if (el) itemRefs.current.set(index, el)\n else itemRefs.current.delete(index)\n },\n []\n )\n\n const items = React.useMemo(\n () => React.Children.toArray(children).filter(React.isValidElement) as React.ReactElement[],\n [children]\n )\n\n const enhancedChildren = items.map((child, i) =>\n React.cloneElement(\n child as React.ReactElement<{ ref?: React.Ref<HTMLElement> }>,\n { ref: registerItem(i) }\n )\n )\n\n // Menu 模式沒有 arrows,但仍套 fade mask (reserveArrowWidth: 0) 軟化內容硬邊\n // code-quality-allow: long-function — helper fn 結構緊密,拆 sub-fn 會跨 fn 傳 state 反而複雜\n const maskImage = buildFadeMask({ canScroll, atStart, atEnd, reserveArrowWidth: 0 })\n\n const handleMenuSelect = React.useCallback(\n (triggerValue: string, index: number) => {\n onValueChange?.(triggerValue)\n // 下一個 tick 再 scroll, 讓 Radix 先完成 data-state 更新\n if (scrollRafIdRef.current) cancelAnimationFrame(scrollRafIdRef.current)\n scrollRafIdRef.current = requestAnimationFrame(() => {\n scrollRafIdRef.current = 0\n itemRefs.current.get(index)?.scrollIntoView({ behavior: 'smooth', inline: 'center', block: 'nearest' })\n })\n },\n [onValueChange]\n )\n\n return (\n // 2026-05-19 fix(scroll-overflow underline clip + y auto-promote,parallel to ScrollTabsList):\n // outer 改 items-stretch(menu button 容器跟 TabsList 含 border 共底線)+ 撤 border。\n // list 套 TABS_LIST_BASE,inner scroll 加 overflow-y-hidden。menu button 容器自帶\n // border-b border-divider 跟 TabsList border 同 y 對齊(items-stretch 保證)。\n <div className=\"flex items-stretch\">\n <div\n ref={scrollRef}\n className=\"flex-1 min-w-0 overflow-x-auto overflow-y-hidden [scrollbar-width:none] [&::-webkit-scrollbar]:hidden\"\n style={{ maskImage, WebkitMaskImage: maskImage }}\n >\n <TabsPrimitive.List\n ref={ref}\n className={cn(TABS_LIST_BASE, 'w-fit', className)}\n {...props}\n >\n {enhancedChildren}\n </TabsPrimitive.List>\n </div>\n {canScroll && (\n <div className=\"flex-shrink-0 pl-[var(--layout-space-loose)] flex items-center border-b border-divider\">\n <DropdownMenu>\n <DropdownMenuTrigger asChild>\n <OverflowMenuTriggerButton\n aria-label={`頁籤選單(共 ${items.length} 個)`}\n />\n </DropdownMenuTrigger>\n <DropdownMenuContent align=\"end\">\n {items.map((trigger, index) => {\n const triggerProps = trigger.props as {\n value?: string\n children?: React.ReactNode\n disabled?: boolean\n }\n const triggerValue = triggerProps.value\n if (typeof triggerValue !== 'string') return null\n // 單選 active 用 DropdownMenuItem 的 selected prop\n // → 對應 bg-neutral-selected 持續選中背景, 跟 SelectMenu 單選完全\n // 同一套 canonical 視覺, 全 DS 一致。不可用 className 發明樣式。\n return (\n <DropdownMenuItem\n key={triggerValue}\n disabled={triggerProps.disabled}\n selected={activeValue === triggerValue}\n onSelect={() => handleMenuSelect(triggerValue, index)}\n >\n {triggerProps.children}\n </DropdownMenuItem>\n )\n })}\n </DropdownMenuContent>\n </DropdownMenu>\n </div>\n )}\n </div>\n )\n})\nMenuTabsList.displayName = 'MenuTabsList'\n\n// ── Trigger ──\nconst tabsTriggerVariants = cva(\n [\n 'relative inline-flex items-center justify-center',\n 'gap-2',\n 'whitespace-nowrap',\n 'font-medium text-fg-secondary',\n 'transition-colors duration-150',\n 'cursor-pointer select-none',\n 'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-1',\n // Trigger 無水平 padding — 寬度 = 內容寬度。triggers 間的分隔靠 TabsList 的 gap-[layout-space-loose]\n // selected underline:::after 絕對定位在 bottom:-1px,2px primary-hover\n // left-0/right-0 因為 trigger 已無 padding,底線等於內容寬度\n // 底線蓋住 TabsList 的 1px gray border,視覺單一線條\n 'after:absolute after:left-0 after:right-0 after:bottom-[-1px] after:h-0.5',\n 'after:bg-transparent after:transition-colors after:duration-150',\n // hover(未選):文字轉深\n 'hover:text-foreground',\n // selected\n 'data-[state=active]:text-foreground data-[state=active]:font-medium',\n 'data-[state=active]:after:bg-primary-hover',\n // disabled:cursor-not-allowed + 不吃 hover 色\n // 不用 pointer-events-none,否則 cursor 不會改變;button[disabled] 本身就擋 click\n 'disabled:cursor-not-allowed disabled:text-fg-disabled',\n 'disabled:hover:text-fg-disabled',\n ],\n {\n variants: {\n size: {\n // leading-compact:trigger 是單行文字容器,使用 1.3 行高避免 text-body/body-lg 預設 1.5 造成垂直偏移\n sm: 'h-[var(--tab-height-sm)] text-body leading-compact',\n md: 'h-[var(--tab-height-md)] text-body leading-compact',\n lg: 'h-[var(--tab-height-lg)] text-body-lg leading-compact',\n },\n },\n defaultVariants: {\n // size = 'sm' per header-canonical.spec.md W6:overlay / chrome / dense toolbar\n // 都用 sm;獨立 page hero 用 lg。md 為 future tier(無 recommended use case)。\n // 2026-05-17 從 'md' 改 'sm'(M31 codex 比稿後)— production consumer = 0,\n // 影響面限 stories baseline(已過 visual diff gate)。\n size: 'sm',\n },\n }\n)\n\ninterface TabsTriggerProps\n extends React.ComponentPropsWithoutRef<typeof TabsPrimitive.Trigger> {\n /** 左側 icon(LucideIcon) */\n startIcon?: LucideIcon\n /** 右側 badge(通常是計數指示器) */\n badge?: React.ReactNode\n /**\n * 右側純視覺 indicator(LucideIcon)。**僅限方向 / 狀態 indicator**:ChevronDown / Pin / Star。\n * **不要拼 click 行為** — endIcon 是 tab body 的一部分,點到也是切 tab。\n * 需要「點該後綴開 dropdown / menu」場景請用 `inlineAction` slot(2026-05-21 拆分)。\n */\n endIcon?: LucideIcon\n /**\n * Inline action slot(2026-05-21 v3 加,per user「圖一 後綴應該是 inline action」+「點擊\n * 該 tab 的 inline action 跟其他地方應該不同反應」directive):\n * 提供 `<ItemInlineAction>` / `<DropdownMenuTrigger asChild><ItemInlineAction ... /></DropdownMenuTrigger>`\n * 等獨立 click target。**TabsTrigger 自動 stopPropagation**,inline action 點擊不冒泡到 tab body,\n * 達成 split-click 行為(對齊 GitHub「Code ▾」/ Linear \"Triage...\" menu / Atlassian split-tab 共識)。\n *\n * 跟 endIcon 區別:endIcon = 純視覺 indicator(無獨立行為,連同 tab 一起 click),\n * inlineAction = 獨立 click target(自己的 handler,不切 tab)。語意分家明確。\n */\n inlineAction?: React.ReactNode\n}\n\nconst TabsTrigger = React.forwardRef<\n React.ElementRef<typeof TabsPrimitive.Trigger>,\n TabsTriggerProps\n>(({ className, startIcon: StartIcon, badge, endIcon: EndIcon, inlineAction, children, ...props }, ref) => {\n const { size } = React.useContext(TabsContext)\n // 2026-05-18 改 import ICON_SIZE SSOT(per user『做完』approval,消除 M17 違反 7+ 重複 ternary)\n const iconSize = ICON_SIZE[size as 'sm' | 'md' | 'lg']\n const hasSuffix = badge != null || EndIcon !== undefined || inlineAction != null\n\n return (\n <TabsPrimitive.Trigger\n ref={ref}\n className={cn(tabsTriggerVariants({ size }), className)}\n {...props}\n >\n {StartIcon && <StartIcon size={iconSize} aria-hidden />}\n {children != null && <span>{children}</span>}\n {hasSuffix && (\n <span className=\"inline-flex items-center gap-1\">\n {badge}\n {EndIcon && <EndIcon size={iconSize} aria-hidden />}\n {inlineAction != null && (\n // 2026-05-21 split-click invariant:inlineAction 點擊不冒泡到 TabsPrimitive.Trigger。\n // Radix Tabs 在 3 個 channel 觸發 tab 切換,全部 stopPropagation:\n // - onMouseDown(primary, Radix Tabs source code main switch trigger)\n // - onFocus(activationMode='automatic' default,focus 落內部按鈕也算「focused」)\n // - onKeyDown Enter/Space(鍵盤啟動)\n // 加 onPointerDown / onClick 防禦其他 framework 慣例。\n // 對齊 GitHub「Code ▾」/ Linear \"Triage...\" split-tab 共識。\n <span\n onPointerDown={(e) => e.stopPropagation()}\n onMouseDown={(e) => e.stopPropagation()}\n onClick={(e) => e.stopPropagation()}\n onFocus={(e) => e.stopPropagation()}\n onKeyDown={(e) => {\n if (e.key === 'Enter' || e.key === ' ') e.stopPropagation()\n }}\n >\n {inlineAction}\n </span>\n )}\n </span>\n )}\n </TabsPrimitive.Trigger>\n )\n})\nTabsTrigger.displayName = 'TabsTrigger'\n\n// ── Content ──\nconst TabsContent = React.forwardRef<\n React.ElementRef<typeof TabsPrimitive.Content>,\n React.ComponentPropsWithoutRef<typeof TabsPrimitive.Content>\n>(({ className, ...props }, ref) => (\n <TabsPrimitive.Content\n ref={ref}\n className={cn(\n 'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-1',\n className\n )}\n {...props}\n />\n))\nTabsContent.displayName = 'TabsContent'\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 tabsMeta = {\n component: 'Tabs',\n family: null, // non-family composite / overlay / layout\n variants: {\n\n },\n // 注:`fieldHeight` 為 meta 通用 height key;Tabs **不複用 field-height**,值為 `--tab-height-*`(md density,對齊 spec.md size table + uiSize.css)\n sizes: {\n sm: { fieldHeight: 32, iconSize: 16, typography: 'body' },\n md: { fieldHeight: 40, iconSize: 16, typography: 'body' },\n lg: { fieldHeight: 48, iconSize: 20, typography: 'body-lg' },\n },\n states: ['default', 'hover', 'active', 'focus-visible', 'disabled'],\n tokens: {\n bg: ['bg-primary-hover', 'bg-transparent'],\n fg: ['text-fg-disabled', 'text-fg-secondary', 'text-foreground'],\n ring: ['ring-ring'],\n },\n defaultSize: 'sm',\n} as const\n\nexport { Tabs, TabsList, TabsTrigger, TabsContent, tabsTriggerVariants }\nexport type { TabsSize, TabsListProps, TabsTriggerProps }\n"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"tabs.js","sources":["../../../src/components/Tabs/tabs.tsx"],"sourcesContent":["// @benchmark-unverified-blanket: file-level retraction per M22 (d) — claims herein not individually URL-cited; treat as unverified visual/usage rumor unless retrofit per-claim. Hook escape preserved.\n// code-quality-allow: file-size — foundational composite(Tabs + overflow scroll mode + dropdown switcher + inline action slot)在單一 wrapper SSOT 內,拆分會破壞 a11y / focus management chain。當前 515 < cap 800。\nimport * as React from 'react'\nimport * as TabsPrimitive from '@radix-ui/react-tabs'\nimport { cva } from 'class-variance-authority'\nimport type { LucideIcon } from 'lucide-react'\nimport { cn } from '@/lib/utils'\nimport {\n DropdownMenu,\n DropdownMenuTrigger,\n DropdownMenuContent,\n DropdownMenuItem,\n} from '@/design-system/components/DropdownMenu/dropdown-menu'\nimport {\n useScrollEdges,\n useScrollByPage,\n buildFadeMask,\n ARROW_BUTTON_WIDTH,\n OverflowScrollArrow,\n OverflowMenuTriggerButton,\n} from '@/design-system/patterns/horizontal-overflow/horizontal-overflow'\nimport { ICON_SIZE } from '@/design-system/tokens/uiSize/icon-size'\n\n/**\n * Tabs — 基於 Radix Tabs,橋接設計系統 token\n *\n * ── 定位 ──\n * 同一上下文底下切換平行的 view。切 view(不是切 value)。\n * 切 value 用 SegmentedControl;切路由用 navigation。\n *\n * ── Size ──\n * sm h-tab-sm(32/40),★ 預設(2026-05-17 從 md 改),overlay / chrome / dense\n * md h-tab-md(40/48),future tier 無 recommended use case\n * lg h-tab-lg(48/56),page-level hero / 獨立 tabs 取代 chrome header\n *\n * ── 寬度行為 ──\n * Trigger 寬度永遠由內容決定(hug content)。\n * Triggers 之間 gap 為 --layout-space-loose(16px / 24px lg density)。\n *\n * ── Trigger 結構 ──\n * [startIcon?] [label] [suffix: badge? + endIcon?]\n * slot 間 gap-2,suffix 內 gap-1\n *\n * ── Selected underline ──\n * 使用 ::after 絕對定位在 bottom: -1px,2px primary-hover,\n * 蓋住 TabsList 的 1px gray border(單一視覺線條,不雙線)。\n */\n\n// ── Size context ──\ntype TabsSize = 'sm' | 'md' | 'lg'\ntype TabsOverflow = 'none' | 'scroll' | 'menu'\n\ninterface TabsContextValue {\n size: TabsSize\n}\nconst TabsContext = React.createContext<TabsContextValue>({ size: 'md' })\n\n// ── Root ──\n// Wrap Radix Tabs 以支援 value/onValueChange 的 context pass-through\n// (menu 模式的 overflow items 需要能 proxy click 觸發同一個 onValueChange)\ninterface TabsProps extends React.ComponentPropsWithoutRef<typeof TabsPrimitive.Root> {}\n\nconst TabsValueContext = React.createContext<{\n value?: string\n onValueChange?: (value: string) => void\n}>({})\n\nconst Tabs = React.forwardRef<\n React.ElementRef<typeof TabsPrimitive.Root>,\n TabsProps\n>(({ value, onValueChange, defaultValue, children, ...props }, ref) => {\n // 內部維護一份 uncontrolled state,讓 overflow menu 的 proxy 有 onValueChange 可呼叫\n const [internalValue, setInternalValue] = React.useState<string | undefined>(defaultValue)\n const isControlled = value !== undefined\n const currentValue = isControlled ? value : internalValue\n\n const handleValueChange = React.useCallback(\n (next: string) => {\n if (!isControlled) setInternalValue(next)\n onValueChange?.(next)\n },\n [isControlled, onValueChange]\n )\n\n const valueContext = React.useMemo(\n () => ({ value: currentValue, onValueChange: handleValueChange }),\n [currentValue, handleValueChange]\n )\n\n return (\n <TabsValueContext.Provider value={valueContext}>\n <TabsPrimitive.Root\n ref={ref}\n value={currentValue}\n onValueChange={handleValueChange}\n {...props}\n >\n {children}\n </TabsPrimitive.Root>\n </TabsValueContext.Provider>\n )\n})\nTabs.displayName = 'Tabs'\n\n// ── List ──\n// TabsList 基礎 class — inline-flex 單列 + gap-loose + 底部 border-divider\n// 2026-05-18 改 border-border → border-divider(per user verbatim「我認為應該把 tabs 的\n// 下底線統一改成是 divider 色吧?」+「做」approval):\n// - 跟 Dialog / Sheet / Popover / Sidebar header `border-b border-divider`(neutral-4)同色\n// - withTabs scenario 下 tabs underline = chrome separator,跟 dialog 其他 separator 視覺一致\n// - Selected trigger 2px primary 仍 overlay underlying divider(對比 primary >> divider 不弱)\n// - 對齊 `color.spec.md`「T-junction connectivity 原則」段 outer-vs-divider 判準(Dialog 結構,T-junction 思路適用)\nconst TABS_LIST_BASE = [\n 'inline-flex items-stretch',\n 'gap-[var(--layout-space-loose)]',\n 'border-b border-divider',\n].join(' ')\n\ninterface TabsListProps\n extends React.ComponentPropsWithoutRef<typeof TabsPrimitive.List> {\n size?: TabsSize\n /**\n * Overflow 處理模式。詳見 tabs.spec.md 的 overflow 段。\n * 'none' ★ 預設,不處理,triggers 溢出父容器(適用 tabs 數量可控的情境)\n * 'scroll' 單行橫向滾動 + 邊緣 fade mask(Material / Polaris / iOS 作法)\n * 'menu' show-all navigator——全部 triggers 一直顯示在底層 overflow-x-auto 捲動容器內,\n * (Ant Design / Atlassian 作法)\n */\n overflow?: TabsOverflow\n}\n\nconst TabsList = React.forwardRef<\n React.ElementRef<typeof TabsPrimitive.List>,\n TabsListProps\n>(({ className, size = 'sm', overflow = 'none', children, ...props }, ref) => {\n const tabsSizeContext = React.useMemo(() => ({ size }), [size])\n if (overflow === 'scroll') {\n return (\n <TabsContext.Provider value={tabsSizeContext}>\n <ScrollTabsList ref={ref} className={className} {...props}>\n {children}\n </ScrollTabsList>\n </TabsContext.Provider>\n )\n }\n if (overflow === 'menu') {\n return (\n <TabsContext.Provider value={tabsSizeContext}>\n <MenuTabsList ref={ref} className={className} {...props}>\n {children}\n </MenuTabsList>\n </TabsContext.Provider>\n )\n }\n // none(預設)\n return (\n <TabsContext.Provider value={tabsSizeContext}>\n <TabsPrimitive.List\n ref={ref}\n className={cn(TABS_LIST_BASE, 'w-full', className)}\n {...props}\n >\n {children}\n </TabsPrimitive.List>\n </TabsContext.Provider>\n )\n})\nTabsList.displayName = 'TabsList'\n\n// ── Scroll mode ──\n//\n// 共同策略(對齊 Material 3 / Ant Design / Primer UnderlineNav 世界級作法):\n// - 容器 overflow-x-auto + overflow-y-hidden(真的可滾,鍵盤焦點可 scroll-into-view;\n// 明示 y-hidden 阻 CSS overflow-3 spec「一軸 auto 時另軸 visible compute auto」)\n// - border-b 在 TabsList 內部(TABS_LIST_BASE),不在 outer wrapper,避免\n// active underline `after:bottom:-1px` 跟 outer + overflow-x-auto 觸發 y promote bug\n// - mask / arrow / fade 全部從 horizontal-overflow pattern module 取得——\n// 參見 `patterns/horizontal-overflow/horizontal-overflow.spec.md`\n// - Menu 模式 = scroll 模式 + 額外的 ⌄ quick-jump button,點 menu item 同時\n// 觸發 onValueChange + scrollIntoView,讓使用者在 tab strip 看到選中的 tab\n// 單行水平滾動 + 邊緣 fade mask 延伸到 arrow button 底下 + 左右 always-visible arrows\n// - 鍵盤: Radix 原生方向鍵 + 瀏覽器 scroll-into-view\n// - Trackpad: 兩指橫向滑動\n// - 滑鼠滾輪: 點 arrow buttons (Shift+wheel 太隱晦)\n\nconst ScrollTabsList = React.forwardRef<\n React.ElementRef<typeof TabsPrimitive.List>,\n React.ComponentPropsWithoutRef<typeof TabsPrimitive.List>\n>(({ className, children, ...props }, ref) => {\n const { scrollRef, atStart, atEnd, canScroll } = useScrollEdges<HTMLDivElement>()\n const scrollByPage = useScrollByPage(scrollRef)\n const maskImage = buildFadeMask({\n canScroll,\n atStart,\n atEnd,\n reserveArrowWidth: ARROW_BUTTON_WIDTH,\n })\n\n return (\n // 2026-05-19 fix(scroll-overflow underline clip + y auto-promote):\n // outer 撤 `border-b` → owner 升到 `TabsList` (TABS_LIST_BASE 含 border-b border-divider)\n // 把 trigger `after:bottom-[-1px]` 2px underline 的下半部 1px 收進 list border-box,\n // 再加 `overflow-y-hidden` 明示阻 browser y auto-promote(CSS overflow-3 spec:\n // overflow-x:auto + overflow-y:visible 必 compute auto)。\n // 不加 `pb-px`(outer border 撤後 list border 已接 -1px 部分,加 pb 多 1px 多餘空白)。\n // 對齊 Primer UnderlineNav `overflow-x:auto; overflow-y:hidden` canonical 同步動\n // horizontal-overflow.spec.md(「Hook re-export」+「典型 scroll / menu 模式組裝」段)owner 升 list 內部。\n <div className=\"relative\">\n <div\n ref={scrollRef}\n className=\"overflow-x-auto overflow-y-hidden [scrollbar-width:none] [&::-webkit-scrollbar]:hidden\"\n style={{ maskImage, WebkitMaskImage: maskImage }}\n >\n <TabsPrimitive.List\n ref={ref}\n className={cn(TABS_LIST_BASE, 'min-w-full', className)}\n {...props}\n >\n {children}\n </TabsPrimitive.List>\n </div>\n {!atStart && canScroll && (\n <OverflowScrollArrow direction=\"left\" onClick={() => scrollByPage('left')} />\n )}\n {!atEnd && canScroll && (\n <OverflowScrollArrow direction=\"right\" onClick={() => scrollByPage('right')} />\n )}\n </div>\n )\n})\nScrollTabsList.displayName = 'ScrollTabsList'\n\n// ── Menu mode ──\n// Show-all navigator pattern (Chrome tab dropdown / VS Code editor tabs / Discord channel jumper):\n// - Menu 永遠顯示全部 tabs,active 的用 checked 標記 (單選語意)\n// - 點 menu item = onValueChange + scrollIntoView(center),把該 tab 捲到視圖中央\n// - Menu 內容穩定,跟 scroll 位置無關,使用者對「⌄ = navigator」的直覺一致\n//\n// 為什麼底層仍是 overflow-x-auto 而非 overflow-hidden:\n// - scrollIntoView 需要真實 scroll 容器\n// - 鍵盤 focus 也依賴真實 scroll 讓 browser auto scroll-into-view\n// - scrollbar 用 CSS 隱藏,視覺看不出來\n//\n// Fade mask 純視覺,軟化內容硬邊,跟 menu 機制正交 (兩者可並存)。\n// Menu button 出現條件: canScroll (有溢出空間才需要 navigator)。\n\n// code-quality-allow: long-function — foundational composite main body — 拆 sub-fn 會複雜化 local state / ref / context binding\nconst MenuTabsList = React.forwardRef<\n React.ElementRef<typeof TabsPrimitive.List>,\n React.ComponentPropsWithoutRef<typeof TabsPrimitive.List>\n>(({ className, children, ...props }, ref) => {\n const { scrollRef, atStart, atEnd, canScroll } = useScrollEdges<HTMLDivElement>()\n const { onValueChange, value: activeValue } = React.useContext(TabsValueContext)\n\n // Local ref map — 追蹤每個 trigger 的 DOM 元素,供 scrollIntoView 使用。\n // 不用 useOverflowIndices 因為 menu 永遠顯示全部,不需要動態 overflow 計算。\n // code-quality-allow: long-function — helper fn 結構緊密,拆 sub-fn 會跨 fn 傳 state 反而複雜\n const itemRefs = React.useRef<Map<number, HTMLElement>>(new Map())\n // 2026-05-16 audit codex Round 6:capture rAF + cancel on unmount(defensive hygiene)\n const scrollRafIdRef = React.useRef<number>(0)\n React.useEffect(() => () => { if (scrollRafIdRef.current) cancelAnimationFrame(scrollRafIdRef.current) }, [])\n const registerItem = React.useCallback(\n (index: number) => (el: HTMLElement | null) => {\n if (el) itemRefs.current.set(index, el)\n else itemRefs.current.delete(index)\n },\n []\n )\n\n const items = React.useMemo(\n () => React.Children.toArray(children).filter(React.isValidElement) as React.ReactElement[],\n [children]\n )\n\n const enhancedChildren = items.map((child, i) =>\n React.cloneElement(\n child as React.ReactElement<{ ref?: React.Ref<HTMLElement> }>,\n { ref: registerItem(i) }\n )\n )\n\n // Menu 模式沒有 arrows,但仍套 fade mask (reserveArrowWidth: 0) 軟化內容硬邊\n // code-quality-allow: long-function — helper fn 結構緊密,拆 sub-fn 會跨 fn 傳 state 反而複雜\n const maskImage = buildFadeMask({ canScroll, atStart, atEnd, reserveArrowWidth: 0 })\n\n const handleMenuSelect = React.useCallback(\n (triggerValue: string, index: number) => {\n onValueChange?.(triggerValue)\n // 下一個 tick 再 scroll, 讓 Radix 先完成 data-state 更新\n if (scrollRafIdRef.current) cancelAnimationFrame(scrollRafIdRef.current)\n scrollRafIdRef.current = requestAnimationFrame(() => {\n scrollRafIdRef.current = 0\n itemRefs.current.get(index)?.scrollIntoView({ behavior: 'smooth', inline: 'center', block: 'nearest' })\n })\n },\n [onValueChange]\n )\n\n return (\n // 2026-05-19 fix(scroll-overflow underline clip + y auto-promote,parallel to ScrollTabsList):\n // outer 改 items-stretch(menu button 容器跟 TabsList 含 border 共底線)+ 撤 border。\n // list 套 TABS_LIST_BASE,inner scroll 加 overflow-y-hidden。menu button 容器自帶\n // border-b border-divider 跟 TabsList border 同 y 對齊(items-stretch 保證)。\n <div className=\"flex items-stretch\">\n <div\n ref={scrollRef}\n className=\"flex-1 min-w-0 overflow-x-auto overflow-y-hidden [scrollbar-width:none] [&::-webkit-scrollbar]:hidden\"\n style={{ maskImage, WebkitMaskImage: maskImage }}\n >\n <TabsPrimitive.List\n ref={ref}\n className={cn(TABS_LIST_BASE, 'min-w-full', className)}\n {...props}\n >\n {enhancedChildren}\n </TabsPrimitive.List>\n </div>\n {canScroll && (\n <div className=\"flex-shrink-0 pl-[var(--layout-space-loose)] flex items-center border-b border-divider\">\n <DropdownMenu>\n <DropdownMenuTrigger asChild>\n <OverflowMenuTriggerButton\n aria-label={`頁籤選單(共 ${items.length} 個)`}\n />\n </DropdownMenuTrigger>\n <DropdownMenuContent align=\"end\">\n {items.map((trigger, index) => {\n const triggerProps = trigger.props as {\n value?: string\n children?: React.ReactNode\n disabled?: boolean\n }\n const triggerValue = triggerProps.value\n if (typeof triggerValue !== 'string') return null\n // 單選 active 用 DropdownMenuItem 的 selected prop\n // → 對應 bg-neutral-selected 持續選中背景, 跟 SelectMenu 單選完全\n // 同一套 canonical 視覺, 全 DS 一致。不可用 className 發明樣式。\n return (\n <DropdownMenuItem\n key={triggerValue}\n disabled={triggerProps.disabled}\n selected={activeValue === triggerValue}\n onSelect={() => handleMenuSelect(triggerValue, index)}\n >\n {triggerProps.children}\n </DropdownMenuItem>\n )\n })}\n </DropdownMenuContent>\n </DropdownMenu>\n </div>\n )}\n </div>\n )\n})\nMenuTabsList.displayName = 'MenuTabsList'\n\n// ── Trigger ──\nconst tabsTriggerVariants = cva(\n [\n 'relative inline-flex items-center justify-center',\n 'gap-2',\n 'whitespace-nowrap',\n 'font-medium text-fg-secondary',\n 'transition-colors duration-150',\n 'cursor-pointer select-none',\n 'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-1',\n // Trigger 無水平 padding — 寬度 = 內容寬度。triggers 間的分隔靠 TabsList 的 gap-[layout-space-loose]\n // selected underline:::after 絕對定位在 bottom:-1px,2px primary-hover\n // left-0/right-0 因為 trigger 已無 padding,底線等於內容寬度\n // 底線蓋住 TabsList 的 1px gray border,視覺單一線條\n 'after:absolute after:left-0 after:right-0 after:bottom-[-1px] after:h-0.5',\n 'after:bg-transparent after:transition-colors after:duration-150',\n // hover(未選):文字轉深\n 'hover:text-foreground',\n // selected\n 'data-[state=active]:text-foreground data-[state=active]:font-medium',\n 'data-[state=active]:after:bg-primary-hover',\n // disabled:cursor-not-allowed + 不吃 hover 色\n // 不用 pointer-events-none,否則 cursor 不會改變;button[disabled] 本身就擋 click\n 'disabled:cursor-not-allowed disabled:text-fg-disabled',\n 'disabled:hover:text-fg-disabled',\n ],\n {\n variants: {\n size: {\n // leading-compact:trigger 是單行文字容器,使用 1.3 行高避免 text-body/body-lg 預設 1.5 造成垂直偏移\n sm: 'h-[var(--tab-height-sm)] text-body leading-compact',\n md: 'h-[var(--tab-height-md)] text-body leading-compact',\n lg: 'h-[var(--tab-height-lg)] text-body-lg leading-compact',\n },\n },\n defaultVariants: {\n // size = 'sm' per header-canonical.spec.md W6:overlay / chrome / dense toolbar\n // 都用 sm;獨立 page hero 用 lg。md 為 future tier(無 recommended use case)。\n // 2026-05-17 從 'md' 改 'sm'(M31 codex 比稿後)— production consumer = 0,\n // 影響面限 stories baseline(已過 visual diff gate)。\n size: 'sm',\n },\n }\n)\n\ninterface TabsTriggerProps\n extends React.ComponentPropsWithoutRef<typeof TabsPrimitive.Trigger> {\n /** 左側 icon(LucideIcon) */\n startIcon?: LucideIcon\n /** 右側 badge(通常是計數指示器) */\n badge?: React.ReactNode\n /**\n * 右側純視覺 indicator(LucideIcon)。**僅限方向 / 狀態 indicator**:ChevronDown / Pin / Star。\n * **不要拼 click 行為** — endIcon 是 tab body 的一部分,點到也是切 tab。\n * 需要「點該後綴開 dropdown / menu」場景請用 `inlineAction` slot(2026-05-21 拆分)。\n */\n endIcon?: LucideIcon\n /**\n * Inline action slot(2026-05-21 v3 加,per user「圖一 後綴應該是 inline action」+「點擊\n * 該 tab 的 inline action 跟其他地方應該不同反應」directive):\n * 提供 `<ItemInlineAction>` / `<DropdownMenuTrigger asChild><ItemInlineAction ... /></DropdownMenuTrigger>`\n * 等獨立 click target。**TabsTrigger 自動 stopPropagation**,inline action 點擊不冒泡到 tab body,\n * 達成 split-click 行為(對齊 GitHub「Code ▾」/ Linear \"Triage...\" menu / Atlassian split-tab 共識)。\n *\n * 跟 endIcon 區別:endIcon = 純視覺 indicator(無獨立行為,連同 tab 一起 click),\n * inlineAction = 獨立 click target(自己的 handler,不切 tab)。語意分家明確。\n */\n inlineAction?: React.ReactNode\n}\n\nconst TabsTrigger = React.forwardRef<\n React.ElementRef<typeof TabsPrimitive.Trigger>,\n TabsTriggerProps\n>(({ className, startIcon: StartIcon, badge, endIcon: EndIcon, inlineAction, children, ...props }, ref) => {\n const { size } = React.useContext(TabsContext)\n // 2026-05-18 改 import ICON_SIZE SSOT(per user『做完』approval,消除 M17 違反 7+ 重複 ternary)\n const iconSize = ICON_SIZE[size as 'sm' | 'md' | 'lg']\n const hasSuffix = badge != null || EndIcon !== undefined || inlineAction != null\n\n return (\n <TabsPrimitive.Trigger\n ref={ref}\n className={cn(tabsTriggerVariants({ size }), className)}\n {...props}\n >\n {StartIcon && <StartIcon size={iconSize} aria-hidden />}\n {children != null && <span>{children}</span>}\n {hasSuffix && (\n <span className=\"inline-flex items-center gap-1\">\n {badge}\n {EndIcon && <EndIcon size={iconSize} aria-hidden />}\n {inlineAction != null && (\n // 2026-05-21 split-click invariant:inlineAction 點擊不冒泡到 TabsPrimitive.Trigger。\n // Radix Tabs 在 3 個 channel 觸發 tab 切換,全部 stopPropagation:\n // - onMouseDown(primary, Radix Tabs source code main switch trigger)\n // - onFocus(activationMode='automatic' default,focus 落內部按鈕也算「focused」)\n // - onKeyDown Enter/Space(鍵盤啟動)\n // 加 onPointerDown / onClick 防禦其他 framework 慣例。\n // 對齊 GitHub「Code ▾」/ Linear \"Triage...\" split-tab 共識。\n <span\n onPointerDown={(e) => e.stopPropagation()}\n onMouseDown={(e) => e.stopPropagation()}\n onClick={(e) => e.stopPropagation()}\n onFocus={(e) => e.stopPropagation()}\n onKeyDown={(e) => {\n if (e.key === 'Enter' || e.key === ' ') e.stopPropagation()\n }}\n >\n {inlineAction}\n </span>\n )}\n </span>\n )}\n </TabsPrimitive.Trigger>\n )\n})\nTabsTrigger.displayName = 'TabsTrigger'\n\n// ── Content ──\nconst TabsContent = React.forwardRef<\n React.ElementRef<typeof TabsPrimitive.Content>,\n React.ComponentPropsWithoutRef<typeof TabsPrimitive.Content>\n>(({ className, ...props }, ref) => (\n <TabsPrimitive.Content\n ref={ref}\n className={cn(\n // 與 TabsList 的間距 canonical(2026-06-12 user 拍板):--layout-space-tight(md 12px,\n // density 連動)。依 layoutSpace.spec「親疏 3 級」:Tabs↔Content 同 bundle(第一級,\n // 元件 spec own),值取規則 3「直接功能依賴 = tight」精神(heading → labeled content 同類)。\n // 收斂原 DS-wide 四種土法(無間距 / mt-4 / p-4 / pt-4 — M17 假 SSOT)。\n // full-height 佈局(AppShell pane)用 className=\"mt-0\" 覆寫(tailwind-merge)。\n 'mt-[var(--layout-space-tight)]',\n 'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-1',\n className\n )}\n {...props}\n />\n))\nTabsContent.displayName = 'TabsContent'\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 tabsMeta = {\n component: 'Tabs',\n family: null, // non-family composite / overlay / layout\n variants: {\n\n },\n // 注:`fieldHeight` 為 meta 通用 height key;Tabs **不複用 field-height**,值為 `--tab-height-*`(md density,對齊 spec.md size table + uiSize.css)\n sizes: {\n sm: { fieldHeight: 32, iconSize: 16, typography: 'body' },\n md: { fieldHeight: 40, iconSize: 16, typography: 'body' },\n lg: { fieldHeight: 48, iconSize: 20, typography: 'body-lg' },\n },\n states: ['default', 'hover', 'active', 'focus-visible', 'disabled'],\n tokens: {\n bg: ['bg-primary-hover', 'bg-transparent'],\n fg: ['text-fg-disabled', 'text-fg-secondary', 'text-foreground'],\n ring: ['ring-ring'],\n },\n defaultSize: 'sm',\n} as const\n\nexport { Tabs, TabsList, TabsTrigger, TabsContent, tabsTriggerVariants }\nexport type { TabsSize, TabsListProps, TabsTriggerProps }\n"],"names":[],"mappings":"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"}
@@ -218,6 +218,15 @@ if echo "$CONTENT" | grep -qE '\b[a-z][a-z-]*-\[(#[0-9a-fA-F]{3,8}|rgb|rgba|hsl|
218
218
  VIOLATIONS="${VIOLATIONS} - 硬寫色值/字級/shadow 繞過 DS token(bg-[#hex] / text-[14px] / shadow-md)→ 改 semantic color token / text-body 等 typography token / shadow-[var(--elevation-N)](per ui-development.md「Tailwind 5 條核心」rule 3)\n"
219
219
  fi
220
220
 
221
+ # Pattern 9(2026-06-12,user 抓 fork「四不像」G4 補洞):AppShell slot 餵 raw HTML element。
222
+ # app-shell.spec.md:296-299 明文禁 sidebar={<div>}/header={<header>},原註「靠 audit 把關」
223
+ # 無機械閘 → 兌現成 P0(per memory feedback_ssot_mechanical_p0_not_p1_warn)。
224
+ # 零誤判:雙條件 = 同檔有 <DS.AppShell> + slot 屬性直接餵 raw tag(div/header/nav/aside/section)。
225
+ if echo "$CONTENT" | grep -qE '<DS\.AppShell\b' && \
226
+ echo "$CONTENT" | grep -qE '\b(header|sidebar|aside)=\{ *<(div|header|nav|aside|section)\b'; then
227
+ VIOLATIONS="${VIOLATIONS} - <AppShell header/sidebar/aside={<raw element>}> — slot 必餵 DS 元件(ChromeHeader / Sidebar / AppShellAside),raw div/header 漏接 border/scroll/responsive canonical(per app-shell.spec.md:296-299)\n"
228
+ fi
229
+
221
230
  # Pattern 6: Overlay trigger without defaultOpen state for visual demo
222
231
  # (Skip in production .tsx; only enforce in .stories.tsx where visual snapshot matters)
223
232
  if echo "$FILE" | grep -qE '\.stories\.tsx$'; then
@@ -12,7 +12,12 @@
12
12
  "components": {
13
13
  "Sidebar": {
14
14
  "baseline": "packages/design-system/src/components/Sidebar/sidebar.stories.tsx#IconCollapse",
15
- "requiredHelpers": ["WorkspaceBrand", "MAIN_NAV", "SidebarHeader", "SidebarFooter"],
15
+ "requiredHelpers": [
16
+ "WorkspaceBrand",
17
+ "MAIN_NAV",
18
+ "SidebarHeader",
19
+ "SidebarFooter"
20
+ ],
16
21
  "antiPatterns": [
17
22
  {
18
23
  "id": "sidebar-header-simplified-span",
@@ -25,13 +30,24 @@
25
30
  "regex": "<SidebarMenuButton[^>]*>[^<]*<[A-Z][a-zA-Z]+[[:space:]]+className=\"size-",
26
31
  "severity": "block",
27
32
  "rationale": "SidebarMenuButton 必用 startIcon prop + tooltip prop;children inline icon = drift"
33
+ },
34
+ {
35
+ "id": "sidebar-header-subtitle-second-line",
36
+ "regex": "<SidebarHeader[^>]*>[[:space:]]*<div[^>]*flex-col",
37
+ "severity": "block",
38
+ "rationale": "SidebarHeader brand 必單行(Fixed-h chrome 不可成長,sidebar.spec.md SidebarHeader 段 2026-06-12 明文)。flex-col 直接子層 = logo name 下加副標的結構簽名 — shadcn TeamSwitcher demo prior 滲漏;plan/org 資訊歸 workspace switcher dropdown row,非常駐 header"
28
39
  }
29
40
  ]
30
41
  },
31
42
  "DataTable": {
32
43
  "baseline": "packages/design-system/src/components/DataTable/data-table.stories.tsx#WithBulkActions",
33
44
  "toolbar": {
34
- "requiredHelpers": ["Popover", "PopoverTrigger", "PopoverContent", "Input"],
45
+ "requiredHelpers": [
46
+ "Popover",
47
+ "PopoverTrigger",
48
+ "PopoverContent",
49
+ "Input"
50
+ ],
35
51
  "filterPanel": "DataTableFilterPanel",
36
52
  "sortManager": "DataTableSortManager",
37
53
  "buttonCanonical": {
@@ -58,3 +58,24 @@ Icon 尺寸按 context 分三類:
58
58
  | **一次性 / 非 row / 非 Button**(chrome / decorative / toolbar)| inline `size={n}`,**n 必對齊 uiSize token**(16/20/24,不自創)| `<FileIcon size={16} />` |
59
59
 
60
60
  **禁止**:Tailwind `w-4 h-4` / `size-4` 表達 icon size(是 dimension 非 semantic)/ Row 內手刻 `<Icon size={16} />` 繞過 Context(density 切換不聯動)/ 自創非 uiSize 值(`size={18}` / `size={22}`)。
61
+
62
+ ---
63
+
64
+ ## 小尺寸 icon stroke 補償 canonical(SSOT,2026-06-12 codify)
65
+
66
+ **機制(3 層,2026-06-12 修正後)**:lucide SVG 等比縮放(stroke 隨尺寸自動變細)→ ① DS 全域預設 = **1.75**(`styles/base.css` `.lucide[stroke-width='2']`,只覆寫**沒有** explicit prop 的 icon;比 lucide 原廠 2 輕一階的整體風格)② component 顯式 `strokeWidth` prop → attribute 直接生效(補償用此)③ 本 DS **不用 `absoluteStrokeWidth`**(那是鎖絕對 px;我們要的是「小尺寸**相對**更粗」)。
67
+ **⚠️ 失敗 anchor(2026-06-12,user 視覺抓到)**:base.css 原寫 `.lucide { stroke-width: 1.75 }` 無條件 → CSS class 蓋過 SVG attribute → 全 DS strokeWidth prop 自 2026-04-08 起**從未真渲染**(Checkbox 勾實畫 0.80px 而非設計的 1.50px;2026-05-18「3.5 vs 3 看不出差別」的視覺測試兩者其實都是 0.875px = 證據已被污染)。M2/M32 教訓:icon 粗細宣稱必驗 rendered DOM computed style + pixel,不可信 prop。
68
+
69
+ **三個 bucket**(邊界判準 = 「控件內的裸線條 state glyph」才補償):
70
+
71
+ | Bucket | 成員 | 規則 | 實畫 |
72
+ |------|------|------|------|
73
+ | **控件 state glyph**(裸 Check / X / Minus,線條稀疏,縮小後視覺權重不足)| Checkbox check/minus、Switch thumb check、Steps indicator、PeoplePicker avatar dismiss X | **12px → strokeWidth 3;16-20px → 2.5** | 1.5 / 1.67-2.08 |
74
+ | **其他所有 icon**(Button startIcon、menu/chrome/toolbar icon、status icon 如 CircleCheck/Info/TriangleAlert — 自帶外框或與文字並排,跟文字 weight-matched)| Notice(Alert 家族)、FileItem status、ProgressBar status affix、Button、MenuItem…(DS 99% icon)| **不傳 prop → 全域 1.75** | 1.17 @16px / 1.46 @20px / 1.75 @24px |
75
+ | **大型 illustrative**(≥32px)| Coachmark | 顯式調細 1.75(= 跟全域同值,語意上是「放大不增重」)| 2.33 @32px |
76
+
77
+ fill-only shape(Rating Star `stroke="none"`、Radio dot `fill-current`)無此規則。
78
+
79
+ **世界級對照**:SF Symbols 小 scale 非線性縮、stroke 隨文字 weight 補償(WWDC19 #206「it's not just linearly-scaled, the stroke thickness is adjusted」);Material Symbols opsz 軸自動小尺寸相對加粗(developers.google.com/fonts/docs/material_symbols「For the image to look the same at different sizes, the stroke weight changes as the icon size scales」);Carbon 線性縮但 16px 專屬重繪 override 集中在 checkmark/chevron 類 = 同樣只特調小 state glyph(github.com/carbon-design-system/carbon icons src 實測 68 個 16px override)。
80
+
81
+ **值的 SSOT**:`checkbox.tsx` `checkStrokeWidth {sm:3, md:3, lg:2.5}`(2026-05-18 user 視覺證拍板);Switch `SPECS.checkStroke` / Steps `strokeWidth={2.5}` / PeoplePicker dismiss X 對齊此表。改值先改 checkbox.tsx 再同步全家族(M17)。
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.64
3
+ > 全 component / pattern 的 variants / sizes / 禁止事項。build-time 從 spec.md frontmatter 生成,禁手改。v0.1.0-beta.65
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.64
4
+ > 54 components + 4 public patterns + design tokens。v0.1.0-beta.65
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.64",
3
+ "version": "0.1.0-beta.65",
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",
@@ -112,7 +112,7 @@ Consumer 無需額外處理,保留 Radix `data-state` 屬性即可。
112
112
 
113
113
  ## 邊界狀態
114
114
 
115
- Empty state 由 consumer 處理(無 items 則不渲染);loading 狀態由 consumer 用 `<Skeleton />` 包;disabled state 詳 `../Field/field-controls.spec.md`;本元件無 density 概念,padding 固定為 `py-4 / pb-4`(不隨 density token 變動);item 數量無內建上限與虛擬捲動(全數 render),極長清單的收納(拆頁 / 搜尋)由 consumer 內容層處理。
115
+ Empty state 由 consumer 處理(無 items 則不渲染);loading 狀態由 consumer 用 `<Skeleton />` 包;disabled state 詳 `../Field/field-controls.spec.md`;本元件無 density 概念,padding 固定為 `py-4 / pb-4`(不隨 density token 變動)。**垂直留白世界級對照(2026-06-12 source-verified)**:shadcn trigger `py-4`=16px(本 DS 基底,一字不差)/ Ant Collapse header 垂直 = paddingSM 12px / MUI Accordion = minHeight 48 + content margin 12px(expanded 64/20 漸進式)/ Carbon = min-height token 模型——業界垂直節奏值域 12-16px,16px 為舒適端上緣;item 數量無內建上限與虛擬捲動(全數 render),極長清單的收納(拆頁 / 搜尋)由 consumer 內容層處理。
116
116
 
117
117
  ---
118
118
 
@@ -334,7 +334,7 @@ export const PrimarySidebarWithTabs: Story = {
334
334
  asideOpen={asideOpen}
335
335
  onAsideOpenChange={setAsideOpen}
336
336
  >
337
- <TabsContent value="all" className="flex-1 min-h-0 flex flex-col">
337
+ <TabsContent value="all" className="mt-0 flex-1 min-h-0 flex flex-col">
338
338
  <IssuesView
339
339
  selectedId={selected?.id}
340
340
  asideOpen={asideOpen}
@@ -344,7 +344,7 @@ export const PrimarySidebarWithTabs: Story = {
344
344
  }}
345
345
  />
346
346
  </TabsContent>
347
- <TabsContent value="open" className="flex-1 min-h-0 flex flex-col">
347
+ <TabsContent value="open" className="mt-0 flex-1 min-h-0 flex flex-col">
348
348
  <IssuesView
349
349
  selectedId={selected?.id}
350
350
  asideOpen={asideOpen}
@@ -354,7 +354,7 @@ export const PrimarySidebarWithTabs: Story = {
354
354
  }}
355
355
  />
356
356
  </TabsContent>
357
- <TabsContent value="done" className="flex-1 min-h-0 flex flex-col">
357
+ <TabsContent value="done" className="mt-0 flex-1 min-h-0 flex flex-col">
358
358
  <IssuesView
359
359
  selectedId={selected?.id}
360
360
  asideOpen={asideOpen}
@@ -167,13 +167,13 @@ export const UsageGuidance: Story = {
167
167
  <TabsTrigger value="members">成員</TabsTrigger>
168
168
  <TabsTrigger value="settings">設定</TabsTrigger>
169
169
  </TabsList>
170
- <TabsContent value="overview" className="pt-4">
170
+ <TabsContent value="overview" className="">
171
171
  <p className="text-body">專案狀態、最近活動、關鍵指標</p>
172
172
  </TabsContent>
173
- <TabsContent value="members" className="pt-4">
173
+ <TabsContent value="members" className="">
174
174
  <p className="text-body">團隊成員列表、角色權限</p>
175
175
  </TabsContent>
176
- <TabsContent value="settings" className="pt-4">
176
+ <TabsContent value="settings" className="">
177
177
  <p className="text-body">通知、整合、危險區</p>
178
178
  </TabsContent>
179
179
  </Tabs>
@@ -249,7 +249,15 @@ Indeterminate 是由父層邏輯控制的狀態,Checkbox 本身不會自動進
249
249
 
250
250
  - **Uncontrolled**:只傳 `defaultChecked`,DOM 自管 — 適合表單 native submit
251
251
  - **Controlled**:傳 `checked` + `onCheckedChange`,React state 主導 — 適合需即時聯動其他欄位
252
- - **Read-only**:用 `readOnly` prop(不是省略 `onCheckedChange`)。`readOnly` 設 `aria-readonly` / `data-readonly` / `tabIndex=-1` + cva `pointer-events-none`,鎖互動但保留 checked 視覺。注意:只傳 `checked` 而不傳 `onCheckedChange` 僅讓 Radix 把值鎖在 prop(不會更新),控件仍可 focus / click、不會進 disabled state — 那不是 readonly
252
+ - **Read-only**:用 `readOnly` prop(不是省略 `onCheckedChange`)。standalone:`readOnly` 設 `aria-readonly` / `data-readonly` / `tabIndex=-1` + cva `pointer-events-none`,鎖互動但保留 checked 視覺;**Field 內(無 inline label)改渲染 readonly 灰框 + ✓/—**(見上方 mode 段)。注意:只傳 `checked` 而不傳 `onCheckedChange` 僅讓 Radix 把值鎖在 prop(不會更新),控件仍可 focus / click、不會進 disabled state — 那不是 readonly
253
+
254
+ ### `mode` prop(Field mode,正交於 size)
255
+
256
+ `mode?: 'edit' | 'display' | 'readonly' | 'disabled'`(默認 inherit Field context 或 `'edit'`),對齊 `field-types.ts` FieldMode(完整 4-mode canonical SSOT → `Field/field-controls.spec.md`;本段鏡像 switch.spec.md 同名段):
257
+ - `edit`(預設)— 可勾選的 Checkbox。
258
+ - `display` — **純展示**:渲染 ✓(checked)/ —(其他),非互動、無 input chrome,供 DataTable boolean cell 非編輯態共用。
259
+ - `readonly` — **Field 內(無 inline label)**= `fieldWrapperStyles` readonly 灰框 + ✓/—(與 Input readonly 同一視覺語言,2026-06-12 user 拍板;世界級:Salesforce output ✓ glyph / SAP 靜態文字);**standalone / 有 inline label**(SelectionItem row)= 正常色鎖互動(同下方 Read-only)。
260
+ - `disabled` — 落到真 disabled chrome(`effectiveDisabled`,2026-06-12 修:mode='disabled' 直傳〔如 DataTable disabled cell〕與 `disabled` prop 等效,降色 + 不可 focus)。
253
261
 
254
262
  CheckboxGroup 是純 layout primitive — **不**持有 group-level selection state(無 `value` / `defaultValue` / `onValueChange`)。每個 `<Checkbox>` child 各自管自己的 `checked` / `defaultChecked` / `onCheckedChange`;CheckboxGroup 只透過 `CheckboxGroupContext` 告知 child「你在 group 裡」(保留各自 label)。
255
263
 
@@ -6,7 +6,8 @@ import { cva, type VariantProps } from "class-variance-authority"
6
6
 
7
7
  import { cn } from "@/lib/utils"
8
8
  import type { FieldMode, FieldVariant } from "@/design-system/components/Field/field-types"
9
- import { useFieldContext, useResolvedFieldDisabled, useResolvedFieldMode } from "@/design-system/components/Field/field-context"
9
+ import { useFieldContext, useResolvedFieldDisabled, useResolvedFieldMode, useResolvedFieldSize } from "@/design-system/components/Field/field-context"
10
+ import { fieldWrapperStyles } from "@/design-system/components/Field/field-wrapper"
10
11
  import { SelectionItem } from "@/design-system/components/SelectionControl/selection-item"
11
12
  import type { LucideIcon } from "lucide-react"
12
13
  import type { AvatarData } from "@/design-system/components/Avatar/avatar"
@@ -70,6 +71,10 @@ const checkIconSize: Record<string, number> = { sm: 12, md: 12, lg: 16 }
70
71
  // - 原 {3.5, 3.5, 2.5} → effective render thickness 1.75 / 1.75 / 1.67 = 跨 size 差 0.08px(視覺看不出)
71
72
  // - 改 {3, 3, 2.5} 保留 sm/md 小尺寸 legibility insurance(per iOS HIG / Material 3 cite)
72
73
  // + lg 仍稍粗於 Lucide default 2(保留 compensation 主旨,但不過度差異化)
74
+ // ⚠️ 2026-06-12:發現 base.css `.lucide{stroke-width:1.75}` 全域規則自 2026-04-08 起
75
+ // 無條件蓋掉本 prop(CSS class > SVG attribute)→ 本表從未真渲染過,上述 05-18 視覺
76
+ // 測試兩者實為 0.875px(證據污染)。base.css 已改 `[stroke-width='2']` 限定,本表自此
77
+ // 真實生效(pixel 驗證 1.50px)。SSOT → .claude/references/ui-dev-rules.md「小尺寸 icon stroke 補償」
73
78
  const checkStrokeWidth: Record<string, number> = { sm: 3, md: 3, lg: 2.5 }
74
79
 
75
80
  // ── Types ───────────────────────────────────────────────────────────────────
@@ -176,6 +181,12 @@ const Checkbox = React.forwardRef<
176
181
  const resolvedDisabled = useResolvedFieldDisabled(disabled)
177
182
  const resolvedMode = useResolvedFieldMode({ mode, disabled, readOnly })
178
183
  const effectiveReadOnly = readOnly || resolvedMode === 'readonly'
184
+ // mode='disabled'(如 DataTable disabled cell 直傳)必須落到真 disabled chrome —
185
+ // 2026-06-12 修:原本只看 resolvedDisabled(prop/fieldCtx),mode='disabled' 無人消費
186
+ // → disabled boolean cell 渲出可 focus 的正常外觀 checkbox(違 field-controls.spec L286)
187
+ const effectiveDisabled = resolvedDisabled || resolvedMode === 'disabled'
188
+ // readonly 灰框 size:走 SSOT resolver(prop > ctx > 'md',field-context.ts:150-161)
189
+ const resolvedBoxSize = useResolvedFieldSize(size ?? undefined, 'md') as 'sm' | 'md' | 'lg'
179
190
 
180
191
  // ── mode='display'(下移至所有 hooks 之後,per #35 Rules of Hooks)──────────
181
192
  // 純展示模式:無互動 primitive、渲染 ✓ / —(checked=true → ✓ / 其他 → —)。取代 BooleanDisplay。
@@ -186,11 +197,42 @@ const Checkbox = React.forwardRef<
186
197
  : <span className="text-fg-muted">—</span>
187
198
  }
188
199
 
200
+ // ── mode='readonly' in Field(2026-06-12 user 拍板「灰框 + ✓/—」)─────────────
201
+ // Field 內 readonly boolean = readonly 灰框 chrome + display 同款值語言 ✓/—。
202
+ // 灰框消費 fieldWrapperStyles 同一 cva = 與 Input readonly 字面同源(SSOT,改一處全動)。
203
+ // 理由:同一張 readonly 表單裡文字控件有 bg-readonly 灰框鎖定訊號,boolean 保留全彩
204
+ // 控件會誤導「仍可操作」(世界級 0/4 用原樣鎖互動:Salesforce=✓ 靜態 glyph /
205
+ // SAP=靜態文字 / Atlassian=readView / Ant Pro=文字)。
206
+ // Scope:僅 Field 內且無 inline label(FieldLabel 接管 label 的表單欄位場景);
207
+ // standalone readOnly(settings list / SelectionItem row)維持原樣鎖互動不變。
208
+ if (effectiveReadOnly && insideField && effectiveLabel == null) {
209
+ const isChecked = (props.checked ?? props.defaultChecked) === true
210
+ const boxSize = resolvedBoxSize
211
+ return (
212
+ <div
213
+ role="checkbox"
214
+ aria-checked={isChecked}
215
+ aria-readonly="true"
216
+ aria-labelledby={fieldCtx?.labelId}
217
+ aria-invalid={fieldCtx?.invalid || undefined}
218
+ data-readonly="true"
219
+ tabIndex={0}
220
+ className={cn(
221
+ fieldWrapperStyles({ size: boxSize, mode: 'readonly', variant: 'default' }),
222
+ 'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring',
223
+ className,
224
+ )}
225
+ >
226
+ {isChecked ? <span className="text-foreground">✓</span> : <span className="text-fg-muted">—</span>}
227
+ </div>
228
+ )
229
+ }
230
+
189
231
  const rootEl = (
190
232
  <CheckboxPrimitive.Root
191
233
  id={inputId}
192
234
  ref={ref}
193
- disabled={resolvedDisabled}
235
+ disabled={effectiveDisabled}
194
236
  aria-readonly={effectiveReadOnly || undefined}
195
237
  data-readonly={effectiveReadOnly || undefined}
196
238
  tabIndex={effectiveReadOnly ? -1 : undefined}
@@ -219,7 +261,7 @@ const Checkbox = React.forwardRef<
219
261
  icon={icon}
220
262
  avatar={avatar}
221
263
  htmlFor={inputId}
222
- disabled={resolvedDisabled}
264
+ disabled={effectiveDisabled}
223
265
  size={sizeKey}
224
266
  />
225
267
  )
@@ -68,6 +68,8 @@ components/
68
68
 
69
69
  三種模式共用同一個 wrapper 結構(`fieldWrapperStyles`),只有底色、邊框、文字色不同。
70
70
 
71
+ **Boolean / 單選控件的 readonly(2026-06-12 user 拍板)**:Field 內 readonly 的 Checkbox / Switch = 同一 `fieldWrapperStyles` readonly 灰框 + ✓/—(display 同款值語言);RadioGroup = 灰框 + 選中項 label(= Select readonly 同款呈現)。理由:同一張 readonly 表單中,文字控件有灰框鎖定訊號、boolean 保留全彩控件會誤導「仍可操作」(世界級 0/4 採原樣鎖互動:Salesforce = ✓ 無框靜態 glyph / SAP = 靜態文字 / Atlassian = readView / Ant Pro = 文字)。standalone readOnly(settings list / SelectionItem row)維持原樣鎖互動。**邊界**:Rating readonly = 星星本身(星星即值語言,role=img,全業界 review-stars canonical,不包灰框);Slider 在 `<Field mode="readonly">` 內 = 鎖互動保留正常視覺(value 可讀不降色,pointer-events-none + thumb tabIndex=-1)。
72
+
71
73
  ### Loading state(async 驗證 / debounce fetch 中)
72
74
 
73
75
  Loading **不是第四個 mode**,是 `edit` mode 的子狀態,語義 = **editable 仍可輸入**(UX「邊改邊讀」:debounce search / async validation 場景中 user 常需要繼續打字修正,凍結輸入反而破壞心流)。
@@ -161,7 +163,7 @@ Form wrapper 可透過 context 注入 `error` prop,消費者不需要在每個
161
163
  - **欄位內的展示元素**(Avatar)→ 跟隨 `<Field disabled>` / `<Field mode="disabled">` **變淡**(視覺一致),用 fieldCtx 存在性 scope(DataTable cell 無 fieldCtx → 不影響)。
162
164
  - **獨立 action 元件**(Button)→ **不**自動 cascade;由 consumer 自控 `disabled`(對齊 MUI Button 無 FormControl 整合 + Ant 排除 custom/非表單控件)。
163
165
 
164
- 注:有 display 渲染分支者(Input 家族 / Select / Combobox / DatePicker / TimePicker / PeoplePicker / **Checkbox** / **Switch**,後二者 display = ✓/—)完整響應 `<Field mode="display"/"readonly">` + `<Field disabled>`;**Slider / Rating / SegmentedControl 無 display/readonly 態**(僅 enabled/disabled)→ 只響應 `<Field disabled>`。**group 控件(Checkbox/RadioGroup/Switch/SegmentedControl)雖非 fieldWrapperStyles 消費者,仍一律經 resolver hook 解析**(gate Check 1b/2 強制)。
166
+ 注:有 display 渲染分支者(Input 家族 / Select / Combobox / DatePicker / TimePicker / PeoplePicker / **Checkbox** / **Switch**,後二者 display = ✓/—)完整響應 `<Field mode="display"/"readonly">` + `<Field disabled>`;**Slider / Rating display 態但有 readonly cascade**(2026-06-12 補:Slider readonly = 鎖互動保留視覺;Rating readonly = 星星鎖定 role=img)+ 響應 `<Field disabled>`;**SegmentedControl 無 display/readonly 態**(僅 enabled/disabled)→ 只響應 `<Field disabled>`。**group 控件(Checkbox/RadioGroup/Switch/SegmentedControl)雖非 fieldWrapperStyles 消費者,仍一律經 resolver hook 解析**(gate Check 1b/2 強制)。
165
167
 
166
168
  **機械強制**:`scripts/check-field-cascade-resolve.mjs`(ci + release:preflight)—— 消費 `fieldWrapperStyles` 的控件若散落手刻 `fieldCtx?.{disabled,mode}` 解析(而非走 resolver hook)= fail,防新控件重演 cascade 漏接。
167
169
 
@@ -243,7 +243,9 @@ export const OrientationMatrix: Story = {
243
243
  </Field>
244
244
  <Field orientation="horizontal">
245
245
  <FieldLabel>訂閱電子報</FieldLabel>
246
- <Switch defaultChecked />
246
+ {/* 混合表單(Input/Select 同列)= Form-edit 情境 → Switch 靠左對齊其他控件
247
+ (switch.spec.md「兩種對齊慣例」判準;ml-0 覆寫 horizontal Field 預設 ml-auto) */}
248
+ <Switch defaultChecked className="ml-0" />
247
249
  </Field>
248
250
  <Field orientation="horizontal">
249
251
  <FieldLabel>個人檔案公開</FieldLabel>
@@ -85,6 +85,18 @@ export const StateCascade: Story = {
85
85
  <Field disabled className="w-44"><FieldLabel>付款方式</FieldLabel><RadioGroup defaultValue="card"><RadioGroupItem value="card" label="信用卡" /><RadioGroupItem value="cash" label="貨到付款" /></RadioGroup></Field>
86
86
  </div>
87
87
  </div>
88
+ <div>
89
+ <p className="text-body-sm font-medium text-fg-secondary mb-3">{'<Field mode="readonly"> — 鎖定表單:文字控件灰框;boolean/單選 = 灰框 + ✓/—/選中項(2026-06-12 拍板);Rating 星星鎖定、Slider 鎖互動保留視覺'}</p>
90
+ <div className="flex flex-wrap gap-x-8 gap-y-4 max-w-3xl">
91
+ <Field mode="readonly" className="w-44"><FieldLabel>負責人</FieldLabel><Input value="王小明" onChange={() => {}} /></Field>
92
+ <Field mode="readonly" className="w-44"><FieldLabel>同意條款</FieldLabel><Checkbox checked /></Field>
93
+ <Field mode="readonly" className="w-44"><FieldLabel>已啟用</FieldLabel><Switch checked /></Field>
94
+ <Field mode="readonly" className="w-44"><FieldLabel>未勾選範例</FieldLabel><Checkbox /></Field>
95
+ <Field mode="readonly" className="w-44"><FieldLabel>付款方式</FieldLabel><RadioGroup value="card"><RadioGroupItem value="card" label="信用卡" /><RadioGroupItem value="cash" label="貨到付款" /></RadioGroup></Field>
96
+ <Field mode="readonly" className="w-44"><FieldLabel>滿意度</FieldLabel><Rating value={4} aria-label="滿意度" /></Field>
97
+ <Field mode="readonly" className="w-44"><FieldLabel>完成度</FieldLabel><Slider defaultValue={[40]} aria-label="完成度" /></Field>
98
+ </div>
99
+ </div>
88
100
  <div>
89
101
  <p className="text-body-sm font-medium text-fg-secondary mb-3">{'<Field mode="display"> — 有展示態的控件自動切純展示(Select / DatePicker / Checkbox / Switch 修復後生效)'}</p>
90
102
  <div className="flex flex-wrap gap-x-8 gap-y-4 max-w-3xl">
@@ -245,7 +257,8 @@ export const MixedControlAlignment: Story = {
245
257
  </Field>
246
258
  <Field orientation="horizontal" labelWidth="120px">
247
259
  <FieldLabel>開啟通知</FieldLabel>
248
- <Switch />
260
+ {/* 混合表單 = Form-edit → ml-0 靠左(switch.spec.md「兩種對齊慣例」) */}
261
+ <Switch className="ml-0" />
249
262
  </Field>
250
263
  </FieldGroup>
251
264
  </div>
@@ -283,8 +283,9 @@ MultiPersonDisplay.displayName = 'MultiPersonDisplay'
283
283
  // - **12×12 圓**(固定,不隨 field size 變)
284
284
  // - **bg `--surface-strong`**(neutral-6),hover → `--surface-strong-hover`
285
285
  // (light=neutral-5 / dark=neutral-7,跨 mode 對稱)
286
- // - **X icon size=12 strokeWidth=3.5**(icon 跟底色一樣大,對齊 checkbox checkmark
287
- // sm/md stroke 規格)
286
+ // - **X icon size=12 strokeWidth=3**(icon 跟底色一樣大,對齊 checkbox checkmark
287
+ // sm/md stroke 規格;2026-06-12 同步 checkbox 2026-05-18 簡化 3.5→3,SSOT →
288
+ // .claude/references/ui-dev-rules.md「小尺寸 icon stroke 補償」)
288
289
  // - **text-on-emphasis**(白 X,確保飽和色底對比)
289
290
  // - **位置 `absolute top-0 right-0`**(button 右上角貼齊 avatar 右上角,完全在 avatar
290
291
  // 內 — user-confirmed canonical)
@@ -324,7 +325,7 @@ function AvatarDismissOverlay({ onRemove, label }: { onRemove: () => void; label
324
325
  'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring',
325
326
  ].join(' ')}
326
327
  >
327
- <X size={12} strokeWidth={3.5} aria-hidden />
328
+ <X size={12} strokeWidth={3} aria-hidden />
328
329
  </button>
329
330
  )
330
331
  }
@@ -374,8 +374,8 @@ export const AffixBehavior = {
374
374
  <div className="flex flex-col gap-2">
375
375
  <span className="text-caption font-medium text-fg-secondary">affix="status-icon" — final state</span>
376
376
  <p className="text-footnote text-fg-muted">
377
- success → <CircleCheck size={14} className="inline text-success" /> CircleCheck(16px, text-success);
378
- error → <XCircle size={14} className="inline text-error" /> XCircle(16px, text-error);
377
+ success → <CircleCheck size={16} className="inline text-success" /> CircleCheck(16px, text-success);
378
+ error → <XCircle size={16} className="inline text-error" /> XCircle(16px, text-error);
379
379
  inProgress → 無 icon(inProgress 非終態)。
380
380
  </p>
381
381
  <div className="flex flex-col gap-2 w-[360px]">
@@ -193,7 +193,7 @@ export const Overview = {
193
193
  ['disabled', 'boolean', 'false', '不可互動,移除品牌色'],
194
194
  ['label', 'ReactNode', '—', '選項 label;傳入時自動以 SelectionItem 包裝'],
195
195
  ['description', 'ReactNode', '—', '次要說明文字(須與 label 搭配)'],
196
- ['readOnly', 'boolean', 'false', '鎖互動、保留 checked 視覺(整組 readonly 由 RadioGroup mode="readonly" 傳遞)'],
196
+ ['readOnly', 'boolean', 'false', '鎖互動、保留 checked 視覺(standalone 整組 readonly 由 RadioGroup mode="readonly" 傳遞;Field 內整組改渲染灰框 + 選中項 label)'],
197
197
  ['id', 'string', '—', '搭配 SelectionItem 的 htmlFor'],
198
198
  ].map(([p, t, d, desc]) => (
199
199
  <tr key={p}><Td mono>{p}</Td><Td mono>{t}</Td><Td mono>{d}</Td><Td>{desc}</Td></tr>
@@ -90,6 +90,8 @@ Item-level default / hover / checked / disabled **色彩**與 Checkbox 共用同
90
90
 
91
91
  繼承 Field family,詳見 `../Field/field-controls.spec.md` + `../Field/form-validation.spec.md`。
92
92
 
93
+ **Field 內 readonly(2026-06-12 user 拍板)**:不渲染 radio 群組,改渲染 `fieldWrapperStyles` readonly 灰框 + 選中項 label(= Select readonly 同款,同為單選資料的鎖定呈現);standalone readonly 維持原樣鎖互動(ReadonlyContext)。
94
+
93
95
  ---
94
96
 
95
97
  ## 禁止事項