@qijenchen/design-system 0.1.0-beta.146 → 0.1.0-beta.147
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/dist/components/Accordion/accordion.d.ts.map +1 -1
- package/dist/components/Accordion/accordion.js +4 -1
- package/dist/components/Accordion/accordion.js.map +1 -1
- package/dist/components/AccountMenu/account-menu.d.ts.map +1 -1
- package/dist/components/AccountMenu/account-menu.js.map +1 -1
- package/dist/components/AgentPanel/agent-panel-fab.d.ts +7 -1
- package/dist/components/AgentPanel/agent-panel-fab.d.ts.map +1 -1
- package/dist/components/AgentPanel/agent-panel-fab.js +13 -9
- package/dist/components/AgentPanel/agent-panel-fab.js.map +1 -1
- package/dist/components/AgentPanel/agent-panel-logo.d.ts +3 -3
- package/dist/components/AgentPanel/agent-panel-logo.js.map +1 -1
- package/dist/components/AgentPanel/agent-panel.d.ts.map +1 -1
- package/dist/components/AgentPanel/agent-panel.js +6 -1
- package/dist/components/AgentPanel/agent-panel.js.map +1 -1
- package/dist/components/Breadcrumb/breadcrumb.d.ts +3 -1
- package/dist/components/Breadcrumb/breadcrumb.d.ts.map +1 -1
- package/dist/components/Breadcrumb/breadcrumb.js.map +1 -1
- package/dist/components/BulkActionBar/bulk-action-bar.d.ts.map +1 -1
- package/dist/components/BulkActionBar/bulk-action-bar.js +27 -2
- package/dist/components/BulkActionBar/bulk-action-bar.js.map +1 -1
- package/dist/components/Button/button.d.ts.map +1 -1
- package/dist/components/Button/button.js +56 -9
- package/dist/components/Button/button.js.map +1 -1
- package/dist/components/Calendar/calendar.d.ts +8 -3
- package/dist/components/Calendar/calendar.d.ts.map +1 -1
- package/dist/components/Calendar/calendar.js +22 -8
- package/dist/components/Calendar/calendar.js.map +1 -1
- package/dist/components/Carousel/carousel.d.ts.map +1 -1
- package/dist/components/Carousel/carousel.js +2 -0
- package/dist/components/Carousel/carousel.js.map +1 -1
- package/dist/components/Combobox/combobox.d.ts.map +1 -1
- package/dist/components/Combobox/combobox.js +10 -4
- package/dist/components/Combobox/combobox.js.map +1 -1
- package/dist/components/Command/command.d.ts.map +1 -1
- package/dist/components/Command/command.js +9 -5
- package/dist/components/Command/command.js.map +1 -1
- package/dist/components/DataTable/cell-registry.d.ts.map +1 -1
- package/dist/components/DataTable/cell-registry.js +2 -1
- package/dist/components/DataTable/cell-registry.js.map +1 -1
- package/dist/components/DataTable/data-table-column-visibility-panel.d.ts.map +1 -1
- package/dist/components/DataTable/data-table-column-visibility-panel.js +24 -24
- package/dist/components/DataTable/data-table-column-visibility-panel.js.map +1 -1
- package/dist/components/DataTable/data-table-sort-manager.d.ts.map +1 -1
- package/dist/components/DataTable/data-table-sort-manager.js +20 -20
- package/dist/components/DataTable/data-table-sort-manager.js.map +1 -1
- package/dist/components/DataTable/data-table.d.ts.map +1 -1
- package/dist/components/DataTable/data-table.js +104 -40
- package/dist/components/DataTable/data-table.js.map +1 -1
- package/dist/components/Dialog/dialog.d.ts.map +1 -1
- package/dist/components/Dialog/dialog.js +55 -43
- package/dist/components/Dialog/dialog.js.map +1 -1
- package/dist/components/Field/field-context.d.ts +4 -0
- package/dist/components/Field/field-context.d.ts.map +1 -1
- package/dist/components/Field/field-context.js +1 -2
- package/dist/components/Field/field-context.js.map +1 -1
- package/dist/components/Field/field-edit-keys.d.ts +8 -0
- package/dist/components/Field/field-edit-keys.d.ts.map +1 -1
- package/dist/components/Field/field-edit-keys.js +3 -0
- package/dist/components/Field/field-edit-keys.js.map +1 -1
- package/dist/components/Field/field.d.ts.map +1 -1
- package/dist/components/Field/field.js +1 -0
- package/dist/components/Field/field.js.map +1 -1
- package/dist/components/FileViewer/file-viewer.d.ts.map +1 -1
- package/dist/components/FileViewer/file-viewer.js +5 -2
- package/dist/components/FileViewer/file-viewer.js.map +1 -1
- package/dist/components/LinkInput/link-input.d.ts.map +1 -1
- package/dist/components/LinkInput/link-input.js +1 -0
- package/dist/components/LinkInput/link-input.js.map +1 -1
- package/dist/components/Notice/notice.js +1 -1
- package/dist/components/Notice/notice.js.map +1 -1
- package/dist/components/NumberInput/number-input.d.ts.map +1 -1
- package/dist/components/NumberInput/number-input.js +1 -0
- package/dist/components/NumberInput/number-input.js.map +1 -1
- package/dist/components/PeoplePicker/people-picker.d.ts.map +1 -1
- package/dist/components/PeoplePicker/people-picker.js.map +1 -1
- package/dist/components/ProfileCard/profile-card.d.ts.map +1 -1
- package/dist/components/ProfileCard/profile-card.js.map +1 -1
- package/dist/components/Rating/rating.d.ts.map +1 -1
- package/dist/components/Rating/rating.js +6 -3
- package/dist/components/Rating/rating.js.map +1 -1
- package/dist/components/Select/select.d.ts.map +1 -1
- package/dist/components/Select/select.js +13 -7
- package/dist/components/Select/select.js.map +1 -1
- package/dist/components/SelectionControl/selection-item.d.ts.map +1 -1
- package/dist/components/SelectionControl/selection-item.js +1 -0
- package/dist/components/SelectionControl/selection-item.js.map +1 -1
- package/dist/components/Sidebar/sidebar.d.ts.map +1 -1
- package/dist/components/Sidebar/sidebar.js +6 -1
- package/dist/components/Sidebar/sidebar.js.map +1 -1
- package/dist/components/Steps/steps.d.ts.map +1 -1
- package/dist/components/Steps/steps.js +13 -3
- package/dist/components/Steps/steps.js.map +1 -1
- package/dist/components/Tag/tag.d.ts.map +1 -1
- package/dist/components/Tag/tag.js +2 -1
- package/dist/components/Tag/tag.js.map +1 -1
- package/dist/components/Textarea/textarea.d.ts.map +1 -1
- package/dist/components/Textarea/textarea.js +4 -0
- package/dist/components/Textarea/textarea.js.map +1 -1
- package/dist/components/TimePicker/time-columns.d.ts.map +1 -1
- package/dist/components/TimePicker/time-columns.js +3 -3
- package/dist/components/TimePicker/time-columns.js.map +1 -1
- package/dist/components/TreeView/tree-view.d.ts.map +1 -1
- package/dist/components/TreeView/tree-view.js +54 -10
- package/dist/components/TreeView/tree-view.js.map +1 -1
- package/dist/hooks/use-overflow-items.d.ts.map +1 -1
- package/dist/hooks/use-overflow-items.js +2 -0
- package/dist/hooks/use-overflow-items.js.map +1 -1
- package/dist/hooks/use-truncated.d.ts.map +1 -1
- package/dist/hooks/use-truncated.js.map +1 -1
- package/dist/index.js +3 -1
- package/dist/lib/drag-announcements.d.ts +32 -0
- package/dist/lib/drag-announcements.d.ts.map +1 -1
- package/dist/lib/drag-announcements.js +39 -1
- package/dist/lib/drag-announcements.js.map +1 -1
- package/dist/lib/overlay-coexistence.js +1 -1
- package/dist/lib/roving-list-keyboard.d.ts +15 -0
- package/dist/lib/roving-list-keyboard.d.ts.map +1 -1
- package/dist/lib/roving-list-keyboard.js +6 -0
- package/dist/lib/roving-list-keyboard.js.map +1 -1
- package/dist/lib/utils.js +1 -1
- package/dist/lib/utils.js.map +1 -1
- package/dist/patterns/horizontal-overflow/horizontal-overflow.d.ts.map +1 -1
- package/dist/patterns/horizontal-overflow/horizontal-overflow.js +25 -0
- package/dist/patterns/horizontal-overflow/horizontal-overflow.js.map +1 -1
- package/dist/patterns/overlay-surface/overlay-surface.js.map +1 -1
- package/ds-canonical/fork/codex/agents/skills/performance-audit/SKILL.md +1 -1
- package/ds-canonical/fork/consumer/lock.json +11 -11
- package/ds-canonical/fork/consumer/managed-files/46f10bc55cdfdf834d0749eebff42ea0bb8576f361b36d2b6ba1851354c50150.blob +47 -9
- package/ds-canonical/fork/consumer/managed-files/5af0a46760af338d51fd0f7950675f8e97f7d2f09386b8818fcd7a58e6654e75.blob +71 -1
- package/ds-canonical/fork/consumer/managed-files/7eb6cc60abfdabaf6617c336058fad1454ccde08889295767bfd9b7fd153cc6e.blob +45 -6
- package/ds-canonical/fork/consumer/managed-files/ab072e7f2eca51f0e18a5e18c4b333260ee5d219a12cc6df5bb2d683f0b2734d.blob +27 -1
- package/ds-canonical/fork/governance.lock +9 -9
- package/ds-canonical/fork/manifest.json +10 -10
- package/ds-canonical/fork/providers/claude/skills/performance-audit/SKILL.md +1 -1
- package/ds-canonical/fork/providers/codex/skills/performance-audit/SKILL.md +1 -1
- package/ds-canonical/fork/skills/performance-audit/SKILL.md +1 -1
- package/ds-canonical/hooks/check_substantive_edit_approval_preflight.sh +29 -8
- package/ds-canonical/hooks/tests/test_check_substantive_edit_approval_preflight.sh +16 -6
- package/ds-canonical/references/focus-canonical.md +1 -1
- package/ds-canonical/references/governance-audit-coverage.md +1 -1
- package/ds-canonical/references/hit-area-canonical.md +4 -2
- package/ds-canonical/references/keyboard-model-canonical.md +4 -4
- package/ds-canonical/rules/meta-patterns.md +1 -1
- package/ds-canonical/rules/self-verify.md +4 -4
- package/ds-canonical/skills/design-system-audit/references/historical-bugs.md +27 -0
- package/ds-canonical/skills/performance-audit/SKILL.md +1 -1
- package/ds-story-manifest.json +11 -5
- package/llms-full.txt +2 -2
- package/llms.txt +2 -2
- package/package.json +3 -1
- package/src/components/Accordion/accordion.tsx +4 -1
- package/src/components/AccountMenu/account-menu.spec.md +1 -2
- package/src/components/AccountMenu/account-menu.tsx +0 -1
- package/src/components/AgentPanel/agent-decision-card.spec.md +88 -0
- package/src/components/AgentPanel/agent-panel-fab.spec.md +151 -0
- package/src/components/AgentPanel/agent-panel-fab.tsx +41 -17
- package/src/components/AgentPanel/agent-panel-logo.spec.md +87 -0
- package/src/components/AgentPanel/agent-panel-logo.tsx +3 -3
- package/src/components/AgentPanel/agent-panel.principles.stories.tsx +1 -1
- package/src/components/AgentPanel/agent-panel.spec.md +24 -284
- package/src/components/AgentPanel/agent-panel.stories.tsx +112 -7
- package/src/components/AgentPanel/agent-panel.tsx +9 -4
- package/src/components/Breadcrumb/breadcrumb.tsx +3 -1
- package/src/components/BulkActionBar/bulk-action-bar.spec.md +1 -0
- package/src/components/BulkActionBar/bulk-action-bar.stories.tsx +53 -0
- package/src/components/BulkActionBar/bulk-action-bar.tsx +32 -2
- package/src/components/Button/button.anatomy.stories.tsx +60 -31
- package/src/components/Button/button.spec.md +6 -4
- package/src/components/Button/button.tsx +52 -8
- package/src/components/Calendar/calendar.anatomy.stories.tsx +10 -7
- package/src/components/Calendar/calendar.principles.stories.tsx +8 -3
- package/src/components/Calendar/calendar.spec.md +12 -10
- package/src/components/Calendar/calendar.stories.tsx +76 -0
- package/src/components/Calendar/calendar.tsx +39 -14
- package/src/components/Carousel/carousel.spec.md +1 -1
- package/src/components/Carousel/carousel.tsx +5 -0
- package/src/components/Chip/chip.spec.md +1 -0
- package/src/components/Combobox/combobox.anatomy.stories.tsx +2 -1
- package/src/components/Combobox/combobox.tsx +19 -5
- package/src/components/Command/command.tsx +9 -5
- package/src/components/DataTable/cell-registry.tsx +3 -0
- package/src/components/DataTable/data-table-column-visibility-panel.tsx +5 -6
- package/src/components/DataTable/data-table-known-defects.spec.md +72 -0
- package/src/components/DataTable/data-table-scroll-performance.spec.md +295 -0
- package/src/components/DataTable/data-table-sort-manager.tsx +5 -6
- package/src/components/DataTable/data-table.spec.md +9 -341
- package/src/components/DataTable/data-table.stories.tsx +1 -1
- package/src/components/DataTable/data-table.tsx +152 -51
- package/src/components/DatePicker/date-picker.spec.md +1 -1
- package/src/components/Dialog/dialog.spec.md +5 -1
- package/src/components/Dialog/dialog.stories.tsx +9 -4
- package/src/components/Dialog/dialog.tsx +80 -47
- package/src/components/DropdownMenu/dropdown-menu.anatomy.stories.tsx +7 -0
- package/src/components/Empty/empty.spec.md +1 -2
- package/src/components/Field/field-context.ts +5 -2
- package/src/components/Field/field-controls.spec.md +3 -0
- package/src/components/Field/field-edit-keys.ts +12 -0
- package/src/components/Field/field.spec.md +1 -1
- package/src/components/Field/field.tsx +3 -0
- package/src/components/FileViewer/file-viewer.tsx +5 -2
- package/src/components/LinkInput/link-input.tsx +3 -0
- package/src/components/Notice/notice.tsx +1 -1
- package/src/components/NumberInput/number-input.tsx +3 -0
- package/src/components/Pagination/pagination.spec.md +1 -0
- package/src/components/PeoplePicker/people-picker.tsx +0 -3
- package/src/components/Popover/popover.spec.md +2 -0
- package/src/components/ProfileCard/profile-card.tsx +0 -3
- package/src/components/Rating/rating.anatomy.stories.tsx +1 -1
- package/src/components/Rating/rating.spec.md +3 -3
- package/src/components/Rating/rating.tsx +7 -3
- package/src/components/ScrollArea/scroll-area.spec.md +2 -0
- package/src/components/Select/select.spec.md +1 -1
- package/src/components/Select/select.tsx +11 -4
- package/src/components/SelectMenu/select-menu.spec.md +4 -0
- package/src/components/SelectionControl/selection-item.tsx +3 -1
- package/src/components/Sidebar/sidebar.spec.md +3 -2
- package/src/components/Sidebar/sidebar.stories.tsx +79 -0
- package/src/components/Sidebar/sidebar.tsx +10 -2
- package/src/components/Skeleton/skeleton.spec.md +1 -1
- package/src/components/Slider/slider.spec.md +1 -0
- package/src/components/Steps/steps-state-visuals.spec.md +157 -0
- package/src/components/Steps/steps.spec.md +13 -144
- package/src/components/Steps/steps.tsx +29 -5
- package/src/components/Tag/tag.spec.md +9 -10
- package/src/components/Tag/tag.tsx +2 -2
- package/src/components/Textarea/textarea.tsx +4 -0
- package/src/components/TimePicker/time-columns.tsx +4 -3
- package/src/components/TimePicker/time-picker.spec.md +1 -1
- package/src/components/Tooltip/tooltip.spec.md +1 -0
- package/src/components/TreeView/tree-view.spec.md +4 -1
- package/src/components/TreeView/tree-view.stories.tsx +87 -3
- package/src/components/TreeView/tree-view.tsx +88 -12
- package/src/hooks/use-overflow-items.ts +4 -0
- package/src/hooks/use-truncated.ts +3 -2
- package/src/lib/drag-announcements.ts +78 -0
- package/src/lib/roving-list-keyboard.ts +20 -0
- package/src/lib/utils.ts +1 -1
- package/src/patterns/action-bar/action-bar.spec.md +2 -0
- package/src/patterns/element-anatomy/inline-action.spec.md +2 -2
- package/src/patterns/element-anatomy/item-anatomy.spec.md +5 -3
- package/src/patterns/element-anatomy/truncated-text.spec.md +1 -0
- package/src/patterns/header-canonical/header-canonical.spec.md +2 -1
- package/src/patterns/horizontal-overflow/horizontal-overflow.spec.md +2 -0
- package/src/patterns/horizontal-overflow/horizontal-overflow.stories.tsx +36 -0
- package/src/patterns/horizontal-overflow/horizontal-overflow.tsx +28 -0
- package/src/patterns/overlay-surface/overlay-chrome-sizing.spec.md +180 -0
- package/src/patterns/overlay-surface/overlay-surface.spec.md +5 -160
- package/src/patterns/overlay-surface/overlay-surface.tsx +1 -1
- package/src/patterns/resize-handle/resize-handle.spec.md +2 -2
- package/src/tokens/color/color.spec.md +14 -6
- package/src/tokens/color/semantic.css +13 -0
- package/src/tokens/layoutSpace/layoutSpace.spec.md +2 -0
- package/src/tokens/motion/motion.spec.md +5 -0
- package/src/tokens/orphan-tokens.spec.md +1 -1
- package/src/tokens/uiSize/uiSize.spec.md +1 -1
- package/dist/node_modules/aria-hidden/dist/es2015/index.js +0 -147
- package/dist/node_modules/aria-hidden/dist/es2015/index.js.map +0 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"button.js","sources":["../../../src/components/Button/button.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(Button + iconOnly + danger + loading + asChild + dismiss + pressedTone)— 跨 7 axis variant 集中 SSOT 一處,拆分會 fragment cva variant catalog。當前 528 < cap 800。\nimport * as React from 'react'\nimport { Slot } from '@radix-ui/react-slot'\nimport { cva, type VariantProps } from 'class-variance-authority'\nimport { CircularProgress } from '@/design-system/components/CircularProgress/circular-progress'\nimport type { LucideIcon } from 'lucide-react'\nimport { cn } from '@/lib/utils'\nimport { useResolvedFieldSize } from '@/design-system/components/Field/field-context'\nimport { Tooltip, TooltipContent, TooltipTrigger } from '@/design-system/components/Tooltip/tooltip'\n\n/**\n * Button — shadcn 風格,橋接設計系統 token\n *\n * ── Variants ──\n * primary 主要操作,藍底白字\n * secondary 次要品牌操作,藍框藍字;正面 vs 負面並存時用於正面那個\n * tertiary 一般輔助操作,灰框灰字,hover 轉藍(最常用的非主要按鈕)\n * text 無底色無邊框,hover 顯示灰底(工具列、密集 UI)\n * link 外觀像連結的按鈕(本質仍是 button)\n *\n * ── danger prop ──\n * danger 套用危險色(紅色);僅 primary / secondary / text 支援(tertiary / link 不支援,見 spec 禁止事項)\n *\n * <Button variant=\"primary\" danger>永久刪除</Button> → 紅底白字(立即不可逆)\n * <Button variant=\"secondary\" danger>移至垃圾桶</Button> → 紅框紅字(點下去還可反悔)\n *\n * ── pressed prop(toggle)──\n * pressed Toggle 按下狀態(持續 on/off),寫入 aria-pressed + data-state\n * 僅 secondary / tertiary / text 三個 variant 支援 toggle 視覺:\n * - secondary + pressed → primary-subtle 底、primary 字、透明邊框\n * - tertiary + pressed → primary-subtle 底、primary 字、透明邊框(同 secondary 按下視覺)\n * - text + pressed → primary-subtle 底、primary 字、透明邊框(預設 pressedTone='emphasis',同 secondary/tertiary 按下視覺);pressedTone='neutral' 時才走 neutral-selected 灰底\n * primary / link 傳入 pressed 無視覺效果(語意不符)\n *\n * ── Sizes(預設 md)──\n * xs h-field-xs(24px 固定),不隨 density 縮放\n * sm h-field-sm,md=28px / lg=32px\n * md h-field-md,md=32px / lg=36px ← 預設(跟 Field/Input 對齊)\n * lg h-field-lg,md=36px / lg=40px\n * icon-only 不是獨立尺寸 — 加 iconOnly prop 讓任何尺寸變正方形\n *\n * ── 內部結構 ──\n * [startIcon?] [label] [badge? + endIcon?]\n *\n * ── 用法範例 ──\n * <Button startIcon={Plus}>新增</Button>\n * <Button variant=\"tertiary\">取消</Button>\n * <Button variant=\"primary\" danger>永久刪除</Button>\n * <Button variant=\"text\" pressed={isPinned} startIcon={Pin} aria-label=\"釘選\" iconOnly />\n * <Button badge={<Badge count={3} />} endIcon={ChevronDown}>通知</Button>\n * <Button size=\"sm\" iconOnly startIcon={Plus} aria-label=\"新增\" />\n * <Button iconOnly startIcon={Bell} aria-label=\"通知 (3 則)\"\n * overlayBadge={<Badge count={3} />} /> ← badge 自動貼 icon 右上角\n *\n * ── asChild ──\n * <Button asChild><Link to=\"/home\">回首頁</Link></Button>\n */\nconst buttonVariants = cva(\n [\n 'inline-flex items-center justify-center',\n 'whitespace-nowrap font-medium',\n 'border border-transparent',\n // hover 底色瞬間切換,不做過渡(user 2026-09-10 拍板「第三題改成全部瞬間」;SSOT = tokens/motion/motion.spec.md「hover 回饋不做過渡」)\n 'cursor-pointer select-none disabled:cursor-not-allowed aria-disabled:cursor-not-allowed',\n // 2026-05-12 Round 4.5 fix(codex M31 Layer C 抓):`aria-disabled` visual 分支補(per WAI-ARIA APG —\n // aria-disabled 給語意 + visual,但不 suppress functionality;functionality 由 consumer 阻 e.g.\n // RowDragHandle listeners 只在 canDrag spread)。**故意不加** `aria-disabled:pointer-events-none`\n // — Round 4 RowDragHandle Tooltip flicker fix root cause:aria-disabled buttons 必保 pointer events\n // 讓 Radix Tooltip pointerenter 通過。HTML `disabled` 保 PE:none(完全 inactive)。\n 'disabled:pointer-events-none',\n // 2026-05-12 fix v2(playwright pixel-quantified verify 抓 opacity=1 不生效):\n // `opacity-disabled` 是 custom Tailwind v4 `@utility`(opacity.css:21),variant prefix\n // `aria-disabled:` 跟 custom @utility 不 compose(`aria-disabled:cursor-not-allowed` 標準\n // utility 能 work,custom 不行)。改 arbitrary value `opacity-[var(--opacity-disabled)]`\n // 繞 custom @utility 限制(Tailwind v4 arbitrary value 直接生 `opacity: var(--opacity-disabled)`)。\n 'aria-disabled:opacity-[var(--opacity-disabled)]',\n 'rounded-md',\n // Defensive:SVG 不被 flex shrink 擠扁(防 inner-area 計算誤差導致 icon 被擠成\n // width<intrinsic 的 asymmetric 顯示)。詳 ICON_ONLY_PX 段 rationale。\n '[&_svg]:shrink-0',\n ],\n {\n variants: {\n variant: {\n primary: [\n 'bg-primary text-on-emphasis',\n 'hover:bg-primary-hover',\n 'active:bg-primary-active',\n // aria-disabled 的 hover / active 釘在靜止(button.spec.md「狀態疊加」表 aria-disabled 列:與 disabled 同樣不給互動回饋;\n // 原生 disabled 靠 pointer-events-none,aria-disabled 為了 Tooltip 保留指標事件,只能顯式釘住;先例 switch.tsx 的 `disabled:…:hover:` 守衛)\n 'aria-disabled:hover:bg-primary aria-disabled:active:bg-primary',\n 'disabled:bg-disabled disabled:text-fg-disabled disabled:border-transparent',\n ],\n secondary: [\n 'bg-surface text-primary border-primary',\n 'hover:text-primary-hover hover:border-primary-hover',\n 'active:text-primary-active active:border-primary-active',\n // Overlay trigger active(asChild Popover/DropdownMenu trigger)— 維持 hover 樣式\n // (canonical 對齊 inline-action.spec.md:trigger 維持 host hover 直到 overlay 關閉)\n 'data-[state=open]:text-primary-hover data-[state=open]:border-primary-hover',\n // aria-disabled 釘住(同 primary 註解);`not-aria-pressed` = 只釘「未按下」的靜止,按下的由 pressed compound 自己釘(避免兩組 (0,3,0) 靠產出順序決勝)\n 'aria-disabled:not-aria-pressed:hover:text-primary aria-disabled:not-aria-pressed:hover:border-primary',\n 'aria-disabled:not-aria-pressed:active:text-primary aria-disabled:not-aria-pressed:active:border-primary',\n 'disabled:bg-transparent disabled:text-fg-disabled disabled:border-border',\n // 2026-05-21 v12:Toggle pressed 視覺移到 compoundVariants(variant × pressedTone),\n // 同時支援 emphasis(藍底)/ neutral(灰底)兩 tone。詳 cva.compoundVariants 段。\n ],\n tertiary: [\n 'bg-surface text-foreground border-border',\n 'hover:text-primary-hover hover:border-primary-hover',\n 'active:text-primary-active active:border-primary-active',\n // Overlay trigger active — 維持 hover 樣式(同 secondary 邏輯)\n 'data-[state=open]:text-primary-hover data-[state=open]:border-primary-hover',\n // aria-disabled 釘住(同 secondary 註解)\n 'aria-disabled:not-aria-pressed:hover:text-foreground aria-disabled:not-aria-pressed:hover:border-border',\n 'aria-disabled:not-aria-pressed:active:text-foreground aria-disabled:not-aria-pressed:active:border-border',\n 'disabled:bg-transparent disabled:text-fg-disabled disabled:border-border',\n ],\n text: [\n 'bg-transparent text-foreground border-transparent',\n 'hover:bg-neutral-hover',\n 'active:bg-neutral-active',\n // Overlay trigger active — 維持 hover 樣式(canonical 2026-05-02 改:hover token,\n // 不另開 selected 4% — 對齊 shadcn/Radix/Material 狀態極簡派,跨 host 一致)\n 'data-[state=open]:bg-neutral-hover',\n // aria-disabled 釘住(同 secondary 註解)\n 'aria-disabled:not-aria-pressed:hover:bg-transparent aria-disabled:not-aria-pressed:active:bg-transparent',\n 'disabled:bg-transparent disabled:text-fg-disabled',\n ],\n link: [\n 'bg-transparent text-primary border-transparent',\n 'hover:text-primary-hover',\n 'active:text-primary-active',\n // aria-disabled 釘住(同 primary 註解;link 無 pressed 視覺,不需 not-aria-pressed)\n 'aria-disabled:hover:text-primary aria-disabled:active:text-primary',\n 'disabled:text-fg-disabled',\n ],\n },\n danger: {\n true: '', // 實際樣式由 compoundVariants 提供\n },\n /**\n * 2026-05-21 v12 — pressed visual tone(per user「我認同這一個方向,然後預設emphasis」):\n * emphasis = 淡藍底(toolbar functional toggle / 篩選啟用 / 面板開關 — Figma toolbar /\n * Linear toolbar / Material ToggleButton 共識)\n * neutral = 灰底(**可取消**的切換鈕、要比藍底低一級時;不用於導覽 / 目前頁 —— 那是選中列,\n * owner = item-anatomy.spec.md「選中 × 互動疊加」;button.spec.md pressedTone 表,待辦總帳 B5 2026-09-25 更正)\n * 預設 `emphasis` per user directive。實際樣式由 compoundVariants(variant × pressedTone)套用。\n * 只在 secondary / tertiary / text variant 觸發 toggle 視覺;primary / link 無視覺效果。\n */\n pressedTone: {\n emphasis: '',\n neutral: '',\n },\n size: {\n xs: 'h-field-xs px-2 text-caption leading-compact gap-0',\n sm: 'h-field-sm px-3 min-w-14 text-body leading-compact gap-1',\n md: 'h-field-md px-3 min-w-16 text-body leading-compact gap-1',\n lg: 'h-field-lg px-3 min-w-20 text-body-lg leading-compact gap-1',\n },\n },\n compoundVariants: [\n // primary + danger → 紅底白字(立即不可逆操作)\n {\n variant: 'primary',\n danger: true,\n class: [\n 'bg-error text-on-emphasis border-transparent',\n 'hover:bg-error-hover',\n 'active:bg-error-active',\n // aria-disabled 釘在自己的靜止紅(button.spec.md「狀態疊加」表 aria-disabled 列)\n 'aria-disabled:hover:bg-error aria-disabled:active:bg-error',\n ],\n },\n // secondary + danger → 紅框紅字(有確認步驟的危險操作)\n {\n variant: 'secondary',\n danger: true,\n class: [\n 'bg-surface text-error border-error',\n 'hover:text-error-hover hover:border-error-hover',\n 'active:text-error-active active:border-error-active',\n // 開啟中 = 自己的 hover(error-hover),不是 secondary 的 primary-hover(inline-action.spec.md「overlay 開啟 → 同 host hover」;\n // button.spec.md「狀態疊加」表開啟中列)。靠 cn()/twMerge 去掉 variant 那組同前綴 class,同本檔 danger 覆寫慣例\n 'data-[state=open]:text-error-hover data-[state=open]:border-error-hover',\n 'aria-disabled:not-aria-pressed:hover:text-error aria-disabled:not-aria-pressed:hover:border-error',\n 'aria-disabled:not-aria-pressed:active:text-error aria-disabled:not-aria-pressed:active:border-error',\n ],\n },\n // text + danger → 紅字,hover 灰底\n {\n variant: 'text',\n danger: true,\n class: [\n 'text-error',\n 'hover:bg-neutral-hover hover:text-error-hover',\n 'active:bg-neutral-active active:text-error-active',\n // 開啟中 = 自己的 hover 配對:底色沿用 text 的 data-[state=open]:bg-neutral-hover,字補上 error-hover(同 secondary+danger 註解)\n 'data-[state=open]:text-error-hover',\n 'aria-disabled:not-aria-pressed:hover:text-error aria-disabled:not-aria-pressed:active:text-error',\n ],\n },\n // ── 2026-05-21 v12 Toggle pressed(variant × pressedTone)──────────────────\n // 視覺由 data-[state=on] + aria-pressed(Radix overlay trigger fallback)觸發\n //\n // emphasis tone:藍底(primary-subtle / primary 字)— functional toggle\n {\n variant: ['secondary', 'tertiary', 'text'],\n pressedTone: 'emphasis',\n class: [\n 'data-[state=on]:bg-primary-subtle data-[state=on]:text-primary data-[state=on]:border-transparent',\n // 按下的 hover / active 只換字(primary-hover / -active),底色與邊框顯式釘住 —— 照 sidebar.tsx\n // `data-[active=true]:hover:bg-neutral-selected` 寫法,釘住 (0,3,0) > variant 的 hover / active (0,2,0)。\n // 之前底色只是靠 CSS 產出順序(data-* 排在 hover 之後)才沒被 text 的 neutral-hover 換掉(button.spec.md「狀態疊加」表 pressed × hover / active 列)\n 'data-[state=on]:hover:bg-primary-subtle data-[state=on]:hover:border-transparent data-[state=on]:hover:text-primary-hover',\n 'data-[state=on]:active:bg-primary-subtle data-[state=on]:active:border-transparent data-[state=on]:active:text-primary-active',\n 'data-[state=on]:disabled:bg-disabled data-[state=on]:disabled:text-fg-disabled data-[state=on]:disabled:border-transparent',\n // aria-pressed fallback(Radix overlay trigger override data-state 時仍生效)—— 逐條鏡像上方 data-[state=on] 分支\n 'aria-pressed:bg-primary-subtle aria-pressed:text-primary aria-pressed:border-transparent',\n 'aria-pressed:hover:bg-primary-subtle aria-pressed:hover:border-transparent aria-pressed:hover:text-primary-hover',\n // 2026-07-07 補齊第三階:fallback 分支與 data-[state=on] 分支同拼寫(選中之上按壓 = active 階)\n 'aria-pressed:active:bg-primary-subtle aria-pressed:active:border-transparent aria-pressed:active:text-primary-active',\n // pressed + disabled:disabled 視覺優先(button.spec.md「狀態 → disabled」pressed + disabled 條);fallback 分支原本缺這條,鏡像 data-[state=on]:disabled\n 'aria-pressed:disabled:bg-disabled aria-pressed:disabled:text-fg-disabled aria-pressed:disabled:border-transparent',\n // 按下的觸發鈕開啟中 = 按下自己的 hover(底色與邊框不動、字 primary-hover),不是 variant 的開啟樣式\n //(inline-action.spec.md「overlay 開啟 → 同 host hover」;button.spec.md「狀態疊加」表開啟中列)。\n // Radix 會把 data-state 改寫成 open,所以只走 aria-pressed;(0,3,0) > variant 的 data-[state=open] (0,2,0)\n 'aria-pressed:data-[state=open]:bg-primary-subtle aria-pressed:data-[state=open]:border-transparent aria-pressed:data-[state=open]:text-primary-hover',\n // 按下 + aria-disabled:hover / active 釘在按下的靜止字色(底色與邊框已由上方釘住)\n 'data-[state=on]:aria-disabled:hover:text-primary data-[state=on]:aria-disabled:active:text-primary',\n 'aria-pressed:aria-disabled:hover:text-primary aria-pressed:aria-disabled:active:text-primary',\n ],\n },\n // neutral tone:灰底(neutral-selected family)— 可取消的切換鈕;導覽 / 目前頁不用它(選中列規則,item-anatomy.spec.md「選中 × 互動疊加」)\n // 階梯值 selected → -selected-hover → -selected-active(2→3→4):user 2026-09-25 選「甲:保留,維持 2→3→4 (Recommended)」\n // (選項由 AI 提供;待辦總帳 B5)。下方開啟中 / aria-disabled 各條都是「跟著本分支的 hover / 靜止 token 走」,改階梯時同一 compound 內一起改。\n {\n variant: ['secondary', 'tertiary', 'text'],\n pressedTone: 'neutral',\n class: [\n 'data-[state=on]:bg-neutral-selected data-[state=on]:text-foreground data-[state=on]:border-transparent',\n // 字與邊框在 hover / active 顯式釘住(secondary / tertiary 的 hover 會把字與框轉 primary;同 emphasis 分支註解)\n 'data-[state=on]:hover:bg-neutral-selected-hover data-[state=on]:hover:text-foreground data-[state=on]:hover:border-transparent',\n 'data-[state=on]:active:bg-neutral-selected-active data-[state=on]:active:text-foreground data-[state=on]:active:border-transparent',\n 'data-[state=on]:disabled:bg-transparent data-[state=on]:disabled:text-fg-disabled',\n // aria-pressed fallback —— 逐條鏡像上方 data-[state=on] 分支\n 'aria-pressed:bg-neutral-selected aria-pressed:text-foreground aria-pressed:border-transparent',\n 'aria-pressed:hover:bg-neutral-selected-hover aria-pressed:hover:text-foreground aria-pressed:hover:border-transparent',\n // 補齊第三階(按壓):fallback 原本缺,data-[state=on] 分支與 emphasis 分支都有(同 emphasis 2026-07-07 註解)\n 'aria-pressed:active:bg-neutral-selected-active aria-pressed:active:text-foreground aria-pressed:active:border-transparent',\n 'aria-pressed:disabled:bg-transparent aria-pressed:disabled:text-fg-disabled',\n // 按下的觸發鈕開啟中 = 按下自己的 hover(neutral-selected-hover),不是 variant 的 neutral-hover / primary-hover(同 emphasis 分支註解)\n 'aria-pressed:data-[state=open]:bg-neutral-selected-hover aria-pressed:data-[state=open]:text-foreground aria-pressed:data-[state=open]:border-transparent',\n // 按下 + aria-disabled:hover / active 釘在按下的靜止底色\n 'data-[state=on]:aria-disabled:hover:bg-neutral-selected data-[state=on]:aria-disabled:active:bg-neutral-selected',\n 'aria-pressed:aria-disabled:hover:bg-neutral-selected aria-pressed:aria-disabled:active:bg-neutral-selected',\n ],\n },\n ],\n defaultVariants: {\n // labeled 預設 = tertiary(2026-06-06 從 primary 改;iconOnly 預設 = text,由 resolvedVariant 注入)。\n // 真正預設邏輯在元件內 resolvedVariant(相對指法,免行號漂移);此 cva default 供直接 buttonVariants() 呼叫者一致。\n variant: 'tertiary',\n size: 'md',\n pressedTone: 'emphasis',\n },\n }\n)\n\n// ── ButtonGroup Context ──────────────────────────────────────────────────────\n// ButtonGroup 提供此 context;Button 讀取它注入 fullWidth。\n// Context 放本檔(不放 button-group.tsx)以避免循環 import。\ninterface ButtonGroupContextValue {\n fullWidth?: boolean\n}\n/** @internal — ButtonGroup↔Button 私有 context(fullWidth 注入);consumer 不直接 import,經 <ButtonGroup> 消費。root barrel 排除(subpath 仍可 wrap 後用)。 */\nconst ButtonGroupContext = React.createContext<ButtonGroupContextValue>({})\n\ntype InternalVariant = VariantProps<typeof buttonVariants>['variant']\n\nexport interface ButtonProps\n extends React.ButtonHTMLAttributes<HTMLButtonElement>,\n Omit<VariantProps<typeof buttonVariants>, 'variant' | 'danger'> {\n /**\n * 將樣式套用至子元件(e.g. React Router Link)。\n *\n * ⚠️ **icon-only + asChild 警告**:當 `iconOnly={true}` 且 `asChild={true}` 時,內建\n * Tooltip wrapper 不啟動(Radix Slot 不接受多 child)。consumer 必須**自管 child 的 `aria-label`**\n * 給 screen reader,並視需要自行包 `<Tooltip>`(對齊 Polaris / Radix asChild idiom)。\n * Button 不會主動補 tooltip — 否則會破壞 Slot 單 child 規則。\n */\n asChild?: boolean\n /**\n * 按鈕視覺強調等級。\n * `destructive` / `ghost` 為 shadcn 內部 compat,請勿在應用程式碼中直接使用。\n */\n variant?: 'primary' | 'secondary' | 'tertiary' | 'text' | 'link'\n /** 套用危險色(紅色)。僅 primary / secondary / text 支援(tertiary / link 不支援,見 spec 禁止事項)。 */\n danger?: boolean\n /**\n * Toggle 按下狀態(持續 on/off)。設定時 Button 變為 toggle:\n * - 自動寫入 `aria-pressed` + `data-state=\"on\" | \"off\"`\n * - 樣式由 variant × `pressedTone` 的 compoundVariants 套用\n * - 僅 secondary / tertiary / text 有 toggle 視覺;primary / link 傳入無效果\n *\n * 不傳此 prop 時 Button 就是一般按鈕,不帶 aria-pressed。\n */\n pressed?: boolean\n /**\n * Pressed 視覺色調(2026-05-21 v12 加):\n * - `'emphasis'`(預設)→ 淡藍底 / primary 字(對齊 Figma toolbar / Linear toolbar /\n * Material ToggleButton 共識 — toolbar functional toggle / 篩選啟用 / 面板開關)\n * - `'neutral'` → 灰底 / foreground 字 —— **可取消**的切換鈕(再點一次就關掉)需要低一級強調時;\n * 導覽 / 目前頁不用它(那是選中列,滑過釘住;button.spec.md pressedTone 表,2026-09-25 更正)\n *\n * 僅在 `pressed` 啟用且 variant ∈ {secondary, tertiary, text} 時生效。\n * 跨 toggle context 維持單一 prop API,consumer 視語意選 tone 不另開 variant。\n */\n pressedTone?: 'emphasis' | 'neutral'\n /** 左側 icon(LucideIcon),最多一個,loading 時自動替換為 spinner */\n startIcon?: LucideIcon\n /** 右側 badge(ReactNode),通常傳入計數指示器 */\n badge?: React.ReactNode\n /**\n * Overlay badge(iconOnly 專用)。接收 `<Badge>` 元素,Button 內部**自動定位在 startIcon 右上角**——\n * badge 中心對齊 icon 的 top-right corner(Material BadgedBox / iOS App icon canonical),不是按鈕邊緣。\n * 解決手刻 `relative + absolute -top-1 -right-1` 讓 badge 飄在按鈕 chrome 右上的問題。\n *\n * 世界級對照:Material BadgedBox、iOS App Icon、Ant Badge wrap icon,badge 相對於**視覺重心**(icon)。\n * 只在 `iconOnly=true` 時生效;非 iconOnly 時應該用 inline `badge` prop 放 suffix 位置。\n */\n overlayBadge?: React.ReactNode\n /** 右側 icon(LucideIcon),放在 badge 右邊,通常用於 ChevronDown 等方向指示 */\n endIcon?: LucideIcon\n /** Icon-only 模式:移除 padding,變為正方形(必須同時設定 aria-label) */\n iconOnly?: boolean\n /**\n * Dismiss 視覺類(X close only canonical)。專用於 **X(close)icon 的 dismiss 語意** —\n * 「關閉 surface / 忽略訊息」。**不適用 Trash / Delete / Clear / Remove 等 destructive / clear 操作**。\n *\n * 自動套用:\n * - `variant=\"text\"`(強制 override 其他 variant)\n * - `iconOnly=true`(強制)\n * - Icon 色 override:`fg-muted` → hover `fg-secondary`(跟 Inline Action dismiss 視覺一致)\n *\n * 典型 case:Dialog / Sheet / Popover / Alert / Toast / Coachmark 的 **chrome corner close X**\n * (action group region — corner 可多 action,close 左側加 Separator + refresh / share 等)。\n *\n * 非 dismiss(**不套此 prop**):\n * - Trash / Delete → destructive action,Button 用一般 variant 或 Inline Action(按 row size 判)\n * - Clear → 欄位清空,用 Inline Action\n * - Remove → collection 移除,用一般 Button / Inline Action\n *\n * 詳見 button.spec.md「Dismiss 視覺類」段 + patterns/element-anatomy/inline-action.spec.md\n * 「Dismiss canonical — X close only」段。\n */\n dismiss?: boolean\n /** 載入中狀態:startIcon 替換為 spinner,自動 disabled;badge / endIcon 維持顯示以避免 layout shift */\n loading?: boolean\n /** 撐滿父容器寬度 */\n fullWidth?: boolean\n}\n\n// IconOnly 用 padding-free + aspect-square + flex-center 的 Polaris/Atlassian idiom\n// (M17 SSOT 必可傳播 — 取代 4 個 size 的 magic-number 公式):\n// - aspect-square 鎖 width=height(來自 h-field-X)\n// - p-0 移除 px-3 (label 模式) override\n// - flex justify-center items-center(base 已有)→ SVG 自動視覺置中\n// 結果:0 magic number,0 公式,0 border-deduction,任何 size / icon size 都自然正方形。\n// World-class 對照:Polaris + Atlassian iconOnly 走 padding-free 派(Material/Ant 走\n// padding-based)。我們選 padding-free 因 SSOT 性更強(SegmentedControl / Tag dismiss\n// 等 host 全可共用同 utility class,無需各自抄公式)。詳 button.spec.md「iconOnly 鐵律」。\nconst ICON_ONLY_BASE = 'aspect-square p-0 min-w-0 gap-0'\n\n// code-quality-allow: long-function — foundational composite main body — 拆 sub-fn 會複雜化 local state / ref / context binding\nconst Button = React.forwardRef<HTMLButtonElement, ButtonProps>(\n (\n {\n className,\n variant: variantProp,\n danger: dangerProp,\n size,\n asChild = false,\n startIcon: StartIcon,\n badge,\n overlayBadge,\n endIcon: EndIcon,\n iconOnly = false,\n dismiss = false,\n loading = false,\n fullWidth = false,\n pressed,\n pressedTone, // 2026-06-05 fix(deep-audit P0):原漏 destructure → 落入 ...props 噴到 DOM + 永遠用 cva default\n children,\n disabled,\n ...props\n },\n ref\n ) => {\n // ── FieldContext:在 Field 內自動讀 size,讓 Button 跟 Input 同高(SSOT:useResolvedFieldSize)──\n const resolvedSize = useResolvedFieldSize(size, 'md')\n\n // ── Dismiss 視覺類 override(2026-04-22 cross-implementation dimming canonical) ──\n // dismiss=true 強制:variant=\"text\" + iconOnly=true + icon 色弱化(fg-muted → hover fg-secondary)\n // 跟 Inline Action dismiss 視覺一致。詳見 button.spec.md「Dismiss 視覺類」。\n const resolvedIconOnly = iconOnly || dismiss\n\n // ── Dev-mode warning:overlayBadge 只適用 iconOnly ──\n // 有 label 的 Button 傳入 overlayBadge 會被忽略(只 render icon / 不渲染 overlay slot),\n // 靜默忽略會讓 consumer 誤以為「傳了但位置錯」。Dev mode 印 warning 引導改用 `badge` prop\n // (inline 位置,跟 label 並列)或改 `iconOnly`。Spec SSOT:badge.spec.md「Overlay 適用元件」。\n if (process.env.NODE_ENV !== 'production' && overlayBadge && !iconOnly) {\n // eslint-disable-next-line no-console\n console.warn(\n '[DS Button] `overlayBadge` 只適用於 `iconOnly` Button。有 label 的 Button 請改用 `badge` prop(inline 位置,跟 label 並列),或移除 label 改為 iconOnly。SSOT:badge.spec.md「Overlay 適用元件 canonical」節。'\n )\n }\n\n // 2026-05-23 deep-audit Phase A.4 Decision 1(user verbatim「決策ㄧ照你建議做」):dev-warn `iconOnly + (endIcon|badge)` 並用。\n // Spec SSOT:button.spec.md「iconOnly 嚴格定義為『只有一個 icon,正方形』,不可與 endIcon 或 badge 並用」。\n // 例外:overlayBadge 跟 iconOnly 並用 canonical(上方 overlayBadge 反向 warn 已 cover;相對指法,免行號漂移)。\n // 不擋 image canonical「icon + 下拉指示 = 不加 iconOnly + startIcon + endIcon + aria-label」(那 case iconOnly=false 不 trigger)。\n if (process.env.NODE_ENV !== 'production' && iconOnly && (EndIcon !== undefined || badge != null)) {\n // eslint-disable-next-line no-console\n console.warn(\n '[DS Button] `iconOnly` 嚴格定義為「只有一個 icon,正方形」,不可與 `endIcon` 或 `badge` 並用。若需 icon + 下拉指示 → 不加 iconOnly + startIcon + endIcon + aria-label;若需 icon + 角標 → iconOnly + overlayBadge。SSOT:button.spec.md `iconOnly 的邊界` 節 + `badge.spec.md` Overlay 適用元件 canonical。'\n )\n }\n\n // shadcn compat:AlertDialog、Toast 等元件內部會傳入這些 alias,\n // 在此靜默轉換,不暴露到型別或自動完成。\n // dismiss=true 強制 variant=text(dismiss canonical);override 其他 variant 傳入。\n // 2026-06-06 預設 emphasis 改低(對齊世界級:MUI 預設 text / Ant 預設 default / Polaris 低 emphasis —\n // 預設 primary CTA 是 outlier)。labeled 無 variant → `tertiary`(中性外框,清楚可點性,再按重要程度升 primary);\n // iconOnly 無 variant → `text`(toolbar ghost,對齊 action-bar.spec.md「純動作工具/低重量輔助 → text」+\n // Material 3「icon-only = no fill」)。**CTA 必 explicit `variant=\"primary\"`**(不靠預設)。dismiss 仍強制 text。\n const resolvedVariant: InternalVariant =\n dismiss ? 'text' :\n (variantProp as string) === 'destructive' ? 'primary' :\n (variantProp as string) === 'ghost' ? 'text' :\n (variantProp as InternalVariant) ?? (iconOnly ? 'text' : 'tertiary')\n\n const resolvedDanger = dangerProp || (variantProp as string) === 'destructive'\n\n // Dev-mode warning:danger 僅 primary / secondary / text 有 compoundVariant。\n // tertiary / link + danger 無匹配 compound → **靜默渲染成一般 tertiary / link**(灰、無紅色\n // danger 視覺)— consumer 可能出「不紅的刪除鍵」而不自知。Spec SSOT:button.spec.md\n // 「禁止事項」danger 條(2026-07-14 deep-audit 補 warn,對齊既有 overlayBadge / iconOnly warn pattern)。\n if (process.env.NODE_ENV !== 'production' && resolvedDanger && (resolvedVariant === 'tertiary' || resolvedVariant === 'link')) {\n // eslint-disable-next-line no-console\n console.warn(\n `[DS Button] \\`danger\\` 不支援 variant=\"${resolvedVariant}\"(無對應 compoundVariant,會靜默渲染成一般 ${resolvedVariant},無紅色 danger 視覺)。危險操作請改用 primary(立即不可逆)/ secondary(可反悔)/ text(低度)。SSOT:button.spec.md「禁止事項」danger 條。`\n )\n }\n\n // ButtonGroup context:vertical group 自動注入 fullWidth\n const groupCtx = React.useContext(ButtonGroupContext)\n const resolvedFullWidth = fullWidth || !!groupCtx.fullWidth\n\n const Comp = asChild ? Slot : 'button'\n const iconSize = resolvedSize === 'lg' ? 20 : 16\n\n // loading 行為:spinner 永遠在 prefix 位置\n // 有 prefix icon → icon 換成 spinner(同位置,零 layout shift)\n // 無 prefix icon → spinner 加在文字左邊(按鈕略微變寬,可接受)\n const hasSuffix = badge != null || EndIcon !== undefined\n\n // icon-only 自動 tooltip:從 props 提取 aria-label,同時保留在 DOM\n const { 'aria-label': ariaLabel, ...restProps } = props\n\n // Toggle 狀態:pressed 定義時自動寫入 aria-pressed + data-state。\n // 未定義時不寫入任何 toggle 屬性(按鈕為一般 action button)。\n // 樣式由 cva data-[state=on] compoundVariants(variant × pressedTone)套用——\n // secondary/tertiary/text 預設 pressedTone='emphasis' 走 primary-subtle,\n // pressedTone='neutral' 才走 neutral-selected family;primary/link 不定義 on 分支,傳入無效果。\n const toggleAttrs =\n pressed === undefined\n ? {}\n : { 'aria-pressed': pressed, 'data-state': pressed ? 'on' : 'off' }\n\n // Chrome-unbounded marker(2026-04-22 v5 canonical):button 若無視覺邊界(text variant 或 dismiss),\n // 標記 data-unbounded=\"true\"。SurfaceHeader 透過 [&_[data-unbounded]]:my-[...] 套負 margin\n // 讓 layout 佔位縮到 24(chrome-header-height 幾何)— button native size 與命中區不變\n // (命中 ≡ 可視,見 ds-canonical/references/hit-area-canonical.md;2026-09-24 把原文的\n // 「touch target」正名為命中區 —— 本 DS 以滑鼠精度為前提,尺寸不以觸控門檻推導)。\n // 詳 overlay-surface.spec.md「Chrome dismiss size canonical」\n const unboundedAttr =\n resolvedVariant === 'text' || dismiss ? { 'data-unbounded': 'true' } : {}\n\n // Native path 內部結構:[loading spinner | startIcon(+overlayBadge)] [label] [badge + endIcon]。\n // asChild 時**不渲染**(2026-07-04 fix):Radix Slot 規範 children 必為單一 element\n // (React.Children.only)— 內部多 expression children 變 array 必 runtime throw;\n // asChild 分支只 render consumer child,內容由 consumer 自帶。\n // 詳 CLAUDE.md 失敗記憶索引「asChild ? Slot : Native 內部 JSX 仍渲染多 children」條。\n const nativeChildren = (\n <>\n {loading ? (\n <CircularProgress size={iconSize} className=\"text-current\" />\n ) : StartIcon ? (\n resolvedIconOnly && overlayBadge ? (\n // Overlay badge canonical:wrapper 貼 icon 尺寸,badge 中心對齊 icon top-right corner\n // (Material BadgedBox / iOS App icon),不是 button chrome 角。\n //\n // CSS 細節(2026-04-20 bug fix):用 `inline-block` + 明確 width/height + `leading-none`\n // 避免 `inline-flex` span 在 Button flex container 內被撐高/撐寬(inline-flex 的 span\n // 在某些瀏覽器下會把絕對定位子元素的 translate 計算基準搞錯,造成 badge 噴飛、Button\n // aspect-square 失效)。明確給 span width/height = iconSize 鎖住 positioning context。\n <span\n className=\"relative inline-block leading-none shrink-0 pointer-events-none\"\n style={{ width: iconSize, height: iconSize }}\n >\n <StartIcon size={iconSize} aria-hidden />\n <span className=\"absolute top-0 right-0 translate-x-1/2 -translate-y-1/2 pointer-events-auto\">\n {overlayBadge}\n </span>\n </span>\n ) : (\n <StartIcon size={iconSize} aria-hidden />\n )\n ) : null}\n {children != null && <span className=\"px-1\">{children}</span>}\n {hasSuffix && (\n <span className=\"inline-flex items-center gap-1\">\n {badge}\n {EndIcon && <EndIcon size={iconSize} aria-hidden />}\n </span>\n )}\n </>\n )\n\n const sharedClassName = cn(\n buttonVariants({ variant: resolvedVariant, danger: resolvedDanger, size: resolvedSize, pressedTone, className }),\n // iconOnly 鐵律:padding-free + aspect-square + flex-center (Polaris idiom)\n // 0 magic-number 0 公式自動正方形。詳 ICON_ONLY_BASE rationale。\n resolvedIconOnly && ICON_ONLY_BASE,\n // Dismiss 視覺弱化:override Button text variant 預設 foreground 為 fg-muted → hover fg-secondary\n // 跟 Inline Action dismiss 視覺一致(cross-implementation dimming canonical)\n // 弱化 icon hover 一階(item-anatomy.tsx ItemInlineActionButton 同階梯 SSOT)\n dismiss && 'text-fg-muted hover:text-fg-secondary',\n // aria-disabled 的 dismiss hover 釘在 fg-muted(button.spec.md「狀態疊加」表 aria-disabled 列)\n dismiss && 'aria-disabled:hover:text-fg-muted',\n resolvedFullWidth && 'w-full',\n )\n\n // 2026-07-05 D4 修:asChild 原 silently drop loading/disabled 語意(無 aria-busy/aria-disabled/\n // click guard → <a> loading 期間可重複點擊)。補 ARIA(任何 element 合法;aria-disabled 自動點亮\n // cva 既有視覺)+ functional guard(APG aria-disabled 模式:語意+視覺由元件、功能阻斷由 guard)。\n const { onClick: slotConsumerOnClick, ...slotRest } = restProps as typeof restProps & { onClick?: React.MouseEventHandler<HTMLElement> }\n const buttonEl = asChild ? (\n // Slot 分支:children 必為單一 consumer element(React.Children.only),\n // 內部 slot(loading/startIcon/badge)不渲染;不硬塞 type/disabled(consumer child 可能是 <a>)。\n <Comp\n className={sharedClassName}\n ref={ref}\n aria-label={ariaLabel}\n aria-busy={loading || undefined}\n aria-disabled={disabled || loading || undefined}\n onClick={disabled || loading\n ? (e: React.MouseEvent<HTMLElement>) => { e.preventDefault(); e.stopPropagation() }\n : slotConsumerOnClick}\n {...toggleAttrs}\n {...unboundedAttr}\n {...slotRest}\n >\n {children}\n </Comp>\n ) : (\n <Comp\n className={sharedClassName}\n ref={ref}\n type=\"button\"\n disabled={disabled || loading}\n aria-busy={loading || undefined}\n aria-label={ariaLabel}\n {...toggleAttrs}\n {...unboundedAttr}\n {...restProps}\n >\n {nativeChildren}\n </Comp>\n )\n\n // icon-only + aria-label → 自動包 Tooltip(tooltip 是元件保證的行為)\n // 不建立獨立 TooltipProvider——依賴全域 Provider,\n // 這樣所有 tooltip 共享同一組 delay 參數和 warm-up 機制\n if (resolvedIconOnly && typeof ariaLabel === 'string' && !asChild) {\n return (\n <Tooltip>\n <TooltipTrigger asChild>{buttonEl}</TooltipTrigger>\n <TooltipContent>{ariaLabel}</TooltipContent>\n </Tooltip>\n )\n }\n\n return buttonEl\n }\n)\nButton.displayName = 'Button'\n\n/**\n * componentMeta — Story Auto-Compile 系統消費的結構化 canonical\n * (見 .claude/planning/story-auto-compile.md Phase 1)\n *\n * compile-stories.mjs 讀本 export + spec.md frontmatter 產出\n * anatomy.stories.tsx 的 variant/size/state/token 矩陣 canonical section。\n *\n * Keys 必跟 buttonVariants cva + spec frontmatter 對齊(compile-time 驗證)。\n */\nexport const buttonMeta = {\n component: 'Button',\n family: 3, // Pill Layout\n variants: {\n primary: { purpose: '主要 action / CTA' },\n secondary: { purpose: '次要 action(陪襯 primary)' },\n tertiary: { purpose: '第三級 action(tool-like)' },\n text: { purpose: '文字樣式 action(low emphasis / toolbar)' },\n link: { purpose: '內文連結(inline reading)' },\n },\n sizes: {\n xs: { fieldHeight: 24, iconSize: 16, typography: 'caption' },\n sm: { fieldHeight: 28, iconSize: 16, typography: 'body' },\n md: { fieldHeight: 32, iconSize: 16, typography: 'body' },\n lg: { fieldHeight: 36, iconSize: 20, typography: 'body-lg' },\n },\n // 'pressed' = toggle on 持續態(data-[state=on],emphasis 淡藍底/neutral 灰底;2026-07-07 meta 詞彙統一補列)\n // 'loading' = CircularProgress spinner 取代 startIcon + aria-busy + click guard(2026-07-14 deep-audit\n // 補列,原漏 → auto-compile 生成矩陣缺 loading;對齊 anatomy StateBehavior Loading 行 + tsx loading 分支)\n states: ['default', 'hover', 'active', 'pressed', 'loading', 'focus-visible', 'disabled'],\n tokens: {\n // 2026-07-04 audit 補齊:原只列 primary variant 家族;按 buttonVariants cva 真實 class 補\n // secondary/tertiary/text/link/danger 消費的 token(auto-compile token 矩陣依此)。\n // 2026-07-14 deep-audit 二次補齊(掃 cva 全 bg-* class):+ neutral-selected-hover / neutral-selected-active\n // (neutral pressedTone compound 消費)+ error / error-hover / error-active(primary+danger bg-error 系)\n bg: ['--primary', '--primary-hover', '--primary-active', '--primary-subtle', '--bg-disabled', '--surface', '--neutral-hover', '--neutral-active', '--neutral-selected', '--neutral-selected-hover', '--neutral-selected-active', '--error', '--error-hover', '--error-active'],\n fg: ['--on-emphasis', '--fg-disabled', '--foreground', '--primary', '--primary-hover', '--primary-active', '--error', '--error-hover', '--error-active'],\n ring: ['--ring'],\n },\n defaultVariant: 'tertiary', // 2026-06-10 修 stale:對齊 cva defaultVariants(2026-06-06 labeled 預設改 tertiary,meta 漏同步)\n defaultSize: 'md',\n} as const\n\nexport { Button, buttonVariants, ButtonGroupContext }\n"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"button.js","sources":["../../../src/components/Button/button.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(Button + iconOnly + danger + loading + asChild + dismiss + pressedTone)— 跨 7 axis variant 集中 SSOT 一處,拆分會 fragment cva variant catalog。當前 528 < cap 800。\nimport * as React from 'react'\nimport { Slot } from '@radix-ui/react-slot'\nimport { cva, type VariantProps } from 'class-variance-authority'\nimport { CircularProgress } from '@/design-system/components/CircularProgress/circular-progress'\nimport type { LucideIcon } from 'lucide-react'\nimport { cn } from '@/lib/utils'\nimport { useResolvedFieldSize } from '@/design-system/components/Field/field-context'\nimport { Tooltip, TooltipContent, TooltipTrigger } from '@/design-system/components/Tooltip/tooltip'\n\n/**\n * Button — shadcn 風格,橋接設計系統 token\n *\n * ── Variants ──\n * primary 主要操作,藍底白字\n * secondary 次要品牌操作,藍框藍字;正面 vs 負面並存時用於正面那個\n * tertiary 一般輔助操作,灰框灰字,hover 轉藍(最常用的非主要按鈕)\n * text 無底色無邊框,hover 顯示灰底(工具列、密集 UI)\n * link 外觀像連結的按鈕(本質仍是 button)\n *\n * ── danger prop ──\n * danger 套用危險色(紅色);僅 primary / secondary / text 支援(tertiary / link 不支援,見 spec 禁止事項)\n *\n * <Button variant=\"primary\" danger>永久刪除</Button> → 紅底白字(立即不可逆)\n * <Button variant=\"secondary\" danger>移至垃圾桶</Button> → 紅框紅字(點下去還可反悔)\n *\n * ── pressed prop(toggle)──\n * pressed Toggle 按下狀態(持續 on/off),寫入 aria-pressed + data-state\n * 僅 secondary / tertiary / text 三個 variant 支援 toggle 視覺:\n * - secondary + pressed → primary-subtle 底、primary 字、透明邊框\n * - tertiary + pressed → primary-subtle 底、primary 字、透明邊框(同 secondary 按下視覺)\n * - text + pressed → primary-subtle 底、primary 字、透明邊框(預設 pressedTone='emphasis',同 secondary/tertiary 按下視覺);pressedTone='neutral' 時才走 neutral-selected 灰底\n * primary / link 傳入 pressed 無視覺效果(語意不符)\n *\n * ── Sizes(預設 md)──\n * xs h-field-xs(24px 固定),不隨 density 縮放\n * sm h-field-sm,md=28px / lg=32px\n * md h-field-md,md=32px / lg=36px ← 預設(跟 Field/Input 對齊)\n * lg h-field-lg,md=36px / lg=40px\n * icon-only 不是獨立尺寸 — 加 iconOnly prop 讓任何尺寸變正方形\n *\n * ── 內部結構 ──\n * [startIcon?] [label] [badge? + endIcon?]\n *\n * ── 用法範例 ──\n * <Button startIcon={Plus}>新增</Button>\n * <Button variant=\"tertiary\">取消</Button>\n * <Button variant=\"primary\" danger>永久刪除</Button>\n * <Button variant=\"text\" pressed={isPinned} startIcon={Pin} aria-label=\"釘選\" iconOnly />\n * <Button badge={<Badge count={3} />} endIcon={ChevronDown}>通知</Button>\n * <Button size=\"sm\" iconOnly startIcon={Plus} aria-label=\"新增\" />\n * <Button iconOnly startIcon={Bell} aria-label=\"通知 (3 則)\"\n * overlayBadge={<Badge count={3} />} /> ← badge 自動貼 icon 右上角\n *\n * ── asChild ──\n * <Button asChild><Link to=\"/home\">回首頁</Link></Button>\n */\nconst buttonVariants = cva(\n [\n 'inline-flex items-center justify-center',\n 'whitespace-nowrap font-medium',\n 'border border-transparent',\n // hover 底色瞬間切換,不做過渡(user 2026-09-10 拍板「第三題改成全部瞬間」;SSOT = tokens/motion/motion.spec.md「hover 回饋不做過渡」)\n 'cursor-pointer select-none disabled:cursor-not-allowed aria-disabled:cursor-not-allowed',\n // 2026-05-12 Round 4.5 fix(codex M31 Layer C 抓):`aria-disabled` visual 分支補(per WAI-ARIA APG —\n // aria-disabled 給語意 + visual,但不 suppress functionality;functionality 由 consumer 阻 e.g.\n // RowDragHandle listeners 只在 canDrag spread)。**故意不加** `aria-disabled:pointer-events-none`\n // — Round 4 RowDragHandle Tooltip flicker fix root cause:aria-disabled buttons 必保 pointer events\n // 讓 Radix Tooltip pointerenter 通過。\n // 2026-09-29(待辦總帳 N46 停用游標全站掃):原生 `disabled` 原本也掛 `pointer-events-none`(「完全 inactive」),\n // 結果上面那句 `disabled:cursor-not-allowed` 從來沒生效 —— 沒有指標事件就沒有游標,全站 35 顆停用的 Button 家族\n //(分頁箭頭 / 輪播箭頭 / AI 面板送出鈕)都是箭頭游標,違反 hit-area-canonical.md「停用的用禁止符號」。\n // 現在停用不再切掉指標事件;滑過 / 按住的回饋改由各 variant 的 `disabled:hover:` / `disabled:active:` 顯式釘在停用靜止值\n //(同 aria-disabled 的寫法;原生 disabled 的按鈕本來就不派發 click)。\n // 2026-05-12 fix v2(playwright pixel-quantified verify 抓 opacity=1 不生效):\n // `opacity-disabled` 是 custom Tailwind v4 `@utility`(opacity.css:21),variant prefix\n // `aria-disabled:` 跟 custom @utility 不 compose(`aria-disabled:cursor-not-allowed` 標準\n // utility 能 work,custom 不行)。改 arbitrary value `opacity-[var(--opacity-disabled)]`\n // 繞 custom @utility 限制(Tailwind v4 arbitrary value 直接生 `opacity: var(--opacity-disabled)`)。\n 'aria-disabled:opacity-[var(--opacity-disabled)]',\n 'rounded-md',\n // Defensive:SVG 不被 flex shrink 擠扁(防 inner-area 計算誤差導致 icon 被擠成\n // width<intrinsic 的 asymmetric 顯示)。詳 ICON_ONLY_PX 段 rationale。\n '[&_svg]:shrink-0',\n ],\n {\n variants: {\n variant: {\n primary: [\n 'bg-primary text-on-emphasis',\n 'hover:bg-primary-hover',\n 'active:bg-primary-active',\n // 開啟中 = 自己的 hover(inline-action.spec.md:26「overlay 開啟 → 同 host hover」)。2026-09-29 待辦總帳 C13:\n // 七家一手裡沒有任何一家「給部分 variant、獨獨豁免 primary」(Atlassian / Fluent 每個 appearance 含 primary 都給;\n // Radix / shadcn / MUI / Ant / Carbon 全都不給);DS 早選了「給」,primary / link 缺的只是同一條規則的落地。\n 'data-[state=open]:bg-primary-hover',\n // aria-disabled 的 hover / active 釘在靜止(button.spec.md「狀態疊加」表 aria-disabled 列:與 disabled 同樣不給互動回饋;\n // 原生 disabled 與 aria-disabled 都顯式釘住(2026-09-29 前原生 disabled 靠 pointer-events-none,見檔頭);先例 switch.tsx 的 `disabled:…:hover:` 守衛)\n 'aria-disabled:hover:bg-primary aria-disabled:active:bg-primary',\n 'disabled:bg-disabled disabled:text-fg-disabled disabled:border-transparent',\n 'disabled:hover:bg-disabled disabled:active:bg-disabled',\n ],\n secondary: [\n 'bg-surface text-primary border-primary',\n 'hover:text-primary-hover hover:border-primary-hover',\n 'active:text-primary-active active:border-primary-active',\n // Overlay trigger active(asChild Popover/DropdownMenu trigger)— 維持 hover 樣式\n // (canonical 對齊 inline-action.spec.md:trigger 維持 host hover 直到 overlay 關閉)\n 'data-[state=open]:text-primary-hover data-[state=open]:border-primary-hover',\n // aria-disabled 釘住(同 primary 註解);`not-aria-pressed` = 只釘「未按下」的靜止,按下的由 pressed compound 自己釘(避免兩組 (0,3,0) 靠產出順序決勝)\n 'aria-disabled:not-aria-pressed:hover:text-primary aria-disabled:not-aria-pressed:hover:border-primary',\n 'aria-disabled:not-aria-pressed:active:text-primary aria-disabled:not-aria-pressed:active:border-primary',\n 'disabled:bg-transparent disabled:text-fg-disabled disabled:border-border',\n 'disabled:hover:text-fg-disabled disabled:hover:border-border disabled:active:text-fg-disabled disabled:active:border-border',\n // 2026-05-21 v12:Toggle pressed 視覺移到 compoundVariants(variant × pressedTone),\n // 同時支援 emphasis(藍底)/ neutral(灰底)兩 tone。詳 cva.compoundVariants 段。\n ],\n tertiary: [\n 'bg-surface text-foreground border-border',\n 'hover:text-primary-hover hover:border-primary-hover',\n 'active:text-primary-active active:border-primary-active',\n // Overlay trigger active — 維持 hover 樣式(同 secondary 邏輯)\n 'data-[state=open]:text-primary-hover data-[state=open]:border-primary-hover',\n // aria-disabled 釘住(同 secondary 註解)\n 'aria-disabled:not-aria-pressed:hover:text-foreground aria-disabled:not-aria-pressed:hover:border-border',\n 'aria-disabled:not-aria-pressed:active:text-foreground aria-disabled:not-aria-pressed:active:border-border',\n 'disabled:bg-transparent disabled:text-fg-disabled disabled:border-border',\n 'disabled:hover:text-fg-disabled disabled:hover:border-border disabled:active:text-fg-disabled disabled:active:border-border',\n ],\n text: [\n 'bg-transparent text-foreground border-transparent',\n 'hover:bg-neutral-hover',\n 'active:bg-neutral-active',\n // Overlay trigger active — 維持 hover 樣式(canonical 2026-05-02 改:hover token,\n // 不另開 selected 4% — 對齊 shadcn/Radix/Material 狀態極簡派,跨 host 一致)\n 'data-[state=open]:bg-neutral-hover',\n // aria-disabled 釘住(同 secondary 註解)\n 'aria-disabled:not-aria-pressed:hover:bg-transparent aria-disabled:not-aria-pressed:active:bg-transparent',\n 'disabled:bg-transparent disabled:text-fg-disabled',\n 'disabled:hover:bg-transparent disabled:hover:text-fg-disabled disabled:active:bg-transparent disabled:active:text-fg-disabled',\n ],\n link: [\n 'bg-transparent text-primary border-transparent',\n 'hover:text-primary-hover',\n 'active:text-primary-active',\n // 開啟中 = 自己的 hover(link 的 hover 只換字色;同 primary 分支註解,2026-09-29 C13)\n 'data-[state=open]:text-primary-hover',\n // aria-disabled 釘住(同 primary 註解;link 無 pressed 視覺,不需 not-aria-pressed)\n 'aria-disabled:hover:text-primary aria-disabled:active:text-primary',\n 'disabled:text-fg-disabled',\n 'disabled:hover:text-fg-disabled disabled:active:text-fg-disabled',\n ],\n },\n danger: {\n true: '', // 實際樣式由 compoundVariants 提供\n },\n /**\n * 2026-05-21 v12 — pressed visual tone(per user「我認同這一個方向,然後預設emphasis」):\n * emphasis = 淡藍底(toolbar functional toggle / 篩選啟用 / 面板開關 — Figma toolbar /\n * Linear toolbar / Material ToggleButton 共識)\n * neutral = 灰底(**可取消**的切換鈕、要比藍底低一級時;不用於導覽 / 目前頁 —— 那是選中列,\n * owner = item-anatomy.spec.md「選中 × 互動疊加」;button.spec.md pressedTone 表,待辦總帳 B5 2026-09-25 更正)\n * 預設 `emphasis` per user directive。實際樣式由 compoundVariants(variant × pressedTone)套用。\n * 只在 secondary / tertiary / text variant 觸發 toggle 視覺;primary / link 無視覺效果。\n */\n pressedTone: {\n emphasis: '',\n neutral: '',\n },\n size: {\n xs: 'h-field-xs px-2 text-caption leading-compact gap-0',\n sm: 'h-field-sm px-3 min-w-14 text-body leading-compact gap-1',\n md: 'h-field-md px-3 min-w-16 text-body leading-compact gap-1',\n lg: 'h-field-lg px-3 min-w-20 text-body-lg leading-compact gap-1',\n },\n },\n compoundVariants: [\n // primary + danger → 紅底白字(立即不可逆操作)\n {\n variant: 'primary',\n danger: true,\n class: [\n 'bg-error text-on-emphasis border-transparent',\n 'hover:bg-error-hover',\n 'active:bg-error-active',\n // 開啟中 = 自己的 hover(同 primary 分支,2026-09-29 C13)\n 'data-[state=open]:bg-error-hover',\n // aria-disabled 釘在自己的靜止紅(button.spec.md「狀態疊加」表 aria-disabled 列)\n 'aria-disabled:hover:bg-error aria-disabled:active:bg-error',\n ],\n },\n // secondary + danger → 紅框紅字(有確認步驟的危險操作)\n {\n variant: 'secondary',\n danger: true,\n class: [\n 'bg-surface text-error border-error',\n 'hover:text-error-hover hover:border-error-hover',\n 'active:text-error-active active:border-error-active',\n // 開啟中 = 自己的 hover(error-hover),不是 secondary 的 primary-hover(inline-action.spec.md「overlay 開啟 → 同 host hover」;\n // button.spec.md「狀態疊加」表開啟中列)。靠 cn()/twMerge 去掉 variant 那組同前綴 class,同本檔 danger 覆寫慣例\n 'data-[state=open]:text-error-hover data-[state=open]:border-error-hover',\n 'aria-disabled:not-aria-pressed:hover:text-error aria-disabled:not-aria-pressed:hover:border-error',\n 'aria-disabled:not-aria-pressed:active:text-error aria-disabled:not-aria-pressed:active:border-error',\n ],\n },\n // text + danger → 紅字,hover 灰底\n {\n variant: 'text',\n danger: true,\n class: [\n 'text-error',\n 'hover:bg-neutral-hover hover:text-error-hover',\n 'active:bg-neutral-active active:text-error-active',\n // 開啟中 = 自己的 hover 配對:底色沿用 text 的 data-[state=open]:bg-neutral-hover,字補上 error-hover(同 secondary+danger 註解)\n 'data-[state=open]:text-error-hover',\n 'aria-disabled:not-aria-pressed:hover:text-error aria-disabled:not-aria-pressed:active:text-error',\n ],\n },\n // ── 2026-05-21 v12 Toggle pressed(variant × pressedTone)──────────────────\n // 視覺由 data-[state=on] + aria-pressed(Radix overlay trigger fallback)觸發\n //\n // emphasis tone:藍底(primary-subtle / primary 字)— functional toggle\n {\n variant: ['secondary', 'tertiary', 'text'],\n pressedTone: 'emphasis',\n class: [\n 'data-[state=on]:bg-primary-subtle data-[state=on]:text-primary data-[state=on]:border-transparent',\n // 按下的 hover / active 只換字(primary-hover / -active),底色與邊框顯式釘住 —— 照 sidebar.tsx\n // `data-[active=true]:hover:bg-neutral-selected` 寫法,釘住 (0,3,0) > variant 的 hover / active (0,2,0)。\n // 之前底色只是靠 CSS 產出順序(data-* 排在 hover 之後)才沒被 text 的 neutral-hover 換掉(button.spec.md「狀態疊加」表 pressed × hover / active 列)\n 'data-[state=on]:hover:bg-primary-subtle data-[state=on]:hover:border-transparent data-[state=on]:hover:text-primary-hover',\n 'data-[state=on]:active:bg-primary-subtle data-[state=on]:active:border-transparent data-[state=on]:active:text-primary-active',\n 'data-[state=on]:disabled:bg-disabled data-[state=on]:disabled:text-fg-disabled data-[state=on]:disabled:border-transparent',\n 'data-[state=on]:disabled:hover:bg-disabled data-[state=on]:disabled:hover:text-fg-disabled data-[state=on]:disabled:hover:border-transparent data-[state=on]:disabled:active:bg-disabled data-[state=on]:disabled:active:text-fg-disabled data-[state=on]:disabled:active:border-transparent',\n // aria-pressed fallback(Radix overlay trigger override data-state 時仍生效)—— 逐條鏡像上方 data-[state=on] 分支\n 'aria-pressed:bg-primary-subtle aria-pressed:text-primary aria-pressed:border-transparent',\n 'aria-pressed:hover:bg-primary-subtle aria-pressed:hover:border-transparent aria-pressed:hover:text-primary-hover',\n // 2026-07-07 補齊第三階:fallback 分支與 data-[state=on] 分支同拼寫(選中之上按壓 = active 階)\n 'aria-pressed:active:bg-primary-subtle aria-pressed:active:border-transparent aria-pressed:active:text-primary-active',\n // pressed + disabled:disabled 視覺優先(button.spec.md「狀態 → disabled」pressed + disabled 條);fallback 分支原本缺這條,鏡像 data-[state=on]:disabled\n 'aria-pressed:disabled:bg-disabled aria-pressed:disabled:text-fg-disabled aria-pressed:disabled:border-transparent',\n 'aria-pressed:disabled:hover:bg-disabled aria-pressed:disabled:hover:text-fg-disabled aria-pressed:disabled:hover:border-transparent aria-pressed:disabled:active:bg-disabled aria-pressed:disabled:active:text-fg-disabled aria-pressed:disabled:active:border-transparent',\n // 按下的觸發鈕開啟中 = 按下自己的 hover(底色與邊框不動、字 primary-hover),不是 variant 的開啟樣式\n //(inline-action.spec.md「overlay 開啟 → 同 host hover」;button.spec.md「狀態疊加」表開啟中列)。\n // Radix 會把 data-state 改寫成 open,所以只走 aria-pressed;(0,3,0) > variant 的 data-[state=open] (0,2,0)\n 'aria-pressed:data-[state=open]:bg-primary-subtle aria-pressed:data-[state=open]:border-transparent aria-pressed:data-[state=open]:text-primary-hover',\n // 按下 + aria-disabled:hover / active 釘在按下的靜止字色(底色與邊框已由上方釘住)\n 'data-[state=on]:aria-disabled:hover:text-primary data-[state=on]:aria-disabled:active:text-primary',\n 'aria-pressed:aria-disabled:hover:text-primary aria-pressed:aria-disabled:active:text-primary',\n ],\n },\n // neutral tone:灰底(neutral-selected family)— 可取消的切換鈕;導覽 / 目前頁不用它(選中列規則,item-anatomy.spec.md「選中 × 互動疊加」)\n // 階梯值 selected → -selected-hover → -selected-active(2→3→4):user 2026-09-25 選「甲:保留,維持 2→3→4 (Recommended)」\n // (選項由 AI 提供;待辦總帳 B5)。下方開啟中 / aria-disabled 各條都是「跟著本分支的 hover / 靜止 token 走」,改階梯時同一 compound 內一起改。\n // 2026-09-29(待辦總帳 N7):secondary 從這一組拆出去(下一組)—— 它的靜止底是 `--surface`(「底」),已按下要**疊層**不換底:\n // 深色 --surface 與 --neutral-selected 都是白 8%,換底後已按下 = 未按下(#1E1E1E = #1E1E1E);tertiary / text 是透明底,換底沒問題。\n {\n variant: ['tertiary', 'text'],\n pressedTone: 'neutral',\n class: [\n 'data-[state=on]:bg-neutral-selected data-[state=on]:text-foreground data-[state=on]:border-transparent',\n // 字與邊框在 hover / active 顯式釘住(secondary / tertiary 的 hover 會把字與框轉 primary;同 emphasis 分支註解)\n 'data-[state=on]:hover:bg-neutral-selected-hover data-[state=on]:hover:text-foreground data-[state=on]:hover:border-transparent',\n 'data-[state=on]:active:bg-neutral-selected-active data-[state=on]:active:text-foreground data-[state=on]:active:border-transparent',\n 'data-[state=on]:disabled:bg-transparent data-[state=on]:disabled:text-fg-disabled',\n 'data-[state=on]:disabled:hover:bg-transparent data-[state=on]:disabled:hover:text-fg-disabled data-[state=on]:disabled:active:bg-transparent data-[state=on]:disabled:active:text-fg-disabled',\n // aria-pressed fallback —— 逐條鏡像上方 data-[state=on] 分支\n 'aria-pressed:bg-neutral-selected aria-pressed:text-foreground aria-pressed:border-transparent',\n 'aria-pressed:hover:bg-neutral-selected-hover aria-pressed:hover:text-foreground aria-pressed:hover:border-transparent',\n // 補齊第三階(按壓):fallback 原本缺,data-[state=on] 分支與 emphasis 分支都有(同 emphasis 2026-07-07 註解)\n 'aria-pressed:active:bg-neutral-selected-active aria-pressed:active:text-foreground aria-pressed:active:border-transparent',\n 'aria-pressed:disabled:bg-transparent aria-pressed:disabled:text-fg-disabled',\n 'aria-pressed:disabled:hover:bg-transparent aria-pressed:disabled:hover:text-fg-disabled aria-pressed:disabled:active:bg-transparent aria-pressed:disabled:active:text-fg-disabled',\n // 按下的觸發鈕開啟中 = 按下自己的 hover(neutral-selected-hover),不是 variant 的 neutral-hover / primary-hover(同 emphasis 分支註解)\n 'aria-pressed:data-[state=open]:bg-neutral-selected-hover aria-pressed:data-[state=open]:text-foreground aria-pressed:data-[state=open]:border-transparent',\n // 按下 + aria-disabled:hover / active 釘在按下的靜止底色\n 'data-[state=on]:aria-disabled:hover:bg-neutral-selected data-[state=on]:aria-disabled:active:bg-neutral-selected',\n 'aria-pressed:aria-disabled:hover:bg-neutral-selected aria-pressed:aria-disabled:active:bg-neutral-selected',\n ],\n },\n // secondary(白底)× neutral:底留 `bg-surface`,已按下三階用疊層 utility(semantic.css `bg-interaction-selected` 家族 =\n // 把 --neutral-selected / -hover / -active 畫成 background-image,在底色之上、內容之下;配對總則第 2 列「底不換,疊一層」)。\n // 淺色疊層結果與換底相同(#F5F5F5 / #F0F0F0 / #E8E8E8),深色才分得出來(#303030 / #37 / #3D)。逐條鏡像上一組;\n // 停用態拿掉疊層(`bg-none`)而不是把底換成透明 —— 白底鈕停用仍是白底。\n {\n variant: 'secondary',\n pressedTone: 'neutral',\n class: [\n // 底寫明 `bg-surface`(與 variant 相同,不換底):疊層是疊在「底」上,hover-own-pair 閘據此判定,不必去別的 compound 找靜止底\n 'data-[state=on]:bg-surface data-[state=on]:bg-interaction-selected data-[state=on]:text-foreground data-[state=on]:border-transparent',\n 'data-[state=on]:hover:bg-interaction-selected-hover data-[state=on]:hover:text-foreground data-[state=on]:hover:border-transparent',\n 'data-[state=on]:active:bg-interaction-selected-active data-[state=on]:active:text-foreground data-[state=on]:active:border-transparent',\n 'data-[state=on]:disabled:bg-none data-[state=on]:disabled:text-fg-disabled',\n 'data-[state=on]:disabled:hover:bg-none data-[state=on]:disabled:hover:text-fg-disabled data-[state=on]:disabled:active:bg-none data-[state=on]:disabled:active:text-fg-disabled',\n 'aria-pressed:bg-surface aria-pressed:bg-interaction-selected aria-pressed:text-foreground aria-pressed:border-transparent',\n 'aria-pressed:hover:bg-interaction-selected-hover aria-pressed:hover:text-foreground aria-pressed:hover:border-transparent',\n 'aria-pressed:active:bg-interaction-selected-active aria-pressed:active:text-foreground aria-pressed:active:border-transparent',\n 'aria-pressed:disabled:bg-none aria-pressed:disabled:text-fg-disabled',\n 'aria-pressed:disabled:hover:bg-none aria-pressed:disabled:hover:text-fg-disabled aria-pressed:disabled:active:bg-none aria-pressed:disabled:active:text-fg-disabled',\n 'aria-pressed:data-[state=open]:bg-interaction-selected-hover aria-pressed:data-[state=open]:text-foreground aria-pressed:data-[state=open]:border-transparent',\n 'data-[state=on]:aria-disabled:hover:bg-interaction-selected data-[state=on]:aria-disabled:active:bg-interaction-selected',\n 'aria-pressed:aria-disabled:hover:bg-interaction-selected aria-pressed:aria-disabled:active:bg-interaction-selected',\n ],\n },\n ],\n defaultVariants: {\n // labeled 預設 = tertiary(2026-06-06 從 primary 改;iconOnly 預設 = text,由 resolvedVariant 注入)。\n // 真正預設邏輯在元件內 resolvedVariant(相對指法,免行號漂移);此 cva default 供直接 buttonVariants() 呼叫者一致。\n variant: 'tertiary',\n size: 'md',\n pressedTone: 'emphasis',\n },\n }\n)\n\n// ── ButtonGroup Context ──────────────────────────────────────────────────────\n// ButtonGroup 提供此 context;Button 讀取它注入 fullWidth。\n// Context 放本檔(不放 button-group.tsx)以避免循環 import。\ninterface ButtonGroupContextValue {\n fullWidth?: boolean\n}\n/** @internal — ButtonGroup↔Button 私有 context(fullWidth 注入);consumer 不直接 import,經 <ButtonGroup> 消費。root barrel 排除(subpath 仍可 wrap 後用)。 */\nconst ButtonGroupContext = React.createContext<ButtonGroupContextValue>({})\n\ntype InternalVariant = VariantProps<typeof buttonVariants>['variant']\n\nexport interface ButtonProps\n extends React.ButtonHTMLAttributes<HTMLButtonElement>,\n Omit<VariantProps<typeof buttonVariants>, 'variant' | 'danger'> {\n /**\n * 將樣式套用至子元件(e.g. React Router Link)。\n *\n * ⚠️ **icon-only + asChild 警告**:當 `iconOnly={true}` 且 `asChild={true}` 時,內建\n * Tooltip wrapper 不啟動(Radix Slot 不接受多 child)。consumer 必須**自管 child 的 `aria-label`**\n * 給 screen reader,並視需要自行包 `<Tooltip>`(對齊 Polaris / Radix asChild idiom)。\n * Button 不會主動補 tooltip — 否則會破壞 Slot 單 child 規則。\n */\n asChild?: boolean\n /**\n * 按鈕視覺強調等級。\n * `destructive` / `ghost` 為 shadcn 內部 compat,請勿在應用程式碼中直接使用。\n */\n variant?: 'primary' | 'secondary' | 'tertiary' | 'text' | 'link'\n /** 套用危險色(紅色)。僅 primary / secondary / text 支援(tertiary / link 不支援,見 spec 禁止事項)。 */\n danger?: boolean\n /**\n * Toggle 按下狀態(持續 on/off)。設定時 Button 變為 toggle:\n * - 自動寫入 `aria-pressed` + `data-state=\"on\" | \"off\"`\n * - 樣式由 variant × `pressedTone` 的 compoundVariants 套用\n * - 僅 secondary / tertiary / text 有 toggle 視覺;primary / link 傳入無效果\n *\n * 不傳此 prop 時 Button 就是一般按鈕,不帶 aria-pressed。\n */\n pressed?: boolean\n /**\n * Pressed 視覺色調(2026-05-21 v12 加):\n * - `'emphasis'`(預設)→ 淡藍底 / primary 字(對齊 Figma toolbar / Linear toolbar /\n * Material ToggleButton 共識 — toolbar functional toggle / 篩選啟用 / 面板開關)\n * - `'neutral'` → 灰底 / foreground 字 —— **可取消**的切換鈕(再點一次就關掉)需要低一級強調時;\n * 導覽 / 目前頁不用它(那是選中列,滑過釘住;button.spec.md pressedTone 表,2026-09-25 更正)\n *\n * 僅在 `pressed` 啟用且 variant ∈ {secondary, tertiary, text} 時生效。\n * 跨 toggle context 維持單一 prop API,consumer 視語意選 tone 不另開 variant。\n */\n pressedTone?: 'emphasis' | 'neutral'\n /** 左側 icon(LucideIcon),最多一個,loading 時自動替換為 spinner */\n startIcon?: LucideIcon\n /** 右側 badge(ReactNode),通常傳入計數指示器 */\n badge?: React.ReactNode\n /**\n * Overlay badge(iconOnly 專用)。接收 `<Badge>` 元素,Button 內部**自動定位在 startIcon 右上角**——\n * badge 中心對齊 icon 的 top-right corner(Material BadgedBox / iOS App icon canonical),不是按鈕邊緣。\n * 解決手刻 `relative + absolute -top-1 -right-1` 讓 badge 飄在按鈕 chrome 右上的問題。\n *\n * 世界級對照:Material BadgedBox、iOS App Icon、Ant Badge wrap icon,badge 相對於**視覺重心**(icon)。\n * 只在 `iconOnly=true` 時生效;非 iconOnly 時應該用 inline `badge` prop 放 suffix 位置。\n */\n overlayBadge?: React.ReactNode\n /** 右側 icon(LucideIcon),放在 badge 右邊,通常用於 ChevronDown 等方向指示 */\n endIcon?: LucideIcon\n /** Icon-only 模式:移除 padding,變為正方形(必須同時設定 aria-label) */\n iconOnly?: boolean\n /**\n * Dismiss 視覺類(X close only canonical)。專用於 **X(close)icon 的 dismiss 語意** —\n * 「關閉 surface / 忽略訊息」。**不適用 Trash / Delete / Clear / Remove 等 destructive / clear 操作**。\n *\n * 自動套用:\n * - `variant=\"text\"`(強制 override 其他 variant)\n * - `iconOnly=true`(強制)\n * - Icon 色 override:`fg-muted` → hover `fg-secondary`(跟 Inline Action dismiss 視覺一致)\n *\n * 典型 case:Dialog / Sheet / Popover / Alert / Toast / Coachmark 的 **chrome corner close X**\n * (action group region — corner 可多 action,close 左側加 Separator + refresh / share 等)。\n *\n * 非 dismiss(**不套此 prop**):\n * - Trash / Delete → destructive action,Button 用一般 variant 或 Inline Action(按 row size 判)\n * - Clear → 欄位清空,用 Inline Action\n * - Remove → collection 移除,用一般 Button / Inline Action\n *\n * 詳見 button.spec.md「Dismiss 視覺類」段 + patterns/element-anatomy/inline-action.spec.md\n * 「Dismiss canonical — X close only」段。\n */\n dismiss?: boolean\n /** 載入中狀態:startIcon 替換為 spinner,自動 disabled;badge / endIcon 維持顯示以避免 layout shift */\n loading?: boolean\n /** 撐滿父容器寬度 */\n fullWidth?: boolean\n}\n\n// IconOnly 用 padding-free + aspect-square + flex-center 的 Polaris/Atlassian idiom\n// (M17 SSOT 必可傳播 — 取代 4 個 size 的 magic-number 公式):\n// - aspect-square 鎖 width=height(來自 h-field-X)\n// - p-0 移除 px-3 (label 模式) override\n// - flex justify-center items-center(base 已有)→ SVG 自動視覺置中\n// 結果:0 magic number,0 公式,0 border-deduction,任何 size / icon size 都自然正方形。\n// World-class 對照:Polaris + Atlassian iconOnly 走 padding-free 派(Material/Ant 走\n// padding-based)。我們選 padding-free 因 SSOT 性更強(SegmentedControl / Tag dismiss\n// 等 host 全可共用同 utility class,無需各自抄公式)。詳 button.spec.md「iconOnly 鐵律」。\nconst ICON_ONLY_BASE = 'aspect-square p-0 min-w-0 gap-0'\n\n// code-quality-allow: long-function — foundational composite main body — 拆 sub-fn 會複雜化 local state / ref / context binding\nconst Button = React.forwardRef<HTMLButtonElement, ButtonProps>(\n (\n {\n className,\n variant: variantProp,\n danger: dangerProp,\n size,\n asChild = false,\n startIcon: StartIcon,\n badge,\n overlayBadge,\n endIcon: EndIcon,\n iconOnly = false,\n dismiss = false,\n loading = false,\n fullWidth = false,\n pressed,\n pressedTone, // 2026-06-05 fix(deep-audit P0):原漏 destructure → 落入 ...props 噴到 DOM + 永遠用 cva default\n children,\n disabled,\n ...props\n },\n ref\n ) => {\n // ── FieldContext:在 Field 內自動讀 size,讓 Button 跟 Input 同高(SSOT:useResolvedFieldSize)──\n const resolvedSize = useResolvedFieldSize(size, 'md')\n\n // ── Dismiss 視覺類 override(2026-04-22 cross-implementation dimming canonical) ──\n // dismiss=true 強制:variant=\"text\" + iconOnly=true + icon 色弱化(fg-muted → hover fg-secondary)\n // 跟 Inline Action dismiss 視覺一致。詳見 button.spec.md「Dismiss 視覺類」。\n const resolvedIconOnly = iconOnly || dismiss\n\n // ── Dev-mode warning:overlayBadge 只適用 iconOnly ──\n // 有 label 的 Button 傳入 overlayBadge 會被忽略(只 render icon / 不渲染 overlay slot),\n // 靜默忽略會讓 consumer 誤以為「傳了但位置錯」。Dev mode 印 warning 引導改用 `badge` prop\n // (inline 位置,跟 label 並列)或改 `iconOnly`。Spec SSOT:badge.spec.md「Overlay 適用元件」。\n if (process.env.NODE_ENV !== 'production' && overlayBadge && !iconOnly) {\n console.warn(\n '[DS Button] `overlayBadge` 只適用於 `iconOnly` Button。有 label 的 Button 請改用 `badge` prop(inline 位置,跟 label 並列),或移除 label 改為 iconOnly。SSOT:badge.spec.md「Overlay 適用元件 canonical」節。'\n )\n }\n\n // 2026-05-23 deep-audit Phase A.4 Decision 1(user verbatim「決策ㄧ照你建議做」):dev-warn `iconOnly + (endIcon|badge)` 並用。\n // Spec SSOT:button.spec.md「iconOnly 嚴格定義為『只有一個 icon,正方形』,不可與 endIcon 或 badge 並用」。\n // 例外:overlayBadge 跟 iconOnly 並用 canonical(上方 overlayBadge 反向 warn 已 cover;相對指法,免行號漂移)。\n // 不擋 image canonical「icon + 下拉指示 = 不加 iconOnly + startIcon + endIcon + aria-label」(那 case iconOnly=false 不 trigger)。\n if (process.env.NODE_ENV !== 'production' && iconOnly && (EndIcon !== undefined || badge != null)) {\n console.warn(\n '[DS Button] `iconOnly` 嚴格定義為「只有一個 icon,正方形」,不可與 `endIcon` 或 `badge` 並用。若需 icon + 下拉指示 → 不加 iconOnly + startIcon + endIcon + aria-label;若需 icon + 角標 → iconOnly + overlayBadge。SSOT:button.spec.md `iconOnly 的邊界` 節 + `badge.spec.md` Overlay 適用元件 canonical。'\n )\n }\n\n // shadcn compat:AlertDialog、Toast 等元件內部會傳入這些 alias,\n // 在此靜默轉換,不暴露到型別或自動完成。\n // dismiss=true 強制 variant=text(dismiss canonical);override 其他 variant 傳入。\n // 2026-06-06 預設 emphasis 改低(對齊世界級:MUI 預設 text / Ant 預設 default / Polaris 低 emphasis —\n // 預設 primary CTA 是 outlier)。labeled 無 variant → `tertiary`(中性外框,清楚可點性,再按重要程度升 primary);\n // iconOnly 無 variant → `text`(toolbar ghost,對齊 action-bar.spec.md「純動作工具/低重量輔助 → text」+\n // Material 3「icon-only = no fill」)。**CTA 必 explicit `variant=\"primary\"`**(不靠預設)。dismiss 仍強制 text。\n const resolvedVariant: InternalVariant =\n dismiss ? 'text' :\n (variantProp as string) === 'destructive' ? 'primary' :\n (variantProp as string) === 'ghost' ? 'text' :\n (variantProp as InternalVariant) ?? (iconOnly ? 'text' : 'tertiary')\n\n const resolvedDanger = dangerProp || (variantProp as string) === 'destructive'\n\n // Dev-mode warning:danger 僅 primary / secondary / text 有 compoundVariant。\n // tertiary / link + danger 無匹配 compound → **靜默渲染成一般 tertiary / link**(灰、無紅色\n // danger 視覺)— consumer 可能出「不紅的刪除鍵」而不自知。Spec SSOT:button.spec.md\n // 「禁止事項」danger 條(2026-07-14 deep-audit 補 warn,對齊既有 overlayBadge / iconOnly warn pattern)。\n if (process.env.NODE_ENV !== 'production' && resolvedDanger && (resolvedVariant === 'tertiary' || resolvedVariant === 'link')) {\n console.warn(\n `[DS Button] \\`danger\\` 不支援 variant=\"${resolvedVariant}\"(無對應 compoundVariant,會靜默渲染成一般 ${resolvedVariant},無紅色 danger 視覺)。危險操作請改用 primary(立即不可逆)/ secondary(可反悔)/ text(低度)。SSOT:button.spec.md「禁止事項」danger 條。`\n )\n }\n\n // ButtonGroup context:vertical group 自動注入 fullWidth\n const groupCtx = React.useContext(ButtonGroupContext)\n const resolvedFullWidth = fullWidth || !!groupCtx.fullWidth\n\n const Comp = asChild ? Slot : 'button'\n const iconSize = resolvedSize === 'lg' ? 20 : 16\n\n // loading 行為:spinner 永遠在 prefix 位置\n // 有 prefix icon → icon 換成 spinner(同位置,零 layout shift)\n // 無 prefix icon → spinner 加在文字左邊(按鈕略微變寬,可接受)\n const hasSuffix = badge != null || EndIcon !== undefined\n\n // icon-only 自動 tooltip:從 props 提取 aria-label,同時保留在 DOM\n const { 'aria-label': ariaLabel, ...restProps } = props\n\n // Toggle 狀態:pressed 定義時自動寫入 aria-pressed + data-state。\n // 未定義時不寫入任何 toggle 屬性(按鈕為一般 action button)。\n // 樣式由 cva data-[state=on] compoundVariants(variant × pressedTone)套用——\n // secondary/tertiary/text 預設 pressedTone='emphasis' 走 primary-subtle,\n // pressedTone='neutral' 才走 neutral-selected family;primary/link 不定義 on 分支,傳入無效果。\n const toggleAttrs =\n pressed === undefined\n ? {}\n : { 'aria-pressed': pressed, 'data-state': pressed ? 'on' : 'off' }\n\n // Chrome-unbounded marker(2026-04-22 v5 canonical):button 若無視覺邊界(text variant 或 dismiss),\n // 標記 data-unbounded=\"true\"。SurfaceHeader 透過 [&_[data-unbounded]]:my-[...] 套負 margin\n // 讓 layout 佔位縮到 24(chrome-header-height 幾何)— button native size 與命中區不變\n // (命中 ≡ 可視,見 ds-canonical/references/hit-area-canonical.md;2026-09-24 把原文的\n // 「touch target」正名為命中區 —— 本 DS 以滑鼠精度為前提,尺寸不以觸控門檻推導)。\n // 詳 patterns/overlay-surface/overlay-chrome-sizing.spec.md「Chrome dismiss size canonical」(2026-09-27 自 overlay-surface.spec.md 拆出)\n const unboundedAttr =\n resolvedVariant === 'text' || dismiss ? { 'data-unbounded': 'true' } : {}\n\n // Native path 內部結構:[loading spinner | startIcon(+overlayBadge)] [label] [badge + endIcon]。\n // asChild 時**不渲染**(2026-07-04 fix):Radix Slot 規範 children 必為單一 element\n // (React.Children.only)— 內部多 expression children 變 array 必 runtime throw;\n // asChild 分支只 render consumer child,內容由 consumer 自帶。\n // 詳 CLAUDE.md 失敗記憶索引「asChild ? Slot : Native 內部 JSX 仍渲染多 children」條。\n const nativeChildren = (\n <>\n {loading ? (\n <CircularProgress size={iconSize} className=\"text-current\" />\n ) : StartIcon ? (\n resolvedIconOnly && overlayBadge ? (\n // Overlay badge canonical:wrapper 貼 icon 尺寸,badge 中心對齊 icon top-right corner\n // (Material BadgedBox / iOS App icon),不是 button chrome 角。\n //\n // CSS 細節(2026-04-20 bug fix):用 `inline-block` + 明確 width/height + `leading-none`\n // 避免 `inline-flex` span 在 Button flex container 內被撐高/撐寬(inline-flex 的 span\n // 在某些瀏覽器下會把絕對定位子元素的 translate 計算基準搞錯,造成 badge 噴飛、Button\n // aspect-square 失效)。明確給 span width/height = iconSize 鎖住 positioning context。\n <span\n className=\"relative inline-block leading-none shrink-0 pointer-events-none\"\n style={{ width: iconSize, height: iconSize }}\n >\n <StartIcon size={iconSize} aria-hidden />\n <span className=\"absolute top-0 right-0 translate-x-1/2 -translate-y-1/2 pointer-events-auto\">\n {overlayBadge}\n </span>\n </span>\n ) : (\n <StartIcon size={iconSize} aria-hidden />\n )\n ) : null}\n {children != null && <span className=\"px-1\">{children}</span>}\n {hasSuffix && (\n <span className=\"inline-flex items-center gap-1\">\n {badge}\n {EndIcon && <EndIcon size={iconSize} aria-hidden />}\n </span>\n )}\n </>\n )\n\n const sharedClassName = cn(\n buttonVariants({ variant: resolvedVariant, danger: resolvedDanger, size: resolvedSize, pressedTone, className }),\n // iconOnly 鐵律:padding-free + aspect-square + flex-center (Polaris idiom)\n // 0 magic-number 0 公式自動正方形。詳 ICON_ONLY_BASE rationale。\n resolvedIconOnly && ICON_ONLY_BASE,\n // Dismiss 視覺弱化:override Button text variant 預設 foreground 為 fg-muted → hover fg-secondary\n // 跟 Inline Action dismiss 視覺一致(cross-implementation dimming canonical)\n // 弱化 icon hover 一階(item-anatomy.tsx ItemInlineActionButton 同階梯 SSOT)\n dismiss && 'text-fg-muted hover:text-fg-secondary',\n // aria-disabled 的 dismiss hover 釘在 fg-muted(button.spec.md「狀態疊加」表 aria-disabled 列)\n dismiss && 'aria-disabled:hover:text-fg-muted',\n resolvedFullWidth && 'w-full',\n )\n\n // 2026-07-05 D4 修:asChild 原 silently drop loading/disabled 語意(無 aria-busy/aria-disabled/\n // click guard → <a> loading 期間可重複點擊)。補 ARIA(任何 element 合法;aria-disabled 自動點亮\n // cva 既有視覺)+ functional guard(APG aria-disabled 模式:語意+視覺由元件、功能阻斷由 guard)。\n const { onClick: slotConsumerOnClick, ...slotRest } = restProps as typeof restProps & { onClick?: React.MouseEventHandler<HTMLElement> }\n const buttonEl = asChild ? (\n // Slot 分支:children 必為單一 consumer element(React.Children.only),\n // 內部 slot(loading/startIcon/badge)不渲染;不硬塞 type/disabled(consumer child 可能是 <a>)。\n <Comp\n className={sharedClassName}\n ref={ref}\n aria-label={ariaLabel}\n aria-busy={loading || undefined}\n aria-disabled={disabled || loading || undefined}\n onClick={disabled || loading\n ? (e: React.MouseEvent<HTMLElement>) => { e.preventDefault(); e.stopPropagation() }\n : slotConsumerOnClick}\n {...toggleAttrs}\n {...unboundedAttr}\n {...slotRest}\n >\n {children}\n </Comp>\n ) : (\n <Comp\n className={sharedClassName}\n ref={ref}\n type=\"button\"\n disabled={disabled || loading}\n aria-busy={loading || undefined}\n aria-label={ariaLabel}\n {...toggleAttrs}\n {...unboundedAttr}\n {...restProps}\n >\n {nativeChildren}\n </Comp>\n )\n\n // icon-only + aria-label → 自動包 Tooltip(tooltip 是元件保證的行為)\n // 不建立獨立 TooltipProvider——依賴全域 Provider,\n // 這樣所有 tooltip 共享同一組 delay 參數和 warm-up 機制\n if (resolvedIconOnly && typeof ariaLabel === 'string' && !asChild) {\n return (\n <Tooltip>\n <TooltipTrigger asChild>{buttonEl}</TooltipTrigger>\n <TooltipContent>{ariaLabel}</TooltipContent>\n </Tooltip>\n )\n }\n\n return buttonEl\n }\n)\nButton.displayName = 'Button'\n\n/**\n * componentMeta — Story Auto-Compile 系統消費的結構化 canonical\n * (見 .claude/planning/story-auto-compile.md Phase 1)\n *\n * compile-stories.mjs 讀本 export + spec.md frontmatter 產出\n * anatomy.stories.tsx 的 variant/size/state/token 矩陣 canonical section。\n *\n * Keys 必跟 buttonVariants cva + spec frontmatter 對齊(compile-time 驗證)。\n */\nexport const buttonMeta = {\n component: 'Button',\n family: 3, // Pill Layout\n variants: {\n primary: { purpose: '主要 action / CTA' },\n secondary: { purpose: '次要 action(陪襯 primary)' },\n tertiary: { purpose: '第三級 action(tool-like)' },\n text: { purpose: '文字樣式 action(low emphasis / toolbar)' },\n link: { purpose: '內文連結(inline reading)' },\n },\n sizes: {\n xs: { fieldHeight: 24, iconSize: 16, typography: 'caption' },\n sm: { fieldHeight: 28, iconSize: 16, typography: 'body' },\n md: { fieldHeight: 32, iconSize: 16, typography: 'body' },\n lg: { fieldHeight: 36, iconSize: 20, typography: 'body-lg' },\n },\n // 'pressed' = toggle on 持續態(data-[state=on],emphasis 淡藍底/neutral 灰底;2026-07-07 meta 詞彙統一補列)\n // 'loading' = CircularProgress spinner 取代 startIcon + aria-busy + click guard(2026-07-14 deep-audit\n // 補列,原漏 → auto-compile 生成矩陣缺 loading;對齊 anatomy StateBehavior Loading 行 + tsx loading 分支)\n states: ['default', 'hover', 'active', 'pressed', 'loading', 'focus-visible', 'disabled'],\n tokens: {\n // 2026-07-04 audit 補齊:原只列 primary variant 家族;按 buttonVariants cva 真實 class 補\n // secondary/tertiary/text/link/danger 消費的 token(auto-compile token 矩陣依此)。\n // 2026-07-14 deep-audit 二次補齊(掃 cva 全 bg-* class):+ neutral-selected-hover / neutral-selected-active\n // (neutral pressedTone compound 消費)+ error / error-hover / error-active(primary+danger bg-error 系)\n bg: ['--primary', '--primary-hover', '--primary-active', '--primary-subtle', '--bg-disabled', '--surface', '--neutral-hover', '--neutral-active', '--neutral-selected', '--neutral-selected-hover', '--neutral-selected-active', '--error', '--error-hover', '--error-active'],\n fg: ['--on-emphasis', '--fg-disabled', '--foreground', '--primary', '--primary-hover', '--primary-active', '--error', '--error-hover', '--error-active'],\n ring: ['--ring'],\n },\n defaultVariant: 'tertiary', // 2026-06-10 修 stale:對齊 cva defaultVariants(2026-06-06 labeled 預設改 tertiary,meta 漏同步)\n defaultSize: 'md',\n} as const\n\nexport { Button, buttonVariants, ButtonGroupContext }\n"],"names":[],"mappings":"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"}
|
|
@@ -67,8 +67,13 @@ interface CalendarOwnProps extends Omit<React.HTMLAttributes<HTMLDivElement>, 'o
|
|
|
67
67
|
today?: Date;
|
|
68
68
|
/** 事件資料 */
|
|
69
69
|
events?: CalendarEvent[];
|
|
70
|
-
/**
|
|
71
|
-
|
|
70
|
+
/**
|
|
71
|
+
* 右上角「新事件」CTA 的回調 —— 全域新增入口,恆渲染;日期能不能新增由新增流程自己擋,不由這顆鈕的有無表達
|
|
72
|
+
*(user 2026-09-29:「右上角那是全域新增,表示點了之後要先選了日期才能新增啊,那肯定會擋住不能新增的日期啊」)。
|
|
73
|
+
* 元件沒有內建的「新增」行為,所以必填:meta-patterns M23(f)—— 固定元素不以 callback 有無當渲染閘、
|
|
74
|
+
* 無內建 fallback 的 callback 必填。唯讀月曆(`readOnlyDates` / `readOnlyEvents`)一樣要傳。
|
|
75
|
+
*/
|
|
76
|
+
onCreateEvent: () => void;
|
|
72
77
|
/** 0 = Sunday, 1 = Monday。預設 0(對齊 Google Calendar 美系預設) */
|
|
73
78
|
weekStartsOn?: 0 | 1;
|
|
74
79
|
/** 自訂 event tile 渲染 */
|
|
@@ -82,7 +87,7 @@ interface CalendarOwnProps extends Omit<React.HTMLAttributes<HTMLDivElement>, 'o
|
|
|
82
87
|
/** 月份導覽 <nav> landmark 的 aria-label。Override for i18n. */
|
|
83
88
|
navAriaLabel?: string;
|
|
84
89
|
todayLabel?: string;
|
|
85
|
-
/** 「新事件」CTA 文字。Override for i18n。CTA
|
|
90
|
+
/** 「新事件」CTA 文字。Override for i18n。CTA 是 toolbar 的固定元素、恆渲染(spec Toolbar 段;`onCreateEvent` 必填)。 */
|
|
86
91
|
createLabel?: string;
|
|
87
92
|
}
|
|
88
93
|
export type CalendarProps = CalendarOwnProps & CalendarDateCellProps & CalendarEventTileProps;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"calendar.d.ts","sourceRoot":"","sources":["../../../src/components/Calendar/calendar.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAA;AAiB9B,OAAO,EAAqC,KAAK,cAAc,EAAE,MAAM,0CAA0C,CAAA;AAMjH;;;;;;;;;;;;;;;;GAgBG;AAIH,MAAM,WAAW,aAAa;IAC5B,EAAE,EAAE,MAAM,CAAA;IACV,KAAK,EAAE,MAAM,CAAA;IACb,8DAA8D;IAC9D,KAAK,EAAE,MAAM,GAAG,IAAI,CAAA;IACpB,GAAG,EAAE,MAAM,GAAG,IAAI,CAAA;IAClB,MAAM,CAAC,EAAE,OAAO,CAAA;IAChB;;;;OAIG;IACH,KAAK,CAAC,EAAE,cAAc,CAAA;IACtB,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;CACnC;AAUD,yGAAyG;AACzG,KAAK,qBAAqB,GACtB;IACE,mDAAmD;IACnD,aAAa,CAAC,EAAE,KAAK,CAAA;IACrB,gFAAgF;IAChF,WAAW,EAAE,CAAC,IAAI,EAAE,IAAI,KAAK,IAAI,CAAA;CAClC,GACD;IACE,0EAA0E;IAC1E,aAAa,EAAE,IAAI,CAAA;IACnB,WAAW,CAAC,EAAE,KAAK,CAAA;CACpB,CAAA;AAEL,8CAA8C;AAC9C,KAAK,sBAAsB,GACvB;IACE,mDAAmD;IACnD,cAAc,CAAC,EAAE,KAAK,CAAA;IACtB,qDAAqD;IACrD,YAAY,EAAE,CAAC,KAAK,EAAE,aAAa,KAAK,IAAI,CAAA;CAC7C,GACD;IACE,yDAAyD;IACzD,cAAc,EAAE,IAAI,CAAA;IACpB,YAAY,CAAC,EAAE,KAAK,CAAA;CACrB,CAAA;AAEL,UAAU,gBAAiB,SAAQ,IAAI,CAAC,KAAK,CAAC,cAAc,CAAC,cAAc,CAAC,EAAE,UAAU,CAAC;IACvF,wBAAwB;IACxB,aAAa,CAAC,EAAE,IAAI,CAAA;IACpB,oBAAoB,CAAC,EAAE,IAAI,CAAA;IAC3B,qBAAqB,CAAC,EAAE,CAAC,IAAI,EAAE,IAAI,KAAK,IAAI,CAAA;IAC5C;;;;OAIG;IACH,KAAK,CAAC,EAAE,IAAI,CAAA;IAEZ,WAAW;IACX,MAAM,CAAC,EAAE,aAAa,EAAE,CAAA;IAExB
|
|
1
|
+
{"version":3,"file":"calendar.d.ts","sourceRoot":"","sources":["../../../src/components/Calendar/calendar.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAA;AAiB9B,OAAO,EAAqC,KAAK,cAAc,EAAE,MAAM,0CAA0C,CAAA;AAMjH;;;;;;;;;;;;;;;;GAgBG;AAIH,MAAM,WAAW,aAAa;IAC5B,EAAE,EAAE,MAAM,CAAA;IACV,KAAK,EAAE,MAAM,CAAA;IACb,8DAA8D;IAC9D,KAAK,EAAE,MAAM,GAAG,IAAI,CAAA;IACpB,GAAG,EAAE,MAAM,GAAG,IAAI,CAAA;IAClB,MAAM,CAAC,EAAE,OAAO,CAAA;IAChB;;;;OAIG;IACH,KAAK,CAAC,EAAE,cAAc,CAAA;IACtB,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;CACnC;AAUD,yGAAyG;AACzG,KAAK,qBAAqB,GACtB;IACE,mDAAmD;IACnD,aAAa,CAAC,EAAE,KAAK,CAAA;IACrB,gFAAgF;IAChF,WAAW,EAAE,CAAC,IAAI,EAAE,IAAI,KAAK,IAAI,CAAA;CAClC,GACD;IACE,0EAA0E;IAC1E,aAAa,EAAE,IAAI,CAAA;IACnB,WAAW,CAAC,EAAE,KAAK,CAAA;CACpB,CAAA;AAEL,8CAA8C;AAC9C,KAAK,sBAAsB,GACvB;IACE,mDAAmD;IACnD,cAAc,CAAC,EAAE,KAAK,CAAA;IACtB,qDAAqD;IACrD,YAAY,EAAE,CAAC,KAAK,EAAE,aAAa,KAAK,IAAI,CAAA;CAC7C,GACD;IACE,yDAAyD;IACzD,cAAc,EAAE,IAAI,CAAA;IACpB,YAAY,CAAC,EAAE,KAAK,CAAA;CACrB,CAAA;AAEL,UAAU,gBAAiB,SAAQ,IAAI,CAAC,KAAK,CAAC,cAAc,CAAC,cAAc,CAAC,EAAE,UAAU,CAAC;IACvF,wBAAwB;IACxB,aAAa,CAAC,EAAE,IAAI,CAAA;IACpB,oBAAoB,CAAC,EAAE,IAAI,CAAA;IAC3B,qBAAqB,CAAC,EAAE,CAAC,IAAI,EAAE,IAAI,KAAK,IAAI,CAAA;IAC5C;;;;OAIG;IACH,KAAK,CAAC,EAAE,IAAI,CAAA;IAEZ,WAAW;IACX,MAAM,CAAC,EAAE,aAAa,EAAE,CAAA;IAExB;;;;;OAKG;IACH,aAAa,EAAE,MAAM,IAAI,CAAA;IAEzB,2DAA2D;IAC3D,YAAY,CAAC,EAAE,CAAC,GAAG,CAAC,CAAA;IAEpB,uBAAuB;IACvB,eAAe,CAAC,EAAE,CAAC,KAAK,EAAE,aAAa,KAAK,KAAK,CAAC,SAAS,CAAA;IAE3D,SAAS,CAAC,EAAE,MAAM,CAAA;IAElB,yBAAyB;IACzB,MAAM,CAAC,EAAE,MAAM,CAAA;IAEf,0DAA0D;IAC1D,aAAa,CAAC,EAAE,MAAM,CAAA;IACtB,aAAa,CAAC,EAAE,MAAM,CAAA;IACtB,0DAA0D;IAC1D,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,gGAAgG;IAChG,WAAW,CAAC,EAAE,MAAM,CAAA;CACrB;AAED,MAAM,MAAM,aAAa,GAAG,gBAAgB,GAAG,qBAAqB,GAAG,sBAAsB,CAAA;AAiH7F,QAAA,MAAM,QAAQ,sFAugBZ,CAAA;AAKF,eAAO,MAAM,YAAY;;;;;;;;;;;CAgBf,CAAA;AAEV,OAAO,EAAE,QAAQ,EAAE,CAAA"}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { jsxs, jsx } from "react/jsx-runtime";
|
|
2
2
|
import * as React from "react";
|
|
3
3
|
import { ChevronLeft, ChevronRight, Plus } from "lucide-react";
|
|
4
|
-
import { startOfMonth, endOfMonth, startOfWeek, endOfWeek, eachDayOfInterval,
|
|
4
|
+
import { startOfMonth, endOfMonth, startOfWeek, endOfWeek, eachDayOfInterval, startOfDay, isSameMonth, format, isSameDay, subMonths, addMonths, addDays } from "date-fns";
|
|
5
5
|
import { cn } from "../../lib/utils.js";
|
|
6
6
|
import { CAT_SUBTLE, CAT_EVENT, CAT_ACCENT } from "../../tokens/categorical-color.js";
|
|
7
7
|
import { Button } from "../Button/button.js";
|
|
@@ -59,8 +59,10 @@ function MonthEventTile({
|
|
|
59
59
|
// hover-instant 閘看不到 .ts 裡的 class 字串,這格因此漏掉;2026-09-25 補拿掉。
|
|
60
60
|
tileBase,
|
|
61
61
|
"cursor-pointer",
|
|
62
|
-
// 焦點:tile 由格內導覽(F2 進格)
|
|
63
|
-
//(
|
|
62
|
+
// 焦點:tile 由格內導覽(F2 進格)取得焦點。框**往內**畫:事件方塊之間 gap-0.5 = 2px,往外 +2px 會壓到上下相鄰的方塊
|
|
63
|
+
//(focus-canonical.md「套回實測值驗證」Calendar 事件 tile 列:內 −2px;與下方自訂 tile 的 wrapper 同一條)。
|
|
64
|
+
// 2026-09-29 前這裡沒寫 class、走全域外描邊,與正本相反(待辦總帳 N46「內建事件方塊鍵盤框往內」)。
|
|
65
|
+
"focus-visible:focus-ring-inset",
|
|
64
66
|
colorClass
|
|
65
67
|
),
|
|
66
68
|
children: /* @__PURE__ */ jsx("span", { ref: truncationRef, className: "block truncate", children: event.title })
|
|
@@ -164,9 +166,10 @@ const Calendar = React.forwardRef(function Calendar2({
|
|
|
164
166
|
const gridRef = React.useRef(null);
|
|
165
167
|
const pendingFocusIso = React.useRef(null);
|
|
166
168
|
const [focusedDateState, setFocusedDateState] = React.useState(null);
|
|
169
|
+
const todayDayMs = startOfDay(resolvedToday).getTime();
|
|
167
170
|
const anchorDate = React.useMemo(
|
|
168
|
-
() => isSameMonth(
|
|
169
|
-
[
|
|
171
|
+
() => isSameMonth(todayDayMs, refDate) ? new Date(todayDayMs) : startOfMonth(refDate),
|
|
172
|
+
[todayDayMs, refDate]
|
|
170
173
|
);
|
|
171
174
|
const gridFirstMs = days.length > 0 ? startOfDay(days[0]).getTime() : 0;
|
|
172
175
|
const gridLastMs = days.length > 0 ? startOfDay(days[days.length - 1]).getTime() : 0;
|
|
@@ -316,7 +319,7 @@ const Calendar = React.forwardRef(function Calendar2({
|
|
|
316
319
|
)
|
|
317
320
|
] }),
|
|
318
321
|
/* @__PURE__ */ jsx("h2", { className: "text-body-lg font-medium text-foreground flex-1 min-w-0 truncate ml-2", children: /* @__PURE__ */ jsx(TruncatedText, { display: "block", children: monthTitle }) }),
|
|
319
|
-
|
|
322
|
+
/* @__PURE__ */ jsx(Button, { variant: "primary", size: "sm", startIcon: Plus, onClick: onCreateEvent, children: createLabel })
|
|
320
323
|
]
|
|
321
324
|
}
|
|
322
325
|
),
|
|
@@ -356,7 +359,7 @@ const Calendar = React.forwardRef(function Calendar2({
|
|
|
356
359
|
// 焦點框往外(= 不寫)。2026-09-10 重量:日期數字鈕(24×24)在格子裡是**置中**的,不是撐滿 ——
|
|
357
360
|
// 上 6 / 下 4(下方是同格的事件容器)/ 左 128 / 右 7,最小 4.00 = canonical「算放得下」。
|
|
358
361
|
// 2026-09-07 那句「往外會壓到隔壁格」量的是**格子**邊界不是鈕的鄰居;真正貼邊的是事件方塊
|
|
359
|
-
//(彼此 gap-0.5 = 2px)
|
|
362
|
+
//(彼此 gap-0.5 = 2px),那一處往內(內建 tile 與自訂 tile 的 wrapper 都寫 focus-ring-inset)。
|
|
360
363
|
"inline-flex items-center justify-center min-w-6 h-6 rounded-full text-body font-medium",
|
|
361
364
|
isToday && "px-2 bg-info text-on-emphasis",
|
|
362
365
|
// 非當月 = 淡字 `fg-muted`,**不是** `fg-disabled`:這天跟當月格同一種可點性(可點就一樣可點,唯讀就一樣唯讀),
|
|
@@ -368,6 +371,10 @@ const Calendar = React.forwardRef(function Calendar2({
|
|
|
368
371
|
// W3C button 語義禁止互動後代,cell 內含 role="button" 事件 tile = nested-interactive 違規。
|
|
369
372
|
// 對齊 Google Calendar:gridcell = 容器,日期數字按鈕 = 日期級 keyboard 入口,tile 各自為 button。
|
|
370
373
|
// div onClick 保留滑鼠「點 cell 空白處等同點日期」便利(keyboard 走日期數字按鈕,功能等價)。
|
|
374
|
+
// 點格內空白處也把**鍵盤位置**帶過來(2026-09-29,待辦總帳 N46):停靠點(roving tabindex)搬到這一天、
|
|
375
|
+
// 焦點落在這一天的日期鈕 —— 之後 Tab 回來停在剛點的那天、consumer 開的對話框關掉後焦點也回到它;
|
|
376
|
+
// 2026-09-29 前只有點到日期數字鈕才會(它是原生 button,瀏覽器自己給焦點),點空白處停靠點留在別天。
|
|
377
|
+
// 同一條規則 DataTable 試算表已做(「滑鼠點到哪一格,鍵盤位置就跟到哪一格」,data-table.spec.md「試算表模式」)。
|
|
371
378
|
//
|
|
372
379
|
// **日期格唯讀(readOnlyDates)時**(2026-09-26,待辦總帳 L8 / C15):格子不接點擊、不亮,日期數字只是文字;
|
|
373
380
|
// 格子裡於是只剩文字(與可點的事件方塊,若有)→ 依 APG Grid「A cell contains text or a single graphic and
|
|
@@ -377,7 +384,14 @@ const Calendar = React.forwardRef(function Calendar2({
|
|
|
377
384
|
"div",
|
|
378
385
|
{
|
|
379
386
|
role: "gridcell",
|
|
380
|
-
...datesInteractive ? {
|
|
387
|
+
...datesInteractive ? {
|
|
388
|
+
onClick: (e) => {
|
|
389
|
+
var _a;
|
|
390
|
+
(_a = dayStopOf(e.currentTarget)) == null ? void 0 : _a.focus({ preventScroll: true });
|
|
391
|
+
setFocusedDateState(date);
|
|
392
|
+
onDateClick(date);
|
|
393
|
+
}
|
|
394
|
+
} : dayStop,
|
|
381
395
|
className: cn(
|
|
382
396
|
"flex flex-col gap-1 min-h-28 p-1.5 text-left",
|
|
383
397
|
"border-r border-b border-divider last:border-r-0",
|