@qijenchen/design-system 0.1.0-beta.81 → 0.1.0-beta.83
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CLAUDE.md +2 -3
- package/dist/components/Avatar/avatar.d.ts.map +1 -1
- package/dist/components/Avatar/avatar.js +1 -2
- package/dist/components/Avatar/avatar.js.map +1 -1
- package/dist/components/BulkActionBar/bulk-action-bar.d.ts +3 -0
- package/dist/components/BulkActionBar/bulk-action-bar.d.ts.map +1 -1
- package/dist/components/BulkActionBar/bulk-action-bar.js +2 -2
- package/dist/components/BulkActionBar/bulk-action-bar.js.map +1 -1
- package/dist/components/Button/button.d.ts +1 -1
- package/dist/components/Button/button.d.ts.map +1 -1
- package/dist/components/Button/button.js +5 -2
- package/dist/components/Button/button.js.map +1 -1
- package/dist/components/Calendar/calendar.d.ts +3 -1
- package/dist/components/Calendar/calendar.d.ts.map +1 -1
- package/dist/components/Calendar/calendar.js +40 -17
- package/dist/components/Calendar/calendar.js.map +1 -1
- package/dist/components/Carousel/carousel.d.ts +1 -1
- package/dist/components/Carousel/carousel.d.ts.map +1 -1
- package/dist/components/Carousel/carousel.js +3 -1
- package/dist/components/Carousel/carousel.js.map +1 -1
- package/dist/components/Checkbox/checkbox.d.ts +1 -1
- package/dist/components/Checkbox/checkbox.d.ts.map +1 -1
- package/dist/components/Checkbox/checkbox.js +2 -1
- package/dist/components/Checkbox/checkbox.js.map +1 -1
- package/dist/components/Chip/chip.d.ts +2 -2
- package/dist/components/Chip/chip.d.ts.map +1 -1
- package/dist/components/Chip/chip.js +41 -12
- package/dist/components/Chip/chip.js.map +1 -1
- package/dist/components/Coachmark/coachmark.d.ts.map +1 -1
- package/dist/components/Coachmark/coachmark.js +6 -6
- package/dist/components/Coachmark/coachmark.js.map +1 -1
- package/dist/components/Combobox/combobox.d.ts +1 -1
- package/dist/components/Combobox/combobox.d.ts.map +1 -1
- package/dist/components/Combobox/combobox.js +5 -2
- package/dist/components/Combobox/combobox.js.map +1 -1
- package/dist/components/Command/command.d.ts +1 -1
- package/dist/components/Command/command.d.ts.map +1 -1
- package/dist/components/Command/command.js +2 -1
- package/dist/components/Command/command.js.map +1 -1
- package/dist/components/DataTable/data-table.d.ts +29 -1
- package/dist/components/DataTable/data-table.d.ts.map +1 -1
- package/dist/components/DataTable/data-table.js +71 -13
- package/dist/components/DataTable/data-table.js.map +1 -1
- package/dist/components/DataTable/filter-tree.d.ts.map +1 -1
- package/dist/components/DateGrid/date-grid.d.ts +2 -2
- package/dist/components/DateGrid/date-grid.d.ts.map +1 -1
- package/dist/components/DateGrid/date-grid.js +54 -10
- package/dist/components/DateGrid/date-grid.js.map +1 -1
- package/dist/components/DatePicker/date-picker.d.ts +2 -8
- package/dist/components/DatePicker/date-picker.d.ts.map +1 -1
- package/dist/components/DatePicker/date-picker.js +15 -3
- package/dist/components/DatePicker/date-picker.js.map +1 -1
- package/dist/components/DescriptionList/description-list.d.ts.map +1 -1
- package/dist/components/DescriptionList/description-list.js +1 -1
- package/dist/components/DescriptionList/description-list.js.map +1 -1
- package/dist/components/DropdownMenu/dropdown-menu.d.ts +3 -3
- package/dist/components/DropdownMenu/dropdown-menu.d.ts.map +1 -1
- package/dist/components/DropdownMenu/dropdown-menu.js +14 -4
- package/dist/components/DropdownMenu/dropdown-menu.js.map +1 -1
- package/dist/components/Empty/empty.d.ts.map +1 -1
- package/dist/components/Empty/empty.js +1 -1
- package/dist/components/Empty/empty.js.map +1 -1
- package/dist/components/Field/field.d.ts +1 -1
- package/dist/components/Field/field.d.ts.map +1 -1
- package/dist/components/Field/field.js +2 -1
- package/dist/components/Field/field.js.map +1 -1
- package/dist/components/Field/use-form-validation.d.ts.map +1 -1
- package/dist/components/Field/use-form-validation.js +5 -7
- package/dist/components/Field/use-form-validation.js.map +1 -1
- package/dist/components/FileUpload/file-upload.d.ts +1 -1
- package/dist/components/FileUpload/file-upload.d.ts.map +1 -1
- package/dist/components/FileUpload/file-upload.js +11 -5
- package/dist/components/FileUpload/file-upload.js.map +1 -1
- package/dist/components/FileViewer/can-render-image.d.ts +6 -0
- package/dist/components/FileViewer/can-render-image.d.ts.map +1 -0
- package/dist/components/FileViewer/can-render-image.js +11 -0
- package/dist/components/FileViewer/can-render-image.js.map +1 -0
- package/dist/components/FileViewer/file-viewer.d.ts +2 -2
- package/dist/components/FileViewer/file-viewer.d.ts.map +1 -1
- package/dist/components/FileViewer/file-viewer.js +20 -13
- package/dist/components/FileViewer/file-viewer.js.map +1 -1
- package/dist/components/FileViewer/image-renderer.d.ts +0 -5
- package/dist/components/FileViewer/image-renderer.d.ts.map +1 -1
- package/dist/components/FileViewer/image-renderer.js +1 -9
- package/dist/components/FileViewer/image-renderer.js.map +1 -1
- package/dist/components/HoverCard/hover-card.d.ts.map +1 -1
- package/dist/components/HoverCard/hover-card.js +3 -1
- package/dist/components/HoverCard/hover-card.js.map +1 -1
- package/dist/components/Input/input.d.ts +26 -3
- package/dist/components/Input/input.d.ts.map +1 -1
- package/dist/components/Input/input.js +11 -3
- package/dist/components/Input/input.js.map +1 -1
- package/dist/components/LinkInput/link-input.d.ts +1 -1
- package/dist/components/LinkInput/link-input.d.ts.map +1 -1
- package/dist/components/LinkInput/link-input.js.map +1 -1
- package/dist/components/Menu/menu-item.d.ts.map +1 -1
- package/dist/components/Menu/menu-item.js +4 -3
- package/dist/components/Menu/menu-item.js.map +1 -1
- package/dist/components/NumberInput/number-input.d.ts +1 -1
- package/dist/components/NumberInput/number-input.d.ts.map +1 -1
- package/dist/components/NumberInput/number-input.js +2 -1
- package/dist/components/NumberInput/number-input.js.map +1 -1
- package/dist/components/Pagination/index.d.ts +2 -0
- package/dist/components/Pagination/index.d.ts.map +1 -0
- package/dist/components/Pagination/index.js +6 -0
- package/dist/components/Pagination/index.js.map +1 -0
- package/dist/components/Pagination/pagination.d.ts +49 -0
- package/dist/components/Pagination/pagination.d.ts.map +1 -0
- package/dist/components/Pagination/pagination.js +182 -0
- package/dist/components/Pagination/pagination.js.map +1 -0
- package/dist/components/PeoplePicker/people-picker.d.ts +1 -1
- package/dist/components/PeoplePicker/people-picker.d.ts.map +1 -1
- package/dist/components/PeoplePicker/people-picker.js +2 -1
- package/dist/components/PeoplePicker/people-picker.js.map +1 -1
- package/dist/components/PeoplePicker/person-display.d.ts +4 -0
- package/dist/components/PeoplePicker/person-display.d.ts.map +1 -1
- package/dist/components/PeoplePicker/person-display.js +1 -1
- package/dist/components/PeoplePicker/person-display.js.map +1 -1
- package/dist/components/Popover/popover.d.ts.map +1 -1
- package/dist/components/Popover/popover.js +58 -32
- package/dist/components/Popover/popover.js.map +1 -1
- package/dist/components/ProfileCard/profile-card.d.ts +6 -1
- package/dist/components/ProfileCard/profile-card.d.ts.map +1 -1
- package/dist/components/ProfileCard/profile-card.js +14 -4
- package/dist/components/ProfileCard/profile-card.js.map +1 -1
- package/dist/components/RadioGroup/radio-group.d.ts +1 -1
- package/dist/components/RadioGroup/radio-group.d.ts.map +1 -1
- package/dist/components/RadioGroup/radio-group.js +2 -1
- package/dist/components/RadioGroup/radio-group.js.map +1 -1
- package/dist/components/SegmentedControl/segmented-control.d.ts +2 -2
- package/dist/components/SegmentedControl/segmented-control.d.ts.map +1 -1
- package/dist/components/SegmentedControl/segmented-control.js +16 -8
- package/dist/components/SegmentedControl/segmented-control.js.map +1 -1
- package/dist/components/Select/select.d.ts +1 -1
- package/dist/components/Select/select.d.ts.map +1 -1
- package/dist/components/Select/select.js +13 -2
- package/dist/components/Select/select.js.map +1 -1
- package/dist/components/SelectMenu/index.js +3 -2
- package/dist/components/SelectMenu/select-menu.d.ts +15 -3
- package/dist/components/SelectMenu/select-menu.d.ts.map +1 -1
- package/dist/components/SelectMenu/select-menu.js +150 -99
- package/dist/components/SelectMenu/select-menu.js.map +1 -1
- package/dist/components/Sheet/sheet.d.ts.map +1 -1
- package/dist/components/Sheet/sheet.js +1 -1
- package/dist/components/Sheet/sheet.js.map +1 -1
- package/dist/components/Sidebar/sidebar.d.ts +2 -2
- package/dist/components/Sidebar/sidebar.d.ts.map +1 -1
- package/dist/components/Sidebar/sidebar.js +5 -2
- package/dist/components/Sidebar/sidebar.js.map +1 -1
- package/dist/components/Slider/slider.d.ts +1 -1
- package/dist/components/Slider/slider.d.ts.map +1 -1
- package/dist/components/Slider/slider.js +7 -3
- package/dist/components/Slider/slider.js.map +1 -1
- package/dist/components/Switch/switch.d.ts +1 -1
- package/dist/components/Switch/switch.d.ts.map +1 -1
- package/dist/components/Switch/switch.js +10 -2
- package/dist/components/Switch/switch.js.map +1 -1
- package/dist/components/Tabs/tabs.d.ts +3 -3
- package/dist/components/Tabs/tabs.d.ts.map +1 -1
- package/dist/components/Tabs/tabs.js +29 -9
- package/dist/components/Tabs/tabs.js.map +1 -1
- package/dist/components/Tag/tag.d.ts.map +1 -1
- package/dist/components/Tag/tag.js +13 -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 -1
- package/dist/components/Textarea/textarea.js.map +1 -1
- package/dist/components/TimePicker/time-picker.d.ts +1 -1
- package/dist/components/TimePicker/time-picker.d.ts.map +1 -1
- package/dist/components/TimePicker/time-picker.js +20 -7
- package/dist/components/TimePicker/time-picker.js.map +1 -1
- package/dist/components/Toast/toast.d.ts.map +1 -1
- package/dist/components/Toast/toast.js +4 -1
- package/dist/components/Toast/toast.js.map +1 -1
- package/dist/components/TreeView/tree-view.d.ts +1 -1
- package/dist/components/TreeView/tree-view.d.ts.map +1 -1
- package/dist/components/TreeView/tree-view.js +8 -3
- package/dist/components/TreeView/tree-view.js.map +1 -1
- package/dist/hooks/use-controllable.d.ts.map +1 -1
- package/dist/hooks/use-controllable.js +4 -2
- package/dist/hooks/use-controllable.js.map +1 -1
- package/dist/hooks/use-is-touch-device.d.ts.map +1 -1
- package/dist/hooks/use-is-touch-device.js +3 -1
- package/dist/hooks/use-is-touch-device.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -1
- package/dist/patterns/element-anatomy/item-anatomy.d.ts +1 -1
- package/dist/patterns/element-anatomy/item-anatomy.d.ts.map +1 -1
- package/dist/patterns/element-anatomy/item-anatomy.js.map +1 -1
- package/ds-canonical/fork/governance.lock +14 -8
- package/ds-canonical/fork/hooks/check_ds_anchor_preflight.sh +15 -4
- package/ds-canonical/fork/hooks/check_opacity_token_usage.sh +8 -0
- package/ds-canonical/fork/hooks/check_story_determinism.sh +37 -0
- package/ds-canonical/fork/manifest.json +5 -0
- package/ds-canonical/fork/preamble.md +3 -1
- package/ds-canonical/fork/skills/bug-fix-rhythm/SKILL.md +6 -0
- package/ds-canonical/fork/skills/delivery-handoff/SKILL.md +7 -0
- package/ds-canonical/fork/skills/performance-audit/SKILL.md +4 -2
- package/ds-canonical/fork/skills/visual-audit/SKILL.md +8 -0
- package/ds-canonical/hooks/check_benchmark_citation.sh +5 -3
- package/ds-canonical/hooks/check_ds_anchor_preflight.sh +15 -4
- package/ds-canonical/hooks/check_opacity_token_usage.sh +8 -0
- package/ds-canonical/hooks/check_propose_without_benchmark.sh +6 -2
- package/ds-canonical/hooks/check_ssot_header_declaration.sh +53 -0
- package/ds-canonical/hooks/check_story_determinism.sh +37 -0
- package/ds-canonical/hooks/tests/test_check_benchmark_citation.sh +3 -3
- package/ds-canonical/hooks/tests/test_check_fork_user_plugin_install.sh +3 -1
- package/ds-canonical/hooks/tests/test_check_opacity_token_usage.sh +2 -2
- package/ds-canonical/hooks/tests/test_check_ssot_header_declaration.sh +72 -0
- package/ds-canonical/rules/self-verify.md +1 -1
- package/ds-canonical/rules/spec-rules.md +1 -1
- package/ds-canonical/rules/ui-development.md +2 -0
- package/ds-canonical/skills/bug-fix-rhythm/SKILL.md +6 -0
- package/ds-canonical/skills/deep-audit-cross-codex/SKILL.md +11 -15
- package/ds-canonical/skills/delivery-handoff/SKILL.md +5 -0
- package/ds-canonical/skills/design-system-audit/SKILL.md +1 -1
- package/ds-canonical/skills/governance-health/SKILL.md +1 -0
- package/ds-canonical/skills/new-component/SKILL.md +2 -0
- package/ds-canonical/skills/performance-audit/SKILL.md +2 -2
- package/ds-canonical/skills/visual-audit/SKILL.md +8 -0
- package/ds-story-manifest.json +27 -4
- package/llms-full.txt +6 -1
- package/llms.txt +2 -1
- package/package.json +1 -1
- package/src/components/Avatar/avatar.spec.md +10 -30
- package/src/components/Avatar/avatar.tsx +13 -7
- package/src/components/Badge/badge.spec.md +1 -0
- package/src/components/Breadcrumb/breadcrumb.spec.md +1 -1
- package/src/components/BulkActionBar/bulk-action-bar.anatomy.stories.tsx +1 -1
- package/src/components/BulkActionBar/bulk-action-bar.tsx +8 -2
- package/src/components/Button/button.spec.md +1 -0
- package/src/components/Button/button.tsx +4 -1
- package/src/components/Calendar/calendar.anatomy.stories.tsx +3 -1
- package/src/components/Calendar/calendar.principles.stories.tsx +3 -1
- package/src/components/Calendar/calendar.spec.md +2 -1
- package/src/components/Calendar/calendar.stories.tsx +3 -1
- package/src/components/Calendar/calendar.tsx +53 -19
- package/src/components/Carousel/carousel.tsx +3 -1
- package/src/components/Checkbox/checkbox.tsx +2 -1
- package/src/components/Chip/chip.anatomy.stories.tsx +8 -8
- package/src/components/Chip/chip.principles.stories.tsx +3 -3
- package/src/components/Chip/chip.spec.md +6 -3
- package/src/components/Chip/chip.tsx +56 -11
- package/src/components/Coachmark/coachmark.tsx +10 -8
- package/src/components/Combobox/combobox.tsx +8 -2
- package/src/components/Command/command.tsx +2 -1
- package/src/components/DataTable/data-table.spec.md +52 -95
- package/src/components/DataTable/data-table.stories.tsx +47 -0
- package/src/components/DataTable/data-table.tsx +145 -11
- package/src/components/DataTable/filter-tree.ts +178 -51
- package/src/components/DateGrid/date-grid.spec.md +5 -4
- package/src/components/DateGrid/date-grid.tsx +64 -11
- package/src/components/DatePicker/date-picker.tsx +19 -3
- package/src/components/DescriptionList/description-list.tsx +3 -1
- package/src/components/DropdownMenu/dropdown-menu.spec.md +4 -3
- package/src/components/DropdownMenu/dropdown-menu.tsx +22 -4
- package/src/components/Empty/empty.spec.md +0 -1
- package/src/components/Empty/empty.tsx +4 -1
- package/src/components/Field/field-controls.spec.md +1 -1
- package/src/components/Field/field.tsx +2 -1
- package/src/components/Field/form-validation.spec.md +1 -1
- package/src/components/Field/use-form-validation.ts +11 -7
- package/src/components/FileItem/file-item.spec.md +9 -0
- package/src/components/FileUpload/file-upload.spec.md +1 -1
- package/src/components/FileUpload/file-upload.tsx +18 -7
- package/src/components/FileViewer/can-render-image.ts +15 -0
- package/src/components/FileViewer/file-viewer.tsx +43 -18
- package/src/components/FileViewer/image-renderer.tsx +3 -8
- package/src/components/HoverCard/hover-card.spec.md +1 -1
- package/src/components/HoverCard/hover-card.tsx +3 -1
- package/src/components/Input/input.tsx +10 -4
- package/src/components/LinkInput/link-input.tsx +4 -1
- package/src/components/Menu/menu-item.anatomy.stories.tsx +1 -1
- package/src/components/Menu/menu-item.spec.md +2 -2
- package/src/components/Menu/menu-item.tsx +12 -3
- package/src/components/NumberInput/number-input.tsx +2 -1
- package/src/components/Pagination/index.ts +5 -0
- package/src/components/Pagination/pagination.anatomy.stories.tsx +143 -0
- package/src/components/Pagination/pagination.principles.stories.tsx +81 -0
- package/src/components/Pagination/pagination.spec.md +150 -0
- package/src/components/Pagination/pagination.stories.tsx +98 -0
- package/src/components/Pagination/pagination.tsx +263 -0
- package/src/components/PeoplePicker/people-picker.tsx +2 -1
- package/src/components/PeoplePicker/person-display.tsx +5 -1
- package/src/components/Popover/popover.anatomy.stories.tsx +1 -1
- package/src/components/Popover/popover.spec.md +2 -1
- package/src/components/Popover/popover.tsx +86 -37
- package/src/components/ProfileCard/profile-card.tsx +24 -6
- package/src/components/RadioGroup/radio-group.tsx +2 -1
- package/src/components/SegmentedControl/segmented-control.anatomy.stories.tsx +1 -1
- package/src/components/SegmentedControl/segmented-control.spec.md +6 -2
- package/src/components/SegmentedControl/segmented-control.tsx +15 -7
- package/src/components/Select/select.spec.md +1 -1
- package/src/components/Select/select.tsx +26 -4
- package/src/components/SelectMenu/select-menu.spec.md +2 -1
- package/src/components/SelectMenu/select-menu.tsx +86 -12
- package/src/components/Sheet/sheet.spec.md +1 -0
- package/src/components/Sheet/sheet.tsx +2 -1
- package/src/components/Sidebar/sidebar.tsx +5 -2
- package/src/components/Slider/slider.anatomy.stories.tsx +4 -4
- package/src/components/Slider/slider.spec.md +19 -85
- package/src/components/Slider/slider.tsx +13 -5
- package/src/components/Steps/steps.spec.md +1 -0
- package/src/components/Switch/switch.spec.md +1 -0
- package/src/components/Switch/switch.tsx +9 -1
- package/src/components/Tabs/tabs.anatomy.stories.tsx +4 -4
- package/src/components/Tabs/tabs.spec.md +13 -47
- package/src/components/Tabs/tabs.tsx +38 -9
- package/src/components/Tag/tag.principles.stories.tsx +1 -1
- package/src/components/Tag/tag.spec.md +11 -43
- package/src/components/Tag/tag.tsx +23 -1
- package/src/components/Textarea/textarea.principles.stories.tsx +2 -1
- package/src/components/Textarea/textarea.tsx +4 -1
- package/src/components/TimePicker/time-picker.tsx +31 -10
- package/src/components/Toast/toast.tsx +4 -1
- package/src/components/TreeView/tree-view.spec.md +1 -1
- package/src/components/TreeView/tree-view.tsx +10 -3
- package/src/hooks/use-controllable.ts +7 -2
- package/src/hooks/use-is-touch-device.ts +8 -1
- package/src/index.ts +1 -0
- package/src/patterns/element-anatomy/inline-action.spec.md +1 -1
- package/src/patterns/element-anatomy/item-anatomy.spec.md +1 -2
- package/src/patterns/element-anatomy/item-anatomy.tsx +4 -5
- package/src/patterns/horizontal-overflow/horizontal-overflow.stories.tsx +137 -0
- package/src/patterns/overlay-surface/overlay-surface.stories.tsx +116 -0
- package/src/tokens/color/color.spec.md +5 -7
- package/src/tokens/color/semantic.css +8 -2
- package/src/tokens/layoutSpace/layoutSpace.spec.md +1 -0
- package/src/tokens/motion/motion.stories.tsx +142 -0
- package/src/tokens/opacity/opacity.spec.md +1 -1
- package/src/tokens/uiSize/uiSize.stories.tsx +103 -0
|
@@ -19,14 +19,26 @@ const chipVariants = cva(
|
|
|
19
19
|
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-1",
|
|
20
20
|
// hover(未選):border 加深一階 + 文字轉深,對齊 Input / SegmentedControl hover
|
|
21
21
|
"hover:border-border-hover hover:text-foreground",
|
|
22
|
-
// selected: 文字 + 邊框都用 primary
|
|
23
|
-
// ── pill 風格 canonical 選中規則,跟 SegmentedControl
|
|
24
|
-
// primary
|
|
25
|
-
|
|
22
|
+
// selected: 文字 + 邊框都用 primary base,底色維持 bg-surface 不變
|
|
23
|
+
// ── pill 風格 canonical 選中規則,跟 SegmentedControl 完全一致(SSOT = semantic.css「選中狀態」段):
|
|
24
|
+
// primary base 同時染文字和線條;底色不改 (不用 primary-subtle)。
|
|
25
|
+
// 2026-07-06 user 拍板:持續選中 hover 階 → base — Ant(pagination/tabs/input focus 全 base)/
|
|
26
|
+
// Atlassian(selected = brand 同顆)/ Material 3(active tab = primary)三家實錘「瞬時弱、持續強」;
|
|
27
|
+
// hover 階專屬瞬時態(hover/預覽),持續選中站 base。
|
|
28
|
+
"data-[state=on]:border-primary data-[state=on]:text-primary",
|
|
29
|
+
// 選中之上的 hover = 同色相升 hover 階(2026-07-06 user 拍板;Button pressed hover
|
|
30
|
+
// data-[state=on]:hover:text-primary-hover 家族 + Ant active 頁 hover = colorPrimaryHover
|
|
31
|
+
// 同派)。原本靠 CSS 平手 source-order 讓選中「碰巧」贏過未選 hover(靜止無回饋)= 脆弱,
|
|
32
|
+
// 顯式宣告一併解掉。
|
|
33
|
+
"data-[state=on]:hover:border-primary-hover data-[state=on]:hover:text-primary-hover",
|
|
26
34
|
// disabled:cursor-not-allowed + 鎖 hover 不變色
|
|
27
35
|
// 不用 pointer-events-none(否則 cursor-not-allowed 不會顯示)
|
|
28
36
|
"disabled:cursor-not-allowed disabled:text-fg-disabled",
|
|
29
|
-
"disabled:hover:border-border disabled:hover:text-fg-disabled"
|
|
37
|
+
"disabled:hover:border-border disabled:hover:text-fg-disabled",
|
|
38
|
+
// disabled × selected 疊加(2026-07-07 user 拍板統一;M24 state > emphasis:disabled 全滅,
|
|
39
|
+
// 選中的 primary 描邊/染字一併退場——原本只滅字不滅框 = 半滅,Checkbox checked+disabled 全滅同族)
|
|
40
|
+
"data-[state=on]:disabled:border-border data-[state=on]:disabled:text-fg-disabled",
|
|
41
|
+
"data-[state=on]:disabled:hover:border-border data-[state=on]:disabled:hover:text-fg-disabled"
|
|
30
42
|
]
|
|
31
43
|
);
|
|
32
44
|
const Chip = React.forwardRef(({ className, startIcon: StartIcon, badge, endIcon: EndIcon, children, ...props }, ref) => {
|
|
@@ -101,6 +113,16 @@ const ScrollChipGroup = React.forwardRef(({ className, children, ...props }, ref
|
|
|
101
113
|
] });
|
|
102
114
|
});
|
|
103
115
|
ScrollChipGroup.displayName = "ScrollChipGroup";
|
|
116
|
+
function getElementRef(element) {
|
|
117
|
+
var _a, _b;
|
|
118
|
+
let getter = (_a = Object.getOwnPropertyDescriptor(element.props, "ref")) == null ? void 0 : _a.get;
|
|
119
|
+
let mayWarn = getter && "isReactWarning" in getter && getter.isReactWarning;
|
|
120
|
+
if (mayWarn) return element.ref;
|
|
121
|
+
getter = (_b = Object.getOwnPropertyDescriptor(element, "ref")) == null ? void 0 : _b.get;
|
|
122
|
+
mayWarn = getter && "isReactWarning" in getter && getter.isReactWarning;
|
|
123
|
+
if (mayWarn) return element.props.ref;
|
|
124
|
+
return element.props.ref ?? element.ref ?? void 0;
|
|
125
|
+
}
|
|
104
126
|
const MenuChipGroup = React.forwardRef(({ className, children, ...props }, ref) => {
|
|
105
127
|
const { scrollRef, atStart, atEnd, canScroll } = useScrollEdges();
|
|
106
128
|
const itemRefs = React.useRef(/* @__PURE__ */ new Map());
|
|
@@ -153,12 +175,19 @@ const MenuChipGroup = React.forwardRef(({ className, children, ...props }, ref)
|
|
|
153
175
|
},
|
|
154
176
|
[groupType, groupValue, groupOnValueChange]
|
|
155
177
|
);
|
|
156
|
-
const enhancedItems = items.map(
|
|
157
|
-
|
|
178
|
+
const enhancedItems = items.map((child, i) => {
|
|
179
|
+
const childRef = getElementRef(child);
|
|
180
|
+
return React.cloneElement(
|
|
158
181
|
child,
|
|
159
|
-
{
|
|
160
|
-
|
|
161
|
-
|
|
182
|
+
{
|
|
183
|
+
ref: (el) => {
|
|
184
|
+
registerItem(i)(el);
|
|
185
|
+
if (typeof childRef === "function") childRef(el);
|
|
186
|
+
else if (childRef) childRef.current = el;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
);
|
|
190
|
+
});
|
|
162
191
|
return /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
|
|
163
192
|
/* @__PURE__ */ jsx(
|
|
164
193
|
"div",
|
|
@@ -213,10 +242,10 @@ const chipMeta = {
|
|
|
213
242
|
sm: { fieldHeight: 28, iconSize: 16, typography: "body" }
|
|
214
243
|
},
|
|
215
244
|
states: ["default", "hover", "selected", "focus-visible", "disabled"],
|
|
216
|
-
// selected = data-[state=on](primary-
|
|
245
|
+
// selected = data-[state=on](primary base,2026-07-06 拍板);cva 無 active 樣式
|
|
217
246
|
tokens: {
|
|
218
247
|
bg: ["bg-surface"],
|
|
219
|
-
fg: ["text-fg-disabled", "text-fg-secondary", "text-foreground", "text-primary
|
|
248
|
+
fg: ["text-fg-disabled", "text-fg-secondary", "text-foreground", "text-primary"],
|
|
220
249
|
ring: ["ring-ring"]
|
|
221
250
|
}
|
|
222
251
|
};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"chip.js","sources":["../../../src/components/Chip/chip.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.\nimport * as React from 'react'\nimport * as ToggleGroupPrimitive from '@radix-ui/react-toggle-group'\nimport { cva } from 'class-variance-authority'\nimport type { LucideIcon } from 'lucide-react'\nimport { cn } from '@/lib/utils'\nimport {\n DropdownMenu,\n DropdownMenuTrigger,\n DropdownMenuContent,\n DropdownMenuCheckboxItem,\n} from '@/design-system/components/DropdownMenu/dropdown-menu'\nimport {\n useScrollEdges,\n useScrollByPage,\n buildFadeMask,\n ARROW_BUTTON_WIDTH,\n OverflowScrollArrow,\n OverflowMenuTriggerButton,\n} from '@/design-system/patterns/horizontal-overflow/horizontal-overflow'\n\n/**\n * Chip — Material Design Filter Chip\n *\n * 基於 Radix ToggleGroup,橋接設計系統 token。\n * 必須在 <ChipGroup> 內使用。\n *\n * ── 內部結構(鏡射 Button)──\n * [startIcon?] [<span px-1>label</span>] [<span gap-1>badge? + endIcon?</span>]\n *\n * ── Size ──\n * 單一 size = h-field-sm(28/32 density-aware)\n * 對齊 Material 3 / Atlassian / Polaris 世界級共識\n *\n * ── State ──\n * default bg-surface border-border text-fg-secondary\n * hover border-border-hover text-foreground(對齊 Input / SegmentedControl)\n * selected bg-surface(不變) border-primary-hover text-primary-hover\n * disabled cursor-not-allowed text-fg-disabled\n *\n * ── 詳見 chip.spec.md ──\n */\n\n// ── Chip item ────────────────────────────────────────────────────────────────\n\nconst chipVariants = cva(\n [\n 'inline-flex items-center justify-center',\n 'h-field-sm px-3 gap-1',\n 'rounded-full border border-border',\n // 預設文字: text-fg-secondary (neutral-8) — 對齊 SegmentedControl / Tabs 未選狀態\n 'bg-surface text-fg-secondary',\n 'text-body leading-compact font-medium whitespace-nowrap',\n 'transition-colors duration-150',\n 'cursor-pointer select-none',\n 'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-1',\n // hover(未選):border 加深一階 + 文字轉深,對齊 Input / SegmentedControl hover\n 'hover:border-border-hover hover:text-foreground',\n // selected: 文字 + 邊框都用 primary-hover,底色維持 bg-surface 不變\n // ── pill 風格 canonical 選中規則,跟 SegmentedControl 完全一致:\n // primary-hover 同時染文字和線條;底色不改 (不用 primary-subtle)。\n 'data-[state=on]:border-primary-hover data-[state=on]:text-primary-hover',\n // disabled:cursor-not-allowed + 鎖 hover 不變色\n // 不用 pointer-events-none(否則 cursor-not-allowed 不會顯示)\n 'disabled:cursor-not-allowed disabled:text-fg-disabled',\n 'disabled:hover:border-border disabled:hover:text-fg-disabled',\n ]\n)\n\nexport interface ChipProps\n extends React.ComponentPropsWithoutRef<typeof ToggleGroupPrimitive.Item> {\n /** 左側 icon(LucideIcon),最多一個 */\n startIcon?: LucideIcon\n /** 右側 badge(通常是計數指示器)*/\n badge?: React.ReactNode\n /** 右側 icon(少見,通常是 ChevronDown 指示可展開)*/\n endIcon?: LucideIcon\n}\n\nconst Chip = React.forwardRef<\n React.ElementRef<typeof ToggleGroupPrimitive.Item>,\n ChipProps\n>(({ className, startIcon: StartIcon, badge, endIcon: EndIcon, children, ...props }, ref) => {\n const hasSuffix = badge != null || EndIcon !== undefined\n\n return (\n <ToggleGroupPrimitive.Item\n ref={ref}\n className={cn(chipVariants(), className)}\n {...props}\n >\n {StartIcon && <StartIcon size={16} aria-hidden />}\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={16} aria-hidden />}\n </span>\n )}\n </ToggleGroupPrimitive.Item>\n )\n})\nChip.displayName = 'Chip'\n\n// ── ChipGroup ────────────────────────────────────────────────────────────────\n\nexport type ChipGroupLayout = 'wrap' | 'scroll' | 'menu'\n\nexport type ChipGroupProps = React.ComponentPropsWithoutRef<\n typeof ToggleGroupPrimitive.Root\n> & {\n /** Overflow 處理模式。預設 `wrap`(塞不下換行)。詳見 chip.spec.md */\n layout?: ChipGroupLayout\n}\n\nconst ChipGroup = React.forwardRef<HTMLDivElement, ChipGroupProps>(\n ({ layout = 'wrap', className, children, ...props }, ref) => {\n if (layout === 'scroll') {\n return (\n <ScrollChipGroup ref={ref} className={className} {...props}>\n {children}\n </ScrollChipGroup>\n )\n }\n if (layout === 'menu') {\n return (\n <MenuChipGroup ref={ref} className={className} {...props}>\n {children}\n </MenuChipGroup>\n )\n }\n // wrap(預設)\n return (\n <ToggleGroupPrimitive.Root\n ref={ref}\n className={cn('flex flex-wrap gap-2', className)}\n {...props}\n >\n {children}\n </ToggleGroupPrimitive.Root>\n )\n }\n)\nChipGroup.displayName = 'ChipGroup'\n\n// ── Scroll / Menu modes ──\n// Fade mask / arrows / menu trigger 全部從 horizontal-overflow pattern module 取用。\n// 詳見 `patterns/horizontal-overflow/horizontal-overflow.spec.md`。\n//\n// Canonical 規則:所有 overflow affordance 一律是 Button text sm iconOnly,\n// 不論在 Chip / Tab / Step / SegmentedControl。Chip menu trigger 曾經用\n// chip-shape 圓形 button,已改回 text button 對齊 mental model。\n\n// ── ScrollChipGroup ──────────────────────────────────────────────────────────\n\nconst ScrollChipGroup = React.forwardRef<\n HTMLDivElement,\n React.ComponentPropsWithoutRef<typeof ToggleGroupPrimitive.Root>\n>(({ className, children, ...props }, ref) => {\n const { scrollRef, atStart, atEnd, canScroll } = useScrollEdges<HTMLDivElement>()\n const scrollByPage = useScrollByPage(scrollRef)\n const maskImage = buildFadeMask({\n canScroll,\n atStart,\n atEnd,\n reserveArrowWidth: ARROW_BUTTON_WIDTH,\n })\n\n return (\n <div className=\"relative\">\n <div\n ref={scrollRef}\n className=\"overflow-x-auto [scrollbar-width:none] [&::-webkit-scrollbar]:hidden\"\n style={{ maskImage, WebkitMaskImage: maskImage }}\n >\n <ToggleGroupPrimitive.Root\n ref={ref}\n className={cn('flex flex-nowrap gap-2 w-fit', className)}\n {...props}\n >\n {children}\n </ToggleGroupPrimitive.Root>\n </div>\n {!atStart && canScroll && (\n <OverflowScrollArrow direction=\"left\" onClick={() => scrollByPage('left')} />\n )}\n {!atEnd && canScroll && (\n <OverflowScrollArrow direction=\"right\" onClick={() => scrollByPage('right')} />\n )}\n </div>\n )\n})\nScrollChipGroup.displayName = 'ScrollChipGroup'\n\n// ── MenuChipGroup ────────────────────────────────────────────────────────────\n// Show-all navigator pattern (Chrome tab dropdown / VS Code editor tabs):\n// - Menu 永遠顯示全部 chips,每個都用 DropdownMenuCheckboxItem + checked 反映 selection\n// - 點 menu item = toggle selection + scrollIntoView,把該 chip 捲到中央\n// - 不用動態 overflow 計算,menu 內容穩定\n//\n// Menu trigger 使用 canonical `<OverflowMenuTriggerButton>`(= Button text sm iconOnly\n// + ChevronDown),跟 Tabs menu trigger 完全同一套——overflow affordance 屬於工具層,\n// 不該用 chip 自己的視覺語言(圓形 outlined)去渲染,否則 mental model 錯誤(使用者\n// 會誤以為是第 N+1 個可選 chip)。\n//\n// 過去 Chip menu trigger 曾用 `chipVariants() + aspect-square + p-0` 做成圓形 pill,\n// 已按 `horizontal-overflow.spec.md` 的 canonical 規則改回 text button。\n//\n// Fade mask 仍保留(reserveArrowWidth: 0),軟化內容硬邊。\n\n// code-quality-allow: long-function — foundational composite main body — 拆 sub-fn 會複雜化 local state / ref / context binding\nconst MenuChipGroup = React.forwardRef<\n HTMLDivElement,\n React.ComponentPropsWithoutRef<typeof ToggleGroupPrimitive.Root>\n>(({ className, children, ...props }, ref) => {\n const { scrollRef, atStart, atEnd, canScroll } = useScrollEdges<HTMLDivElement>()\n\n // Local ref map — 追蹤每個 chip 的 DOM 元素供 scrollIntoView 使用。\n // 不用 useOverflowIndices 因為 menu 永遠顯示全部, 不需要動態 overflow 計算。\n // code-quality-allow: long-function — helper fn 結構緊密,拆 sub-fn 會跨 fn 傳 state 反而複雜\n const itemRefs = React.useRef<Map<number, HTMLElement>>(new Map())\n // 2026-05-16 audit codex Round 6:capture rAF + cancel on unmount(defensive hygiene)\n const scrollRafIdRef = React.useRef<number>(0)\n React.useEffect(() => () => { if (scrollRafIdRef.current) cancelAnimationFrame(scrollRafIdRef.current) }, [])\n const registerItem = React.useCallback(\n (index: number) => (el: HTMLElement | null) => {\n if (el) itemRefs.current.set(index, el)\n else itemRefs.current.delete(index)\n },\n []\n )\n\n const menuMaskImage = buildFadeMask({ canScroll, atStart, atEnd, reserveArrowWidth: 0 })\n\n const items = React.useMemo(\n () => React.Children.toArray(children).filter(React.isValidElement) as React.ReactElement[],\n [children]\n )\n\n const groupType = (props as { type?: 'single' | 'multiple' }).type ?? 'single'\n const groupValue = (props as { value?: string | string[] }).value\n const groupOnValueChange = (\n props as { onValueChange?: (value: string | string[]) => void }\n ).onValueChange\n\n // code-quality-allow: long-function — helper fn 結構緊密,拆 sub-fn 會跨 fn 傳 state 反而複雜\n const isSelected = React.useCallback(\n (chipValue: string): boolean => {\n if (groupType === 'multiple') {\n return Array.isArray(groupValue) && groupValue.includes(chipValue)\n }\n return groupValue === chipValue\n },\n [groupType, groupValue]\n )\n\n // code-quality-allow: long-function — multi / single / none selection mode each has 事務性 branch,拆 fn 會跨 fn 傳 chipValue/index/groupOnValueChange state 反而難讀\n const toggleFromMenu = React.useCallback(\n (chipValue: string, index: number) => {\n if (!groupOnValueChange) {\n if (import.meta.env?.DEV) {\n console.warn(\n '[ChipGroup] layout=\"menu\" 需要 controlled 使用(請傳 value + onValueChange),否則 menu items 無法同步主 chip 選擇狀態。'\n )\n }\n return\n }\n if (groupType === 'multiple') {\n const current = (Array.isArray(groupValue) ? groupValue : []) as string[]\n const next = current.includes(chipValue)\n ? current.filter((v) => v !== chipValue)\n : [...current, chipValue]\n ;(groupOnValueChange as (v: string[]) => void)(next)\n } else {\n ;(groupOnValueChange as (v: string) => void)(chipValue)\n }\n // scrollIntoView: 讓剛選中的 chip 出現在視圖中央\n if (scrollRafIdRef.current) cancelAnimationFrame(scrollRafIdRef.current)\n scrollRafIdRef.current = requestAnimationFrame(() => {\n scrollRafIdRef.current = 0\n itemRefs.current.get(index)?.scrollIntoView({ behavior: 'smooth', inline: 'center', block: 'nearest' })\n })\n },\n [groupType, groupValue, groupOnValueChange]\n )\n\n const enhancedItems = items.map((child, i) =>\n React.cloneElement(\n child as React.ReactElement<{ ref?: React.Ref<HTMLElement> }>,\n { ref: registerItem(i) }\n )\n )\n\n return (\n <div className=\"flex items-center gap-2\">\n <div\n ref={scrollRef}\n className=\"flex-1 min-w-0 overflow-x-auto [scrollbar-width:none] [&::-webkit-scrollbar]:hidden\"\n style={{ maskImage: menuMaskImage, WebkitMaskImage: menuMaskImage }}\n >\n <ToggleGroupPrimitive.Root\n ref={ref}\n className={cn('flex flex-nowrap gap-2 w-fit', className)}\n {...props}\n >\n {enhancedItems}\n </ToggleGroupPrimitive.Root>\n </div>\n {canScroll && (\n <DropdownMenu>\n <DropdownMenuTrigger asChild>\n <OverflowMenuTriggerButton\n aria-label={`選項選單(共 ${items.length} 個)`}\n />\n </DropdownMenuTrigger>\n <DropdownMenuContent align=\"end\">\n {items.map((chip, index) => {\n const chipProps = chip.props as { value?: string; children?: React.ReactNode; disabled?: boolean }\n const chipValue = chipProps.value\n if (typeof chipValue !== 'string') return null\n return (\n <DropdownMenuCheckboxItem\n key={chipValue}\n checked={isSelected(chipValue)}\n disabled={chipProps.disabled}\n onCheckedChange={() => toggleFromMenu(chipValue, index)}\n >\n {chipProps.children}\n </DropdownMenuCheckboxItem>\n )\n })}\n </DropdownMenuContent>\n </DropdownMenu>\n )}\n </div>\n )\n})\nMenuChipGroup.displayName = 'MenuChipGroup'\n\n// Story auto-compile metadata — Phase 1 mechanical migration(2026-04-24)\n// Phase 2 fill needed: purpose descriptions + when rationale + world-class refs\nexport const chipMeta = {\n component: 'Chip',\n family: 3,\n // 2026-07-04 audit 填 Phase 2:Chip 單一視覺無 variant 軸(variants 留空為 intentional,非漏填)\n variants: {},\n sizes: {\n // 單一尺寸:h-field-sm(28/32 依 density)、icon 16、text-body、px-3(cva 真值)\n sm: { fieldHeight: 28, iconSize: 16, typography: 'body' },\n },\n states: ['default', 'hover', 'selected', 'focus-visible', 'disabled'], // selected = data-[state=on](primary-hover);cva 無 active 樣式\n tokens: {\n bg: ['bg-surface'],\n fg: ['text-fg-disabled', 'text-fg-secondary', 'text-foreground', 'text-primary-hover'],\n ring: ['ring-ring'],\n },\n} as const\n\nexport { Chip, ChipGroup, chipVariants }\n"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"chip.js","sources":["../../../src/components/Chip/chip.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.\nimport * as React from 'react'\nimport * as ToggleGroupPrimitive from '@radix-ui/react-toggle-group'\nimport { cva } from 'class-variance-authority'\nimport type { LucideIcon } from 'lucide-react'\nimport { cn } from '@/lib/utils'\nimport {\n DropdownMenu,\n DropdownMenuTrigger,\n DropdownMenuContent,\n DropdownMenuCheckboxItem,\n} from '@/design-system/components/DropdownMenu/dropdown-menu'\nimport {\n useScrollEdges,\n useScrollByPage,\n buildFadeMask,\n ARROW_BUTTON_WIDTH,\n OverflowScrollArrow,\n OverflowMenuTriggerButton,\n} from '@/design-system/patterns/horizontal-overflow/horizontal-overflow'\n\n/**\n * Chip — Material Design Filter Chip\n *\n * 基於 Radix ToggleGroup,橋接設計系統 token。\n * 必須在 <ChipGroup> 內使用。\n *\n * ── 內部結構(鏡射 Button)──\n * [startIcon?] [<span px-1>label</span>] [<span gap-1>badge? + endIcon?</span>]\n *\n * ── Size ──\n * 單一 size = h-field-sm(28/32 density-aware)\n * 對齊 Material 3 / Atlassian / Polaris 世界級共識\n *\n * ── State ──\n * default bg-surface border-border text-fg-secondary\n * hover border-border-hover text-foreground(對齊 Input / SegmentedControl)\n * selected bg-surface(不變) border-primary text-primary\n * disabled cursor-not-allowed text-fg-disabled\n *\n * ── 詳見 chip.spec.md ──\n */\n\n// ── Chip item ────────────────────────────────────────────────────────────────\n\nconst chipVariants = cva(\n [\n 'inline-flex items-center justify-center',\n 'h-field-sm px-3 gap-1',\n 'rounded-full border border-border',\n // 預設文字: text-fg-secondary (neutral-8) — 對齊 SegmentedControl / Tabs 未選狀態\n 'bg-surface text-fg-secondary',\n 'text-body leading-compact font-medium whitespace-nowrap',\n 'transition-colors duration-150',\n 'cursor-pointer select-none',\n 'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-1',\n // hover(未選):border 加深一階 + 文字轉深,對齊 Input / SegmentedControl hover\n 'hover:border-border-hover hover:text-foreground',\n // selected: 文字 + 邊框都用 primary base,底色維持 bg-surface 不變\n // ── pill 風格 canonical 選中規則,跟 SegmentedControl 完全一致(SSOT = semantic.css「選中狀態」段):\n // primary base 同時染文字和線條;底色不改 (不用 primary-subtle)。\n // 2026-07-06 user 拍板:持續選中 hover 階 → base — Ant(pagination/tabs/input focus 全 base)/\n // Atlassian(selected = brand 同顆)/ Material 3(active tab = primary)三家實錘「瞬時弱、持續強」;\n // hover 階專屬瞬時態(hover/預覽),持續選中站 base。\n 'data-[state=on]:border-primary data-[state=on]:text-primary',\n // 選中之上的 hover = 同色相升 hover 階(2026-07-06 user 拍板;Button pressed hover\n // data-[state=on]:hover:text-primary-hover 家族 + Ant active 頁 hover = colorPrimaryHover\n // 同派)。原本靠 CSS 平手 source-order 讓選中「碰巧」贏過未選 hover(靜止無回饋)= 脆弱,\n // 顯式宣告一併解掉。\n 'data-[state=on]:hover:border-primary-hover data-[state=on]:hover:text-primary-hover',\n // disabled:cursor-not-allowed + 鎖 hover 不變色\n // 不用 pointer-events-none(否則 cursor-not-allowed 不會顯示)\n 'disabled:cursor-not-allowed disabled:text-fg-disabled',\n 'disabled:hover:border-border disabled:hover:text-fg-disabled',\n // disabled × selected 疊加(2026-07-07 user 拍板統一;M24 state > emphasis:disabled 全滅,\n // 選中的 primary 描邊/染字一併退場——原本只滅字不滅框 = 半滅,Checkbox checked+disabled 全滅同族)\n 'data-[state=on]:disabled:border-border data-[state=on]:disabled:text-fg-disabled',\n 'data-[state=on]:disabled:hover:border-border data-[state=on]:disabled:hover:text-fg-disabled',\n ]\n)\n\nexport interface ChipProps\n extends React.ComponentPropsWithoutRef<typeof ToggleGroupPrimitive.Item> {\n /** 左側 icon(LucideIcon),最多一個 */\n startIcon?: LucideIcon\n /** 右側 badge(通常是計數指示器)*/\n badge?: React.ReactNode\n /** 右側 icon(少見,通常是 ChevronDown 指示可展開)*/\n endIcon?: LucideIcon\n}\n\nconst Chip = React.forwardRef<\n React.ElementRef<typeof ToggleGroupPrimitive.Item>,\n ChipProps\n>(({ className, startIcon: StartIcon, badge, endIcon: EndIcon, children, ...props }, ref) => {\n const hasSuffix = badge != null || EndIcon !== undefined\n\n return (\n <ToggleGroupPrimitive.Item\n ref={ref}\n className={cn(chipVariants(), className)}\n {...props}\n >\n {StartIcon && <StartIcon size={16} aria-hidden />}\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={16} aria-hidden />}\n </span>\n )}\n </ToggleGroupPrimitive.Item>\n )\n})\nChip.displayName = 'Chip'\n\n// ── ChipGroup ────────────────────────────────────────────────────────────────\n\nexport type ChipGroupLayout = 'wrap' | 'scroll' | 'menu'\n\nexport type ChipGroupProps = React.ComponentPropsWithoutRef<\n typeof ToggleGroupPrimitive.Root\n> & {\n /** Overflow 處理模式。預設 `wrap`(塞不下換行)。詳見 chip.spec.md */\n layout?: ChipGroupLayout\n}\n\nconst ChipGroup = React.forwardRef<HTMLDivElement, ChipGroupProps>(\n ({ layout = 'wrap', className, children, ...props }, ref) => {\n if (layout === 'scroll') {\n return (\n <ScrollChipGroup ref={ref} className={className} {...props}>\n {children}\n </ScrollChipGroup>\n )\n }\n if (layout === 'menu') {\n return (\n <MenuChipGroup ref={ref} className={className} {...props}>\n {children}\n </MenuChipGroup>\n )\n }\n // wrap(預設)\n return (\n <ToggleGroupPrimitive.Root\n ref={ref}\n className={cn('flex flex-wrap gap-2', className)}\n {...props}\n >\n {children}\n </ToggleGroupPrimitive.Root>\n )\n }\n)\nChipGroup.displayName = 'ChipGroup'\n\n// ── Scroll / Menu modes ──\n// Fade mask / arrows / menu trigger 全部從 horizontal-overflow pattern module 取用。\n// 詳見 `patterns/horizontal-overflow/horizontal-overflow.spec.md`。\n//\n// Canonical 規則:所有 overflow affordance 一律是 Button text sm iconOnly,\n// 不論在 Chip / Tab / Step / SegmentedControl。Chip menu trigger 曾經用\n// chip-shape 圓形 button,已改回 text button 對齊 mental model。\n\n// ── ScrollChipGroup ──────────────────────────────────────────────────────────\n\nconst ScrollChipGroup = React.forwardRef<\n HTMLDivElement,\n React.ComponentPropsWithoutRef<typeof ToggleGroupPrimitive.Root>\n>(({ className, children, ...props }, ref) => {\n const { scrollRef, atStart, atEnd, canScroll } = useScrollEdges<HTMLDivElement>()\n const scrollByPage = useScrollByPage(scrollRef)\n const maskImage = buildFadeMask({\n canScroll,\n atStart,\n atEnd,\n reserveArrowWidth: ARROW_BUTTON_WIDTH,\n })\n\n return (\n <div className=\"relative\">\n <div\n ref={scrollRef}\n className=\"overflow-x-auto [scrollbar-width:none] [&::-webkit-scrollbar]:hidden\"\n style={{ maskImage, WebkitMaskImage: maskImage }}\n >\n <ToggleGroupPrimitive.Root\n ref={ref}\n className={cn('flex flex-nowrap gap-2 w-fit', className)}\n {...props}\n >\n {children}\n </ToggleGroupPrimitive.Root>\n </div>\n {!atStart && canScroll && (\n <OverflowScrollArrow direction=\"left\" onClick={() => scrollByPage('left')} />\n )}\n {!atEnd && canScroll && (\n <OverflowScrollArrow direction=\"right\" onClick={() => scrollByPage('right')} />\n )}\n </div>\n )\n})\nScrollChipGroup.displayName = 'ScrollChipGroup'\n\n// ── MenuChipGroup ────────────────────────────────────────────────────────────\n// Show-all navigator pattern (Chrome tab dropdown / VS Code editor tabs):\n// - Menu 永遠顯示全部 chips,每個都用 DropdownMenuCheckboxItem + checked 反映 selection\n// - 點 menu item = toggle selection + scrollIntoView,把該 chip 捲到中央\n// - 不用動態 overflow 計算,menu 內容穩定\n//\n// Menu trigger 使用 canonical `<OverflowMenuTriggerButton>`(= Button text sm iconOnly\n// + ChevronDown),跟 Tabs menu trigger 完全同一套——overflow affordance 屬於工具層,\n// 不該用 chip 自己的視覺語言(圓形 outlined)去渲染,否則 mental model 錯誤(使用者\n// 會誤以為是第 N+1 個可選 chip)。\n//\n// 過去 Chip menu trigger 曾用 `chipVariants() + aspect-square + p-0` 做成圓形 pill,\n// 已按 `horizontal-overflow.spec.md` 的 canonical 規則改回 text button。\n//\n// Fade mask 仍保留(reserveArrowWidth: 0),軟化內容硬邊。\n\n// ── getElementRef(Radix Slot idiom)──────────────────────────────────────────\n// 讀取 child element 既有 ref 而不觸發 React dev deprecation warning:\n// React ≤18 ref 在 element.ref;React 19 起移到 element.props.ref,錯誤側讀取會 fire warning\n// (以 getter 上的 isReactWarning 標記)。抄 Radix UI Slot 的偵測法:\n// https://github.com/radix-ui/primitives/blob/main/packages/react/slot/src/slot.tsx(getElementRef)\n// react-compose-refs 僅為 transitive dep(非 package.json 直接依賴),不直接 import — 最小 local 實作。\nfunction getElementRef(element: React.ReactElement): React.Ref<HTMLElement> | undefined {\n // React <=18 dev:props.ref 掛 isReactWarning getter → 真 ref 在 element.ref\n let getter = Object.getOwnPropertyDescriptor(element.props, 'ref')?.get\n let mayWarn = getter && 'isReactWarning' in getter && getter.isReactWarning\n if (mayWarn) return (element as unknown as { ref?: React.Ref<HTMLElement> }).ref\n // React 19 dev:element.ref 掛 isReactWarning getter → 真 ref 在 props.ref\n getter = Object.getOwnPropertyDescriptor(element, 'ref')?.get\n mayWarn = getter && 'isReactWarning' in getter && getter.isReactWarning\n if (mayWarn) return (element.props as { ref?: React.Ref<HTMLElement> }).ref\n // production:兩處擇一\n return (\n (element.props as { ref?: React.Ref<HTMLElement> }).ref ??\n (element as unknown as { ref?: React.Ref<HTMLElement> }).ref ??\n undefined\n )\n}\n\n// code-quality-allow: long-function — foundational composite main body — 拆 sub-fn 會複雜化 local state / ref / context binding\nconst MenuChipGroup = React.forwardRef<\n HTMLDivElement,\n React.ComponentPropsWithoutRef<typeof ToggleGroupPrimitive.Root>\n>(({ className, children, ...props }, ref) => {\n const { scrollRef, atStart, atEnd, canScroll } = useScrollEdges<HTMLDivElement>()\n\n // Local ref map — 追蹤每個 chip 的 DOM 元素供 scrollIntoView 使用。\n // 不用 useOverflowIndices 因為 menu 永遠顯示全部, 不需要動態 overflow 計算。\n // code-quality-allow: long-function — helper fn 結構緊密,拆 sub-fn 會跨 fn 傳 state 反而複雜\n const itemRefs = React.useRef<Map<number, HTMLElement>>(new Map())\n // 2026-05-16 audit codex Round 6:capture rAF + cancel on unmount(defensive hygiene)\n const scrollRafIdRef = React.useRef<number>(0)\n React.useEffect(() => () => { if (scrollRafIdRef.current) cancelAnimationFrame(scrollRafIdRef.current) }, [])\n const registerItem = React.useCallback(\n (index: number) => (el: HTMLElement | null) => {\n if (el) itemRefs.current.set(index, el)\n else itemRefs.current.delete(index)\n },\n []\n )\n\n const menuMaskImage = buildFadeMask({ canScroll, atStart, atEnd, reserveArrowWidth: 0 })\n\n const items = React.useMemo(\n () => React.Children.toArray(children).filter(React.isValidElement) as React.ReactElement[],\n [children]\n )\n\n const groupType = (props as { type?: 'single' | 'multiple' }).type ?? 'single'\n const groupValue = (props as { value?: string | string[] }).value\n const groupOnValueChange = (\n props as { onValueChange?: (value: string | string[]) => void }\n ).onValueChange\n\n // code-quality-allow: long-function — helper fn 結構緊密,拆 sub-fn 會跨 fn 傳 state 反而複雜\n const isSelected = React.useCallback(\n (chipValue: string): boolean => {\n if (groupType === 'multiple') {\n return Array.isArray(groupValue) && groupValue.includes(chipValue)\n }\n return groupValue === chipValue\n },\n [groupType, groupValue]\n )\n\n // code-quality-allow: long-function — multi / single / none selection mode each has 事務性 branch,拆 fn 會跨 fn 傳 chipValue/index/groupOnValueChange state 反而難讀\n const toggleFromMenu = React.useCallback(\n (chipValue: string, index: number) => {\n if (!groupOnValueChange) {\n if (import.meta.env?.DEV) {\n console.warn(\n '[ChipGroup] layout=\"menu\" 需要 controlled 使用(請傳 value + onValueChange),否則 menu items 無法同步主 chip 選擇狀態。'\n )\n }\n return\n }\n if (groupType === 'multiple') {\n const current = (Array.isArray(groupValue) ? groupValue : []) as string[]\n const next = current.includes(chipValue)\n ? current.filter((v) => v !== chipValue)\n : [...current, chipValue]\n ;(groupOnValueChange as (v: string[]) => void)(next)\n } else {\n ;(groupOnValueChange as (v: string) => void)(chipValue)\n }\n // scrollIntoView: 讓剛選中的 chip 出現在視圖中央\n if (scrollRafIdRef.current) cancelAnimationFrame(scrollRafIdRef.current)\n scrollRafIdRef.current = requestAnimationFrame(() => {\n scrollRafIdRef.current = 0\n itemRefs.current.get(index)?.scrollIntoView({ behavior: 'smooth', inline: 'center', block: 'nearest' })\n })\n },\n [groupType, groupValue, groupOnValueChange]\n )\n\n // 2026-07-05 fix(deep-audit A.1b):原 cloneElement 直接以 registerItem 覆寫 child ref,\n // consumer 傳給 Chip 的自有 ref 在 menu layout 會被靜默取代。改 compose — 同時餵\n // registerItem(scrollIntoView 用)與 child 原 ref(對齊 Radix compose-refs idiom)。\n const enhancedItems = items.map((child, i) => {\n const childRef = getElementRef(child)\n return React.cloneElement(\n child as React.ReactElement<{ ref?: React.Ref<HTMLElement> }>,\n {\n ref: (el: HTMLElement | null) => {\n registerItem(i)(el)\n if (typeof childRef === 'function') childRef(el)\n else if (childRef) (childRef as React.MutableRefObject<HTMLElement | null>).current = el\n },\n }\n )\n })\n\n return (\n <div className=\"flex items-center gap-2\">\n <div\n ref={scrollRef}\n className=\"flex-1 min-w-0 overflow-x-auto [scrollbar-width:none] [&::-webkit-scrollbar]:hidden\"\n style={{ maskImage: menuMaskImage, WebkitMaskImage: menuMaskImage }}\n >\n <ToggleGroupPrimitive.Root\n ref={ref}\n className={cn('flex flex-nowrap gap-2 w-fit', className)}\n {...props}\n >\n {enhancedItems}\n </ToggleGroupPrimitive.Root>\n </div>\n {canScroll && (\n <DropdownMenu>\n <DropdownMenuTrigger asChild>\n <OverflowMenuTriggerButton\n aria-label={`選項選單(共 ${items.length} 個)`}\n />\n </DropdownMenuTrigger>\n <DropdownMenuContent align=\"end\">\n {items.map((chip, index) => {\n const chipProps = chip.props as { value?: string; children?: React.ReactNode; disabled?: boolean }\n const chipValue = chipProps.value\n if (typeof chipValue !== 'string') return null\n return (\n <DropdownMenuCheckboxItem\n key={chipValue}\n checked={isSelected(chipValue)}\n disabled={chipProps.disabled}\n onCheckedChange={() => toggleFromMenu(chipValue, index)}\n >\n {chipProps.children}\n </DropdownMenuCheckboxItem>\n )\n })}\n </DropdownMenuContent>\n </DropdownMenu>\n )}\n </div>\n )\n})\nMenuChipGroup.displayName = 'MenuChipGroup'\n\n// Story auto-compile metadata — Phase 1 mechanical migration(2026-04-24)\n// Phase 2 fill needed: purpose descriptions + when rationale + world-class refs\nexport const chipMeta = {\n component: 'Chip',\n family: 3,\n // 2026-07-04 audit 填 Phase 2:Chip 單一視覺無 variant 軸(variants 留空為 intentional,非漏填)\n variants: {},\n sizes: {\n // 單一尺寸:h-field-sm(28/32 依 density)、icon 16、text-body、px-3(cva 真值)\n sm: { fieldHeight: 28, iconSize: 16, typography: 'body' },\n },\n states: ['default', 'hover', 'selected', 'focus-visible', 'disabled'], // selected = data-[state=on](primary base,2026-07-06 拍板);cva 無 active 樣式\n tokens: {\n bg: ['bg-surface'],\n fg: ['text-fg-disabled', 'text-fg-secondary', 'text-foreground', 'text-primary'],\n ring: ['ring-ring'],\n },\n} as const\n\nexport { Chip, ChipGroup, chipVariants }\n"],"names":[],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"coachmark.d.ts","sourceRoot":"","sources":["../../../src/components/Coachmark/coachmark.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAA;AAe9B;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AAEH,UAAU,aAAa;IACrB,OAAO,EAAE,MAAM,CAAA;IACf,KAAK,EAAE,MAAM,CAAA;CACd;AAED,MAAM,WAAW,cAAe,SAAQ,IAAI,CAAC,KAAK,CAAC,cAAc,CAAC,cAAc,CAAC,EAAE,UAAU,GAAG,OAAO,CAAC;IACtG,2BAA2B;IAC3B,IAAI,CAAC,EAAE,OAAO,CAAA;IACd,sGAAsG;IACtG,WAAW,CAAC,EAAE,OAAO,CAAA;IACrB,YAAY,CAAC,EAAE,CAAC,IAAI,EAAE,OAAO,KAAK,IAAI,CAAA;IACtC,yDAAyD;IACzD,QAAQ,EAAE,KAAK,CAAC,SAAS,CAAA;IACzB,yDAAyD;IACzD,KAAK,CAAC,EAAE,KAAK,CAAC,SAAS,CAAA;IACvB;;;;OAIG;IACH,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB;;;;;;OAMG;IACH,IAAI,CAAC,EAAE,MAAM,GAAG,cAAc,GAAG,KAAK,CAAC,SAAS,CAAA;IAChD,eAAe;IACf,KAAK,CAAC,EAAE,KAAK,CAAC,SAAS,CAAA;IACvB,gCAAgC;IAChC,WAAW,CAAC,EAAE,KAAK,CAAC,SAAS,CAAA;IAC7B,gDAAgD;IAChD,IAAI,CAAC,EAAE,aAAa,CAAA;IACpB,+DAA+D;IAC/D,MAAM,CAAC,EAAE,MAAM,IAAI,CAAA;IACnB,mCAAmC;IACnC,MAAM,CAAC,EAAE,MAAM,IAAI,CAAA;IACnB,oDAAoD;IACpD,MAAM,CAAC,EAAE,MAAM,IAAI,CAAA;IACnB;;;;;OAKG;IACH,UAAU,CAAC,EAAE,OAAO,CAAA;IACpB,kDAAkD;IAClD,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,6BAA6B;IAC7B,IAAI,CAAC,EAAE,KAAK,GAAG,OAAO,GAAG,QAAQ,GAAG,MAAM,CAAA;IAC1C,KAAK,CAAC,EAAE,OAAO,GAAG,QAAQ,GAAG,KAAK,CAAA;IAClC,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,yDAAyD;IACzD,SAAS,CAAC,EAAE,MAAM,CAAA;CACnB;AASD,QAAA,MAAM,SAAS,
|
|
1
|
+
{"version":3,"file":"coachmark.d.ts","sourceRoot":"","sources":["../../../src/components/Coachmark/coachmark.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAA;AAe9B;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AAEH,UAAU,aAAa;IACrB,OAAO,EAAE,MAAM,CAAA;IACf,KAAK,EAAE,MAAM,CAAA;CACd;AAED,MAAM,WAAW,cAAe,SAAQ,IAAI,CAAC,KAAK,CAAC,cAAc,CAAC,cAAc,CAAC,EAAE,UAAU,GAAG,OAAO,CAAC;IACtG,2BAA2B;IAC3B,IAAI,CAAC,EAAE,OAAO,CAAA;IACd,sGAAsG;IACtG,WAAW,CAAC,EAAE,OAAO,CAAA;IACrB,YAAY,CAAC,EAAE,CAAC,IAAI,EAAE,OAAO,KAAK,IAAI,CAAA;IACtC,yDAAyD;IACzD,QAAQ,EAAE,KAAK,CAAC,SAAS,CAAA;IACzB,yDAAyD;IACzD,KAAK,CAAC,EAAE,KAAK,CAAC,SAAS,CAAA;IACvB;;;;OAIG;IACH,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB;;;;;;OAMG;IACH,IAAI,CAAC,EAAE,MAAM,GAAG,cAAc,GAAG,KAAK,CAAC,SAAS,CAAA;IAChD,eAAe;IACf,KAAK,CAAC,EAAE,KAAK,CAAC,SAAS,CAAA;IACvB,gCAAgC;IAChC,WAAW,CAAC,EAAE,KAAK,CAAC,SAAS,CAAA;IAC7B,gDAAgD;IAChD,IAAI,CAAC,EAAE,aAAa,CAAA;IACpB,+DAA+D;IAC/D,MAAM,CAAC,EAAE,MAAM,IAAI,CAAA;IACnB,mCAAmC;IACnC,MAAM,CAAC,EAAE,MAAM,IAAI,CAAA;IACnB,oDAAoD;IACpD,MAAM,CAAC,EAAE,MAAM,IAAI,CAAA;IACnB;;;;;OAKG;IACH,UAAU,CAAC,EAAE,OAAO,CAAA;IACpB,kDAAkD;IAClD,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,6BAA6B;IAC7B,IAAI,CAAC,EAAE,KAAK,GAAG,OAAO,GAAG,QAAQ,GAAG,MAAM,CAAA;IAC1C,KAAK,CAAC,EAAE,OAAO,GAAG,QAAQ,GAAG,KAAK,CAAA;IAClC,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,yDAAyD;IACzD,SAAS,CAAC,EAAE,MAAM,CAAA;CACnB;AASD,QAAA,MAAM,SAAS,uFA2Id,CAAA;AAKD,eAAO,MAAM,aAAa;;;;;;;;;;;CAehB,CAAA;AAEV,OAAO,EAAE,SAAS,EAAE,CAAA"}
|
|
@@ -39,8 +39,8 @@ const Coachmark = React.forwardRef(
|
|
|
39
39
|
const hasFooterContent = Boolean(step || showSkip || onNext || onPrev);
|
|
40
40
|
const stepText = step ? `${step.current} of ${step.total}` : null;
|
|
41
41
|
const headerTitle = kind === "tips" || kind === "new-features" ? KIND_TITLE[kind] : kind;
|
|
42
|
-
const
|
|
43
|
-
const
|
|
42
|
+
const bodyTitleId = React.useId();
|
|
43
|
+
const bodyTitleLabelledBy = !headerTitle && title ? bodyTitleId : void 0;
|
|
44
44
|
return /* @__PURE__ */ jsxs(Popover, { open, defaultOpen, onOpenChange, children: [
|
|
45
45
|
/* @__PURE__ */ jsx(PopoverTrigger, { asChild: true, children }),
|
|
46
46
|
/* @__PURE__ */ jsxs(
|
|
@@ -55,21 +55,21 @@ const Coachmark = React.forwardRef(
|
|
|
55
55
|
e.preventDefault();
|
|
56
56
|
e.currentTarget.focus({ preventScroll: true });
|
|
57
57
|
},
|
|
58
|
-
"aria-labelledby":
|
|
59
|
-
"aria-label":
|
|
58
|
+
"aria-labelledby": bodyTitleLabelledBy,
|
|
59
|
+
"aria-label": headerTitle || title ? void 0 : "提示",
|
|
60
60
|
...props,
|
|
61
61
|
children: [
|
|
62
62
|
headerTitle && // Header title 走 `<PopoverTitle>` 共用 Popover typography canonical(text-body font-medium)。
|
|
63
63
|
// **不 hideClose** — 對齊 Popover / Dialog / 所有 overlay 家族 canonical:header 必有 dismiss X
|
|
64
64
|
// (user 可隨時關閉,跟 Skip / Done 是不同入口,canonical 重複不冗)
|
|
65
|
-
/* @__PURE__ */ jsx(PopoverHeader, { children: /* @__PURE__ */ jsx(PopoverTitle, {
|
|
65
|
+
/* @__PURE__ */ jsx(PopoverHeader, { children: /* @__PURE__ */ jsx(PopoverTitle, { children: headerTitle }) }),
|
|
66
66
|
image && /* @__PURE__ */ jsx(AspectRatio, { ratio: mediaRatio, className: "w-full overflow-hidden bg-muted", children: image }),
|
|
67
67
|
(title || description) && // 對齊 Dialog / Popover canonical:body 左對齊(不中置)
|
|
68
68
|
// Why: Coachmark 雖是 onboarding / feature discovery,但文字可讀性 > 視覺焦點;
|
|
69
69
|
// 中文 / 長句中置會「每行起點不同」造成閱讀鋸齒,左對齊最穩。
|
|
70
70
|
// 世界級參考:Notion / Linear / Figma onboarding tour 皆左對齊;Intercom Messenger 亦如是。
|
|
71
71
|
/* @__PURE__ */ jsxs(PopoverBody, { className: "flex flex-col", children: [
|
|
72
|
-
title && /* @__PURE__ */ jsx("h3", { id: !headerTitle ?
|
|
72
|
+
title && /* @__PURE__ */ jsx("h3", { id: !headerTitle ? bodyTitleId : void 0, className: "text-body-lg font-medium text-foreground", children: title }),
|
|
73
73
|
description && // title(body-lg 16)+ desc(body 14)→ reading-lg token(label tier 決定)
|
|
74
74
|
// 對齊 Dialog / Sheet canonical;移除原 gap-1(4px)drift
|
|
75
75
|
/* @__PURE__ */ jsx("p", { className: "mt-[var(--item-gap-label-desc-reading-lg)] text-body text-fg-secondary", children: description })
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"coachmark.js","sources":["../../../src/components/Coachmark/coachmark.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.\nimport * as React from 'react'\nimport { cn } from '@/lib/utils'\nimport {\n Popover,\n PopoverTrigger,\n PopoverContent,\n PopoverBody,\n PopoverFooter,\n PopoverHeader,\n PopoverTitle,\n} from '@/design-system/components/Popover/popover'\nimport { Button } from '@/design-system/components/Button/button'\nimport { AspectRatio } from '@/design-system/components/AspectRatio/aspect-ratio'\nimport { OVERLAY_SIDE_OFFSET } from '@/design-system/tokens/elevation/overlay-geometry'\n\n/**\n * Coachmark — 功能介紹 / onboarding tour 的浮層卡片\n *\n * 世界級對照:Apple HIG「Coachmark」(Apple 命名原處)/ Material「Feature Discovery」/\n * Ant Design `<Tour>` / Shepherd.js / react-joyride / Intercom Product Tours。\n *\n * 本元件 = **Popover 的 composition pattern**,consume 相同 overlay-surface SSOT:\n * - Header(可選,多步驟建議傳 `kind=\"tips\" | \"new-features\" | 自訂 title`,\n * single-step 預設無 header 避免視覺過重)\n * - Media 區(圖 / 截圖 / illustration,full-width 邊緣對齊,由 AspectRatio 管比例)\n * - Body(SurfaceBody padding):title + description 左對齊\n * - Footer(SurfaceFooter padding,但 justify-between):step 計數左 / actions 右\n *\n * ── 單 vs 多步驟 canonical(世界級行為規則) ──\n * 1. **Single step**(無 `onPrev` 且 `isLastStep`):CTA 文字 = `doneLabel ?? '知道了'`\n * (Apple HIG / Intercom 慣例;不用 \"Next\" 因為沒有下一步)\n * 2. **Multi step 第一步**(無 `onPrev`,有 `onNext`):CTA = \"Next\",Skip 顯示\n * 3. **Multi step 中間 / 最後步**(有 `onPrev`):**Skip 不顯示**(使用者已投入進度,\n * 再給 Skip 會讓「放棄」入口與「回上一步」衝突 — Linear / Pendo / Shepherd.js\n * 同樣規則)。CTA = `isLastStep ? 'Done' : 'Next'`\n * 4. **抑制 CTA 自動 focus,焦點落在 content 容器** — `onOpenAutoFocus` preventDefault\n * 後 focus content 容器本身(非第一個 CTA),避免使用者還在讀 body 時按 Enter 誤推進;\n * Tab 第一下即達 content 內第一個 CTA。焦點不可停在 trigger:non-modal Popover 下\n * 焦點在 DismissableLayer 外按 Tab 會觸發 focus-outside dismiss 誤關閉\n * (2026-07-05 D4;對齊 dialog.tsx handleOpenAutoFocus `?? content` fallback,見 spec「A11y 預設」)。\n *\n * ── 為什麼 Body+Footer 消費 overlay-surface ──\n * 避免 padding token 漂移:Dialog / Popover / Coachmark 三者共用同一套 Header/Body/Footer\n * padding(px-loose / py-tight),改 Dialog 就三邊自動跟進。\n */\n\ninterface CoachmarkStep {\n current: number\n total: number\n}\n\nexport interface CoachmarkProps extends Omit<React.HTMLAttributes<HTMLDivElement>, 'children' | 'title'> {\n /** 控制顯示(controlled mode)*/\n open?: boolean\n /** 預設打開(uncontrolled initial state)— 2026-05-15 audit Dim 26 V1 fix per user verbatim「A:1」approval */\n defaultOpen?: boolean\n onOpenChange?: (open: boolean) => void\n /** 觸發 anchor 元素。通常傳 trigger element;Coachmark 浮層會定位於此 */\n children: React.ReactNode\n /** 頂部 media 區(圖片 / illustration / video 等);不傳則無 media */\n image?: React.ReactNode\n /**\n * Media 區域的長寬比(ratio = 寬 / 高)。預設 `16/9`(onboarding feature tour\n * 世界級 convention — Intercom / Pendo / Shepherd.js)。其他常用:`4/3` 產品截圖 /\n * `1/1` 方圖 / `3/4` 直式 portrait。消費獨立的 `AspectRatio` primitive 元件。\n */\n mediaRatio?: number\n /**\n * 頂部 header 類型(多步驟 tour 建議傳)。\n * - `'tips'` → header title = \"使用技巧\"\n * - `'new-features'` → header title = \"新功能介紹\"\n * - `ReactNode` → 自訂 title(string / JSX)\n * - undefined → 無 header(單步驟常用)\n */\n kind?: 'tips' | 'new-features' | React.ReactNode\n /** 標題(bold) */\n title?: React.ReactNode\n /** 說明文字(normal weight,多行 OK) */\n description?: React.ReactNode\n /** 步驟計數(2 of 3);若需多步導覽 consumer 自行管理 current */\n step?: CoachmarkStep\n /** Skip 按鈕 callback;不傳則不顯示 Skip。多步驟中間 / 最後步自動隱藏(有 onPrev 時) */\n onSkip?: () => void\n /** Next 按鈕 callback;不傳則不顯示 Next */\n onNext?: () => void\n /** Previous 按鈕 callback(多步 tour 第 2+ 步顯示);不傳則不顯示 */\n onPrev?: () => void\n /**\n * 最後一步 flag。影響 primary CTA 文字:\n * - single step(無 onPrev 且 isLastStep):CTA = `doneLabel ?? '知道了'`\n * - multi-step 最後步(有 onPrev 且 isLastStep):CTA = 'Done'\n * - 其他:CTA = 'Next'\n */\n isLastStep?: boolean\n /** 自訂單步驟完成 CTA 文字(預設 `'知道了'`)。僅 single-step 使用 */\n doneLabel?: string\n /** 浮層定位(對齊 Popover props) */\n side?: 'top' | 'right' | 'bottom' | 'left'\n align?: 'start' | 'center' | 'end'\n sideOffset?: number\n /** 外殼寬度(預設 w-80 = 320px,比一般 Popover 寬,因要放 media + 文字) */\n className?: string\n}\n\n// i18n-allow: DS-internal kind → title 預設對照表;consumer 透過 `title` prop 直接覆寫\nconst KIND_TITLE: Record<'tips' | 'new-features', string> = {\n tips: '使用技巧',\n 'new-features': '新功能介紹',\n}\n\n// code-quality-allow: long-function — foundational composite main body — 拆 sub-fn 會複雜化 local state / ref / context binding\nconst Coachmark = React.forwardRef<HTMLDivElement, CoachmarkProps>(\n (\n {\n open,\n defaultOpen,\n onOpenChange,\n children,\n image,\n mediaRatio = 16 / 9,\n kind,\n title,\n description,\n step,\n onSkip,\n onNext,\n onPrev,\n isLastStep = false,\n doneLabel = '知道了', // i18n-allow: DS default; consumer override via doneLabel prop\n side = 'bottom',\n align = 'center',\n sideOffset = OVERLAY_SIDE_OFFSET,\n className,\n ...props\n },\n ref,\n ) => {\n // 單/多步驟行為推導\n const isSingleStep = isLastStep && !onPrev\n // canonical(spec.md CTA 語義表 L116):有 onPrev → 不顯示 Skip;**單步驟也不顯示 Skip**\n // (單步「skip」與「知道了」語義重複 = 同一個關閉動作,雙關閉鍵造成困惑;對齊 Apple HIG / Intercom / Pendo 單步 tip 僅一個完成鍵)\n const showSkip = Boolean(onSkip) && !onPrev && !isSingleStep\n const nextLabel = isSingleStep ? doneLabel : isLastStep ? 'Done' : 'Next'\n\n const hasFooterContent = Boolean(step || showSkip || onNext || onPrev)\n const stepText = step ? `${step.current} of ${step.total}` : null\n\n // Header title 解析\n const headerTitle =\n kind === 'tips' || kind === 'new-features'\n ? KIND_TITLE[kind as 'tips' | 'new-features']\n : kind\n\n // 2026-05-31 #6:PopoverContent(role=dialog)需 accessible name。dialog aria-labelledby 接可見標題\n // (優先 headerTitle 的 PopoverTitle,否則 body title 的 h3);皆無則 aria-label fallback。\n const titleId = React.useId()\n const dialogLabelledBy = (headerTitle || title) ? titleId : undefined\n\n return (\n <Popover open={open} defaultOpen={defaultOpen} onOpenChange={onOpenChange}>\n <PopoverTrigger asChild>{children}</PopoverTrigger>\n <PopoverContent\n ref={ref}\n side={side}\n align={align}\n sideOffset={sideOffset}\n className={cn('w-80 p-0 overflow-hidden', className)}\n // 禁止 Radix 開啟時自動 focus 第一個 focusable(預設會 focus Prev / Skip / Next),\n // Coachmark 的 CTA 不該被 auto-focus 偷觸發(user 可能還在讀 body,按 Enter 就推進)。\n // 2026-07-05 D4:preventDefault 後必 focus content 容器本身(FocusScope 容器自帶 tabIndex=-1)——\n // 焦點若停在 trigger(DismissableLayer 外),non-modal Popover 按 Tab 即觸發 focus-outside\n // dismiss 直接關閉,CTA 鍵盤永遠到不了;focus 容器後 Tab 第一下即達第一個 CTA,Enter 按在\n // 容器上無動作(原「不誤推進」意圖保留;對齊 dialog.tsx handleOpenAutoFocus `?? content` fallback)。\n onOpenAutoFocus={(e) => {\n e.preventDefault()\n ;(e.currentTarget as HTMLElement).focus({ preventScroll: true })\n }}\n aria-labelledby={dialogLabelledBy}\n aria-label={dialogLabelledBy ? undefined : '提示'}\n {...props}\n >\n {headerTitle && (\n // Header title 走 `<PopoverTitle>` 共用 Popover typography canonical(text-body font-medium)。\n // **不 hideClose** — 對齊 Popover / Dialog / 所有 overlay 家族 canonical:header 必有 dismiss X\n // (user 可隨時關閉,跟 Skip / Done 是不同入口,canonical 重複不冗)\n <PopoverHeader>\n <PopoverTitle id={titleId}>{headerTitle}</PopoverTitle>\n </PopoverHeader>\n )}\n\n {image && (\n <AspectRatio ratio={mediaRatio} className=\"w-full overflow-hidden bg-muted\">\n {image}\n </AspectRatio>\n )}\n\n {(title || description) && (\n // 對齊 Dialog / Popover canonical:body 左對齊(不中置)\n // Why: Coachmark 雖是 onboarding / feature discovery,但文字可讀性 > 視覺焦點;\n // 中文 / 長句中置會「每行起點不同」造成閱讀鋸齒,左對齊最穩。\n // 世界級參考:Notion / Linear / Figma onboarding tour 皆左對齊;Intercom Messenger 亦如是。\n <PopoverBody className=\"flex flex-col\">\n {title && (\n <h3 id={!headerTitle ? titleId : undefined} className=\"text-body-lg font-medium text-foreground\">{title}</h3>\n )}\n {description && (\n // title(body-lg 16)+ desc(body 14)→ reading-lg token(label tier 決定)\n // 對齊 Dialog / Sheet canonical;移除原 gap-1(4px)drift\n <p className=\"mt-[var(--item-gap-label-desc-reading-lg)] text-body text-fg-secondary\">{description}</p>\n )}\n </PopoverBody>\n )}\n\n {hasFooterContent && (\n // 專屬 Coachmark canonical:footer 無上方分隔線(media + body + footer 視覺一氣呵成,\n // 不像 Dialog 需要 header/footer 分隔強化結構)。override SurfaceFooter default border-t\n <PopoverFooter className=\"justify-between !border-t-0\">\n {stepText ? (\n // step 文字走 text-body 跟 body content 字體一致\n <span className=\"text-body text-fg-secondary tabular-nums\">\n {stepText}\n </span>\n ) : (\n <span aria-hidden /> /* 保持 justify-between space */\n )}\n <div className=\"flex items-center gap-2\">\n {onPrev && (\n <Button variant=\"tertiary\" size=\"sm\" onClick={onPrev}>\n Previous\n </Button>\n )}\n {showSkip && (\n <Button variant=\"tertiary\" size=\"sm\" onClick={onSkip}>\n Skip\n </Button>\n )}\n {onNext && (\n <Button variant=\"primary\" size=\"sm\" onClick={onNext}>\n {nextLabel}\n </Button>\n )}\n </div>\n </PopoverFooter>\n )}\n </PopoverContent>\n </Popover>\n )\n },\n)\nCoachmark.displayName = 'Coachmark'\n\n// Story auto-compile metadata — Phase 1 mechanical migration(2026-04-24)\n// Phase 2 fill needed: purpose descriptions + when rationale + world-class refs\nexport const coachmarkMeta = {\n component: 'Coachmark',\n family: null, // non-family composite / overlay / layout\n variants: {\n\n },\n sizes: {\n\n },\n states: ['default'], // 純 Popover composition — 無自有 hover / active / disabled 視覺(spec「邊界狀態」)\n tokens: {\n bg: ['bg-muted'],\n fg: ['text-fg-secondary', 'text-foreground'],\n ring: [],\n },\n} as const\n\nexport { Coachmark }\n"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"coachmark.js","sources":["../../../src/components/Coachmark/coachmark.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.\nimport * as React from 'react'\nimport { cn } from '@/lib/utils'\nimport {\n Popover,\n PopoverTrigger,\n PopoverContent,\n PopoverBody,\n PopoverFooter,\n PopoverHeader,\n PopoverTitle,\n} from '@/design-system/components/Popover/popover'\nimport { Button } from '@/design-system/components/Button/button'\nimport { AspectRatio } from '@/design-system/components/AspectRatio/aspect-ratio'\nimport { OVERLAY_SIDE_OFFSET } from '@/design-system/tokens/elevation/overlay-geometry'\n\n/**\n * Coachmark — 功能介紹 / onboarding tour 的浮層卡片\n *\n * 世界級對照:Apple HIG「Coachmark」(Apple 命名原處)/ Material「Feature Discovery」/\n * Ant Design `<Tour>` / Shepherd.js / react-joyride / Intercom Product Tours。\n *\n * 本元件 = **Popover 的 composition pattern**,consume 相同 overlay-surface SSOT:\n * - Header(可選,多步驟建議傳 `kind=\"tips\" | \"new-features\" | 自訂 title`,\n * single-step 預設無 header 避免視覺過重)\n * - Media 區(圖 / 截圖 / illustration,full-width 邊緣對齊,由 AspectRatio 管比例)\n * - Body(SurfaceBody padding):title + description 左對齊\n * - Footer(SurfaceFooter padding,但 justify-between):step 計數左 / actions 右\n *\n * ── 單 vs 多步驟 canonical(世界級行為規則) ──\n * 1. **Single step**(無 `onPrev` 且 `isLastStep`):CTA 文字 = `doneLabel ?? '知道了'`\n * (Apple HIG / Intercom 慣例;不用 \"Next\" 因為沒有下一步)\n * 2. **Multi step 第一步**(無 `onPrev`,有 `onNext`):CTA = \"Next\",Skip 顯示\n * 3. **Multi step 中間 / 最後步**(有 `onPrev`):**Skip 不顯示**(使用者已投入進度,\n * 再給 Skip 會讓「放棄」入口與「回上一步」衝突 — Linear / Pendo / Shepherd.js\n * 同樣規則)。CTA = `isLastStep ? 'Done' : 'Next'`\n * 4. **抑制 CTA 自動 focus,焦點落在 content 容器** — `onOpenAutoFocus` preventDefault\n * 後 focus content 容器本身(非第一個 CTA),避免使用者還在讀 body 時按 Enter 誤推進;\n * Tab 第一下即達 content 內第一個 CTA。焦點不可停在 trigger:non-modal Popover 下\n * 焦點在 DismissableLayer 外按 Tab 會觸發 focus-outside dismiss 誤關閉\n * (2026-07-05 D4;對齊 dialog.tsx handleOpenAutoFocus `?? content` fallback,見 spec「A11y 預設」)。\n *\n * ── 為什麼 Body+Footer 消費 overlay-surface ──\n * 避免 padding token 漂移:Dialog / Popover / Coachmark 三者共用同一套 Header/Body/Footer\n * padding(px-loose / py-tight),改 Dialog 就三邊自動跟進。\n */\n\ninterface CoachmarkStep {\n current: number\n total: number\n}\n\nexport interface CoachmarkProps extends Omit<React.HTMLAttributes<HTMLDivElement>, 'children' | 'title'> {\n /** 控制顯示(controlled mode)*/\n open?: boolean\n /** 預設打開(uncontrolled initial state)— 2026-05-15 audit Dim 26 V1 fix per user verbatim「A:1」approval */\n defaultOpen?: boolean\n onOpenChange?: (open: boolean) => void\n /** 觸發 anchor 元素。通常傳 trigger element;Coachmark 浮層會定位於此 */\n children: React.ReactNode\n /** 頂部 media 區(圖片 / illustration / video 等);不傳則無 media */\n image?: React.ReactNode\n /**\n * Media 區域的長寬比(ratio = 寬 / 高)。預設 `16/9`(onboarding feature tour\n * 世界級 convention — Intercom / Pendo / Shepherd.js)。其他常用:`4/3` 產品截圖 /\n * `1/1` 方圖 / `3/4` 直式 portrait。消費獨立的 `AspectRatio` primitive 元件。\n */\n mediaRatio?: number\n /**\n * 頂部 header 類型(多步驟 tour 建議傳)。\n * - `'tips'` → header title = \"使用技巧\"\n * - `'new-features'` → header title = \"新功能介紹\"\n * - `ReactNode` → 自訂 title(string / JSX)\n * - undefined → 無 header(單步驟常用)\n */\n kind?: 'tips' | 'new-features' | React.ReactNode\n /** 標題(bold) */\n title?: React.ReactNode\n /** 說明文字(normal weight,多行 OK) */\n description?: React.ReactNode\n /** 步驟計數(2 of 3);若需多步導覽 consumer 自行管理 current */\n step?: CoachmarkStep\n /** Skip 按鈕 callback;不傳則不顯示 Skip。多步驟中間 / 最後步自動隱藏(有 onPrev 時) */\n onSkip?: () => void\n /** Next 按鈕 callback;不傳則不顯示 Next */\n onNext?: () => void\n /** Previous 按鈕 callback(多步 tour 第 2+ 步顯示);不傳則不顯示 */\n onPrev?: () => void\n /**\n * 最後一步 flag。影響 primary CTA 文字:\n * - single step(無 onPrev 且 isLastStep):CTA = `doneLabel ?? '知道了'`\n * - multi-step 最後步(有 onPrev 且 isLastStep):CTA = 'Done'\n * - 其他:CTA = 'Next'\n */\n isLastStep?: boolean\n /** 自訂單步驟完成 CTA 文字(預設 `'知道了'`)。僅 single-step 使用 */\n doneLabel?: string\n /** 浮層定位(對齊 Popover props) */\n side?: 'top' | 'right' | 'bottom' | 'left'\n align?: 'start' | 'center' | 'end'\n sideOffset?: number\n /** 外殼寬度(預設 w-80 = 320px,比一般 Popover 寬,因要放 media + 文字) */\n className?: string\n}\n\n// i18n-allow: DS-internal kind → title 預設對照表;consumer 透過 `title` prop 直接覆寫\nconst KIND_TITLE: Record<'tips' | 'new-features', string> = {\n tips: '使用技巧',\n 'new-features': '新功能介紹',\n}\n\n// code-quality-allow: long-function — foundational composite main body — 拆 sub-fn 會複雜化 local state / ref / context binding\nconst Coachmark = React.forwardRef<HTMLDivElement, CoachmarkProps>(\n (\n {\n open,\n defaultOpen,\n onOpenChange,\n children,\n image,\n mediaRatio = 16 / 9,\n kind,\n title,\n description,\n step,\n onSkip,\n onNext,\n onPrev,\n isLastStep = false,\n doneLabel = '知道了', // i18n-allow: DS default; consumer override via doneLabel prop\n side = 'bottom',\n align = 'center',\n sideOffset = OVERLAY_SIDE_OFFSET,\n className,\n ...props\n },\n ref,\n ) => {\n // 單/多步驟行為推導\n const isSingleStep = isLastStep && !onPrev\n // canonical(spec.md CTA 語義表 L116):有 onPrev → 不顯示 Skip;**單步驟也不顯示 Skip**\n // (單步「skip」與「知道了」語義重複 = 同一個關閉動作,雙關閉鍵造成困惑;對齊 Apple HIG / Intercom / Pendo 單步 tip 僅一個完成鍵)\n const showSkip = Boolean(onSkip) && !onPrev && !isSingleStep\n const nextLabel = isSingleStep ? doneLabel : isLastStep ? 'Done' : 'Next'\n\n const hasFooterContent = Boolean(step || showSkip || onNext || onPrev)\n const stepText = step ? `${step.current} of ${step.total}` : null\n\n // Header title 解析\n const headerTitle =\n kind === 'tips' || kind === 'new-features'\n ? KIND_TITLE[kind as 'tips' | 'new-features']\n : kind\n\n // 2026-05-31 #6 / 2026-07-05 改消費 Popover 自動接線:PopoverContent(role=dialog)accessible name 三層 —\n // 1. headerTitle → <PopoverTitle> 掛載時由 popover.tsx PopoverTitleContext 自動接 aria-labelledby(免手動)\n // 2. 無 header 但有 body 主 title(<h3> 非 PopoverTitle,自動接線接不到)→ 仍手動接 aria-labelledby\n // 3. 皆無 → aria-label fallback\n const bodyTitleId = React.useId()\n const bodyTitleLabelledBy = !headerTitle && title ? bodyTitleId : undefined\n\n return (\n <Popover open={open} defaultOpen={defaultOpen} onOpenChange={onOpenChange}>\n <PopoverTrigger asChild>{children}</PopoverTrigger>\n <PopoverContent\n ref={ref}\n side={side}\n align={align}\n sideOffset={sideOffset}\n className={cn('w-80 p-0 overflow-hidden', className)}\n // 禁止 Radix 開啟時自動 focus 第一個 focusable(預設會 focus Prev / Skip / Next),\n // Coachmark 的 CTA 不該被 auto-focus 偷觸發(user 可能還在讀 body,按 Enter 就推進)。\n // 2026-07-05 D4:preventDefault 後必 focus content 容器本身(FocusScope 容器自帶 tabIndex=-1)——\n // 焦點若停在 trigger(DismissableLayer 外),non-modal Popover 按 Tab 即觸發 focus-outside\n // dismiss 直接關閉,CTA 鍵盤永遠到不了;focus 容器後 Tab 第一下即達第一個 CTA,Enter 按在\n // 容器上無動作(原「不誤推進」意圖保留;對齊 dialog.tsx handleOpenAutoFocus `?? content` fallback)。\n onOpenAutoFocus={(e) => {\n e.preventDefault()\n ;(e.currentTarget as HTMLElement).focus({ preventScroll: true })\n }}\n aria-labelledby={bodyTitleLabelledBy}\n aria-label={headerTitle || title ? undefined : '提示'}\n {...props}\n >\n {headerTitle && (\n // Header title 走 `<PopoverTitle>` 共用 Popover typography canonical(text-body font-medium)。\n // **不 hideClose** — 對齊 Popover / Dialog / 所有 overlay 家族 canonical:header 必有 dismiss X\n // (user 可隨時關閉,跟 Skip / Done 是不同入口,canonical 重複不冗)\n <PopoverHeader>\n <PopoverTitle>{headerTitle}</PopoverTitle>\n </PopoverHeader>\n )}\n\n {image && (\n <AspectRatio ratio={mediaRatio} className=\"w-full overflow-hidden bg-muted\">\n {image}\n </AspectRatio>\n )}\n\n {(title || description) && (\n // 對齊 Dialog / Popover canonical:body 左對齊(不中置)\n // Why: Coachmark 雖是 onboarding / feature discovery,但文字可讀性 > 視覺焦點;\n // 中文 / 長句中置會「每行起點不同」造成閱讀鋸齒,左對齊最穩。\n // 世界級參考:Notion / Linear / Figma onboarding tour 皆左對齊;Intercom Messenger 亦如是。\n <PopoverBody className=\"flex flex-col\">\n {title && (\n <h3 id={!headerTitle ? bodyTitleId : undefined} className=\"text-body-lg font-medium text-foreground\">{title}</h3>\n )}\n {description && (\n // title(body-lg 16)+ desc(body 14)→ reading-lg token(label tier 決定)\n // 對齊 Dialog / Sheet canonical;移除原 gap-1(4px)drift\n <p className=\"mt-[var(--item-gap-label-desc-reading-lg)] text-body text-fg-secondary\">{description}</p>\n )}\n </PopoverBody>\n )}\n\n {hasFooterContent && (\n // 專屬 Coachmark canonical:footer 無上方分隔線(media + body + footer 視覺一氣呵成,\n // 不像 Dialog 需要 header/footer 分隔強化結構)。override SurfaceFooter default border-t\n <PopoverFooter className=\"justify-between !border-t-0\">\n {stepText ? (\n // step 文字走 text-body 跟 body content 字體一致\n <span className=\"text-body text-fg-secondary tabular-nums\">\n {stepText}\n </span>\n ) : (\n <span aria-hidden /> /* 保持 justify-between space */\n )}\n <div className=\"flex items-center gap-2\">\n {onPrev && (\n <Button variant=\"tertiary\" size=\"sm\" onClick={onPrev}>\n Previous\n </Button>\n )}\n {showSkip && (\n <Button variant=\"tertiary\" size=\"sm\" onClick={onSkip}>\n Skip\n </Button>\n )}\n {onNext && (\n <Button variant=\"primary\" size=\"sm\" onClick={onNext}>\n {nextLabel}\n </Button>\n )}\n </div>\n </PopoverFooter>\n )}\n </PopoverContent>\n </Popover>\n )\n },\n)\nCoachmark.displayName = 'Coachmark'\n\n// Story auto-compile metadata — Phase 1 mechanical migration(2026-04-24)\n// Phase 2 fill needed: purpose descriptions + when rationale + world-class refs\nexport const coachmarkMeta = {\n component: 'Coachmark',\n family: null, // non-family composite / overlay / layout\n variants: {\n\n },\n sizes: {\n\n },\n states: ['default'], // 純 Popover composition — 無自有 hover / active / disabled 視覺(spec「邊界狀態」)\n tokens: {\n bg: ['bg-muted'],\n fg: ['text-fg-secondary', 'text-foreground'],\n ring: [],\n },\n} as const\n\nexport { Coachmark }\n"],"names":[],"mappings":"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"}
|
|
@@ -150,7 +150,7 @@ export declare const comboboxMeta: {
|
|
|
150
150
|
readonly family: 4;
|
|
151
151
|
readonly variants: {};
|
|
152
152
|
readonly sizes: {};
|
|
153
|
-
readonly states: readonly ["default", "hover", "
|
|
153
|
+
readonly states: readonly ["default", "hover", "focus-visible", "disabled"];
|
|
154
154
|
readonly tokens: {
|
|
155
155
|
readonly bg: readonly ["bg-disabled", "bg-transparent"];
|
|
156
156
|
readonly fg: readonly ["text-fg-disabled", "text-fg-muted"];
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"combobox.d.ts","sourceRoot":"","sources":["../../../src/components/Combobox/combobox.tsx"],"names":[],"mappings":"AAGA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAA;AAG9B,OAAO,KAAK,EAAE,SAAS,EAAE,YAAY,EAAE,MAAM,8CAA8C,CAAA;AAM3F,OAAO,
|
|
1
|
+
{"version":3,"file":"combobox.d.ts","sourceRoot":"","sources":["../../../src/components/Combobox/combobox.tsx"],"names":[],"mappings":"AAGA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAA;AAG9B,OAAO,KAAK,EAAE,SAAS,EAAE,YAAY,EAAE,MAAM,8CAA8C,CAAA;AAM3F,OAAO,EAAwD,KAAK,gBAAgB,EAAE,MAAM,mDAAmD,CAAA;AAc/I;;;;;;;;;;;;;;;;;GAiBG;AACH,MAAM,WAAW,cAAe,SAAQ,gBAAgB;CAEvD;AAgLD,KAAK,qBAAqB,GAAG,QAAQ,GAAG,KAAK,CAAA;AA8I7C,MAAM,WAAW,aAAa;IAC5B,IAAI,CAAC,EAAE,SAAS,CAAA;IAChB,uFAAuF;IACvF,OAAO,CAAC,EAAE,YAAY,CAAA;IACtB,KAAK,CAAC,EAAE,OAAO,CAAA;IACf,IAAI,CAAC,EAAE,IAAI,GAAG,IAAI,GAAG,IAAI,CAAA;IACzB,OAAO,EAAE,cAAc,EAAE,CAAA;IACzB,KAAK,CAAC,EAAE,MAAM,EAAE,CAAA;IAChB,QAAQ,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,IAAI,CAAA;IACpC,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,QAAQ,CAAC,EAAE,OAAO,CAAA;IAClB,IAAI,CAAC,EAAE,OAAO,CAAA;IACd,SAAS,CAAC,EAAE,OAAO,CAAA;IACnB,WAAW;IACX,UAAU,CAAC,EAAE,OAAO,CAAA;IACpB;;kGAE8F;IAC9F,OAAO,CAAC,EAAE,OAAO,CAAA;IACjB,gDAAgD;IAChD,QAAQ,CAAC,EAAE,MAAM,GAAG,SAAS,CAAA;IAC7B,8CAA8C;IAC9C,iBAAiB,CAAC,EAAE,MAAM,CAAA;IAC1B,qCAAqC;IACrC,eAAe,CAAC,EAAE,MAAM,CAAA;IACxB,sDAAsD;IACtD,gBAAgB,CAAC,EAAE,MAAM,CAAA;IACzB,uFAAuF;IACvF,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,4FAA4F;IAC5F,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB;gDAC4C;IAC5C,WAAW,CAAC,EAAE,OAAO,CAAA;IACrB,mFAAmF;IACnF,YAAY,CAAC,EAAE,CAAC,IAAI,EAAE,OAAO,KAAK,IAAI,CAAA;IACtC;;;;;;;;OAQG;IACH,WAAW,CAAC,EAAE,CAAC,IAAI,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,EAAE,QAAQ,EAAE,MAAM,IAAI,KAAK,KAAK,CAAC,SAAS,CAAA;IAC/F;;;;;OAKG;IACH,eAAe,CAAC,EAAE,CAAC,IAAI,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,KAAK,KAAK,CAAC,SAAS,CAAA;IAC7E;;;;;;OAMG;IACH,mBAAmB,CAAC,EAAE,MAAM,CAAA;IAC5B;;;sEAGkE;IAClE,wBAAwB,CAAC,EAAE,MAAM,CAAA;IACjC;;;;;;;;OAQG;IACH,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB;;;;;;;;;;;;;;OAcG;IACH,oBAAoB,CAAC,EAAE,MAAM,CAAA;IAC7B;;;;OAIG;IACH,aAAa,CAAC,EAAE,qBAAqB,CAAA;IACrC;;;;;;;OAOG;IACH,oBAAoB,CAAC,EAAE,MAAM,CAAA;IAC7B;;;;;OAKG;IACH,kBAAkB,CAAC,EAAE,OAAO,CAAA;CAC7B;AAkXD,QAAA,MAAM,QAAQ,sFAab,CAAA;AAKD,eAAO,MAAM,YAAY;;;;;;;;;;;CAgBf,CAAA;AAEV,OAAO,EAAE,QAAQ,EAAE,CAAA"}
|
|
@@ -7,7 +7,7 @@ import { useResolvedFieldSize, useResolvedFieldDisabled, useResolvedFieldVariant
|
|
|
7
7
|
import { Tag } from "../Tag/tag.js";
|
|
8
8
|
import { ItemSuffix, ItemInlineAction, ICON_SIZE } from "../../patterns/element-anatomy/item-anatomy.js";
|
|
9
9
|
import { OverflowIndicator } from "../OverflowIndicator/overflow-indicator.js";
|
|
10
|
-
import { forwardKeyToListbox, SelectMenu } from "../SelectMenu/select-menu.js";
|
|
10
|
+
import { useActiveDescendant, forwardKeyToListbox, SelectMenu } from "../SelectMenu/select-menu.js";
|
|
11
11
|
import { useIsTouchDevice } from "../../hooks/use-is-touch-device.js";
|
|
12
12
|
const GAP = 4;
|
|
13
13
|
const tagPadding = {
|
|
@@ -428,6 +428,7 @@ function CustomCombobox({
|
|
|
428
428
|
const [search, setSearch] = React.useState("");
|
|
429
429
|
const inputRef = React.useRef(null);
|
|
430
430
|
const listboxId = React.useId();
|
|
431
|
+
const activeOptionId = useActiveDescendant(listboxId, open);
|
|
431
432
|
React.useEffect(() => {
|
|
432
433
|
if (!open) setSearch("");
|
|
433
434
|
}, [open]);
|
|
@@ -538,6 +539,7 @@ function CustomCombobox({
|
|
|
538
539
|
setOpen(true);
|
|
539
540
|
},
|
|
540
541
|
"aria-label": searchAriaLabel,
|
|
542
|
+
"aria-activedescendant": activeOptionId,
|
|
541
543
|
className: "flex-1 min-w-[60px] bg-transparent outline-none text-body leading-compact relative z-10"
|
|
542
544
|
}
|
|
543
545
|
) : void 0
|
|
@@ -617,7 +619,8 @@ const comboboxMeta = {
|
|
|
617
619
|
family: 4,
|
|
618
620
|
variants: {},
|
|
619
621
|
sizes: {},
|
|
620
|
-
|
|
622
|
+
// 'active' 移除 — trigger 走 Field chrome(field-wrapper 無按壓態)、menu row 走 MenuItem(其 meta 無 active)(2026-07-07 詞彙統一 DS-wide 按壓訊號盤點:檔內 0 active: utility / 0 *-active token)。
|
|
623
|
+
states: ["default", "hover", "focus-visible", "disabled"],
|
|
621
624
|
tokens: {
|
|
622
625
|
bg: ["bg-disabled", "bg-transparent"],
|
|
623
626
|
fg: ["text-fg-disabled", "text-fg-muted"],
|