@qijenchen/design-system 0.1.0-beta.145 → 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 +1 -1
- package/dist/components/Accordion/accordion.d.ts.map +1 -1
- package/dist/components/Accordion/accordion.js +8 -3
- 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 +36 -14
- 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 +1 -1
- package/dist/components/AgentPanel/agent-panel.d.ts.map +1 -1
- package/dist/components/AgentPanel/agent-panel.js +28 -22
- package/dist/components/AgentPanel/agent-panel.js.map +1 -1
- package/dist/components/AppShell/app-shell.d.ts +1 -1
- package/dist/components/AppShell/app-shell.js +1 -1
- package/dist/components/AppShell/app-shell.js.map +1 -1
- package/dist/components/Avatar/avatar.d.ts +95 -1
- package/dist/components/Avatar/avatar.d.ts.map +1 -1
- package/dist/components/Avatar/avatar.js +106 -15
- package/dist/components/Avatar/avatar.js.map +1 -1
- package/dist/components/Avatar/index.js +14 -2
- package/dist/components/Breadcrumb/breadcrumb.d.ts +4 -2
- package/dist/components/Breadcrumb/breadcrumb.d.ts.map +1 -1
- package/dist/components/Breadcrumb/breadcrumb.js +2 -2
- 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 +2 -2
- package/dist/components/Button/button.d.ts.map +1 -1
- package/dist/components/Button/button.js +118 -22
- package/dist/components/Button/button.js.map +1 -1
- package/dist/components/Calendar/calendar.d.ts +34 -10
- package/dist/components/Calendar/calendar.d.ts.map +1 -1
- package/dist/components/Calendar/calendar.js +210 -36
- 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 -2
- package/dist/components/Carousel/carousel.js.map +1 -1
- package/dist/components/Chart/chart.d.ts +1 -0
- package/dist/components/Chart/chart.d.ts.map +1 -1
- package/dist/components/Chart/chart.js +8 -0
- package/dist/components/Chart/chart.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 +10 -2
- package/dist/components/Checkbox/checkbox.js.map +1 -1
- package/dist/components/Chip/chip.d.ts +1 -1
- package/dist/components/Chip/chip.d.ts.map +1 -1
- package/dist/components/Chip/chip.js +4 -2
- package/dist/components/Chip/chip.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 +37 -11
- 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 +106 -8
- 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 +3 -2
- 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-interaction-layer.d.ts +2 -1
- package/dist/components/DataTable/data-table-interaction-layer.d.ts.map +1 -1
- package/dist/components/DataTable/data-table-interaction-layer.js +3 -4
- package/dist/components/DataTable/data-table-interaction-layer.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 +8 -5
- package/dist/components/DataTable/data-table.d.ts.map +1 -1
- package/dist/components/DataTable/data-table.js +197 -71
- package/dist/components/DataTable/data-table.js.map +1 -1
- package/dist/components/DateGrid/date-grid.d.ts +1 -1
- package/dist/components/DateGrid/date-grid.d.ts.map +1 -1
- package/dist/components/DateGrid/date-grid.js +358 -225
- package/dist/components/DateGrid/date-grid.js.map +1 -1
- package/dist/components/DatePicker/date-picker.d.ts.map +1 -1
- package/dist/components/DatePicker/date-picker.js +16 -1
- package/dist/components/DatePicker/date-picker.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/DropdownMenu/dropdown-menu-keyboard.d.ts +86 -0
- package/dist/components/DropdownMenu/dropdown-menu-keyboard.d.ts.map +1 -0
- package/dist/components/DropdownMenu/dropdown-menu-keyboard.js +172 -0
- package/dist/components/DropdownMenu/dropdown-menu-keyboard.js.map +1 -0
- package/dist/components/DropdownMenu/dropdown-menu.d.ts +6 -1
- package/dist/components/DropdownMenu/dropdown-menu.d.ts.map +1 -1
- package/dist/components/DropdownMenu/dropdown-menu.js +50 -39
- package/dist/components/DropdownMenu/dropdown-menu.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-wrapper.d.ts +18 -1
- package/dist/components/Field/field-wrapper.d.ts.map +1 -1
- package/dist/components/Field/field-wrapper.js +39 -10
- package/dist/components/Field/field-wrapper.js.map +1 -1
- package/dist/components/Field/field.d.ts +3 -2
- 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/FileItem/file-item.d.ts +26 -2
- package/dist/components/FileItem/file-item.d.ts.map +1 -1
- package/dist/components/FileItem/file-item.js +82 -25
- package/dist/components/FileItem/file-item.js.map +1 -1
- package/dist/components/FileItem/index.js +4 -2
- 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 +132 -55
- package/dist/components/FileUpload/file-upload.js.map +1 -1
- package/dist/components/FileViewer/file-viewer.d.ts +1 -1
- package/dist/components/FileViewer/file-viewer.d.ts.map +1 -1
- package/dist/components/FileViewer/file-viewer.js +9 -4
- package/dist/components/FileViewer/file-viewer.js.map +1 -1
- package/dist/components/InlineEdit/inline-edit.d.ts.map +1 -1
- package/dist/components/InlineEdit/inline-edit.js +1 -1
- package/dist/components/InlineEdit/inline-edit.js.map +1 -1
- package/dist/components/Input/input.d.ts.map +1 -1
- package/dist/components/Input/input.js +6 -1
- package/dist/components/Input/input.js.map +1 -1
- package/dist/components/LinkInput/link-input.d.ts.map +1 -1
- package/dist/components/LinkInput/link-input.js +20 -4
- 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 +6 -1
- package/dist/components/NumberInput/number-input.js.map +1 -1
- package/dist/components/OverflowIndicator/index.js +2 -1
- package/dist/components/OverflowIndicator/overflow-indicator.d.ts +19 -1
- package/dist/components/OverflowIndicator/overflow-indicator.d.ts.map +1 -1
- package/dist/components/OverflowIndicator/overflow-indicator.js +47 -27
- package/dist/components/OverflowIndicator/overflow-indicator.js.map +1 -1
- package/dist/components/Pagination/pagination.d.ts +1 -1
- package/dist/components/Pagination/pagination.js +1 -1
- package/dist/components/Pagination/pagination.js.map +1 -1
- package/dist/components/PeoplePicker/avatar-stack-overflow.d.ts +7 -6
- package/dist/components/PeoplePicker/avatar-stack-overflow.d.ts.map +1 -1
- package/dist/components/PeoplePicker/avatar-stack-overflow.js +6 -7
- package/dist/components/PeoplePicker/avatar-stack-overflow.js.map +1 -1
- package/dist/components/PeoplePicker/people-picker-helpers.d.ts.map +1 -1
- package/dist/components/PeoplePicker/people-picker-helpers.js +1 -1
- package/dist/components/PeoplePicker/people-picker-helpers.js.map +1 -1
- package/dist/components/PeoplePicker/people-picker.d.ts.map +1 -1
- package/dist/components/PeoplePicker/people-picker.js +2 -2
- package/dist/components/PeoplePicker/people-picker.js.map +1 -1
- package/dist/components/PeoplePicker/person-display.d.ts.map +1 -1
- package/dist/components/PeoplePicker/person-display.js +26 -28
- package/dist/components/PeoplePicker/person-display.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/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 +10 -4
- package/dist/components/RadioGroup/radio-group.js.map +1 -1
- package/dist/components/Rating/rating.d.ts +28 -6
- package/dist/components/Rating/rating.d.ts.map +1 -1
- package/dist/components/Rating/rating.js +97 -80
- package/dist/components/Rating/rating.js.map +1 -1
- package/dist/components/ScrollArea/scroll-area.d.ts +1 -1
- package/dist/components/ScrollArea/scroll-area.js +1 -1
- package/dist/components/ScrollArea/scroll-area.js.map +1 -1
- package/dist/components/SegmentedControl/segmented-control.d.ts +1 -1
- package/dist/components/SegmentedControl/segmented-control.d.ts.map +1 -1
- package/dist/components/SegmentedControl/segmented-control.js +3 -2
- package/dist/components/SegmentedControl/segmented-control.js.map +1 -1
- package/dist/components/Select/select.d.ts.map +1 -1
- package/dist/components/Select/select.js +15 -8
- package/dist/components/Select/select.js.map +1 -1
- package/dist/components/SelectMenu/index.js +2 -1
- package/dist/components/SelectMenu/index.js.map +1 -1
- package/dist/components/SelectMenu/select-menu-keyboard.d.ts +59 -0
- package/dist/components/SelectMenu/select-menu-keyboard.d.ts.map +1 -0
- package/dist/components/SelectMenu/select-menu-keyboard.js +107 -0
- package/dist/components/SelectMenu/select-menu-keyboard.js.map +1 -0
- package/dist/components/SelectMenu/select-menu.d.ts +1 -22
- package/dist/components/SelectMenu/select-menu.d.ts.map +1 -1
- package/dist/components/SelectMenu/select-menu.js +18 -34
- package/dist/components/SelectMenu/select-menu.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 +7 -9
- package/dist/components/Sidebar/sidebar.d.ts.map +1 -1
- package/dist/components/Sidebar/sidebar.js +171 -50
- 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 +9 -4
- package/dist/components/Slider/slider.js.map +1 -1
- package/dist/components/Steps/steps.d.ts.map +1 -1
- package/dist/components/Steps/steps.js +54 -26
- package/dist/components/Steps/steps.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 +7 -2
- package/dist/components/Switch/switch.js.map +1 -1
- package/dist/components/Tabs/tabs.d.ts +1 -1
- package/dist/components/Tabs/tabs.d.ts.map +1 -1
- package/dist/components/Tabs/tabs.js +39 -4
- package/dist/components/Tabs/tabs.js.map +1 -1
- package/dist/components/Tag/tag.d.ts +1 -1
- package/dist/components/Tag/tag.d.ts.map +1 -1
- package/dist/components/Tag/tag.js +3 -2
- 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 +8 -2
- 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 +8 -4
- package/dist/components/TimePicker/time-columns.js.map +1 -1
- package/dist/components/TimePicker/time-picker.d.ts +1 -1
- package/dist/components/TimePicker/time-picker.js +1 -1
- package/dist/components/TimePicker/time-picker.js.map +1 -1
- package/dist/components/TreeView/tree-view.d.ts +9 -5
- package/dist/components/TreeView/tree-view.d.ts.map +1 -1
- package/dist/components/TreeView/tree-view.js +266 -214
- package/dist/components/TreeView/tree-view.js.map +1 -1
- package/dist/hooks/use-input-modality.d.ts +12 -17
- package/dist/hooks/use-input-modality.d.ts.map +1 -1
- package/dist/hooks/use-input-modality.js +4 -34
- package/dist/hooks/use-input-modality.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.d.ts +3 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +23 -3
- package/dist/index.js.map +1 -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/focus-after-trigger.d.ts +45 -0
- package/dist/lib/focus-after-trigger.d.ts.map +1 -0
- package/dist/lib/focus-after-trigger.js +66 -0
- package/dist/lib/focus-after-trigger.js.map +1 -0
- package/dist/lib/overlay-coexistence.js +1 -1
- package/dist/lib/roving-list-keyboard.d.ts +208 -0
- package/dist/lib/roving-list-keyboard.d.ts.map +1 -0
- package/dist/lib/roving-list-keyboard.js +174 -0
- package/dist/lib/roving-list-keyboard.js.map +1 -0
- package/dist/lib/utils.d.ts.map +1 -1
- package/dist/lib/utils.js +6 -1
- package/dist/lib/utils.js.map +1 -1
- package/dist/patterns/element-anatomy/item-anatomy.d.ts +4 -4
- package/dist/patterns/element-anatomy/item-anatomy.d.ts.map +1 -1
- package/dist/patterns/element-anatomy/item-anatomy.js +14 -4
- package/dist/patterns/element-anatomy/item-anatomy.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.d.ts.map +1 -1
- package/dist/patterns/overlay-surface/overlay-surface.js.map +1 -1
- package/dist/tokens/categorical-color.d.ts +6 -1
- package/dist/tokens/categorical-color.d.ts.map +1 -1
- package/dist/tokens/categorical-color.js.map +1 -1
- package/dist/tokens/uiSize/icon-size.d.ts +1 -1
- package/dist/tokens/uiSize/stack-gap.d.ts +18 -0
- package/dist/tokens/uiSize/stack-gap.d.ts.map +1 -0
- package/dist/tokens/uiSize/stack-gap.js +5 -0
- package/dist/tokens/uiSize/stack-gap.js.map +1 -0
- package/ds-canonical/fork/codex/agents/skills/performance-audit/SKILL.md +1 -1
- package/ds-canonical/fork/codex/hooks.json +1 -1
- package/ds-canonical/fork/consumer/lock.json +17 -14
- package/ds-canonical/fork/consumer/managed-files/423729840acdd43c812f17c245b871716c6b60f76e4d3e94864d0e34a815bf8d.blob +9 -0
- package/ds-canonical/fork/consumer/managed-files/46f10bc55cdfdf834d0749eebff42ea0bb8576f361b36d2b6ba1851354c50150.blob +47 -9
- package/ds-canonical/fork/consumer/managed-files/5a0d134a2645114c8caac20458070b00a7f0fd55b3e8805b7325cb4cf52fa15e.blob +110 -0
- package/ds-canonical/fork/consumer/managed-files/5af0a46760af338d51fd0f7950675f8e97f7d2f09386b8818fcd7a58e6654e75.blob +790 -0
- package/ds-canonical/fork/consumer/managed-files/68bfcddff9c4a16388c248cd34fb66163e9e64b3d809a48ef3984f6ae4ff6d12.blob +211 -0
- package/ds-canonical/fork/consumer/managed-files/7eb6cc60abfdabaf6617c336058fad1454ccde08889295767bfd9b7fd153cc6e.blob +45 -6
- package/ds-canonical/fork/consumer/managed-files/92e4828cdcb96ff0d01daa16f6885f24045a8e72820665650f2a1890dc3a446c.blob +42 -2
- package/ds-canonical/fork/consumer/managed-files/ab072e7f2eca51f0e18a5e18c4b333260ee5d219a12cc6df5bb2d683f0b2734d.blob +27 -1
- package/ds-canonical/fork/consumer/managed-files/e71c65f45c6af7ce12b6b3c0d8028f185da5b719e2050c9b2a6b243d41ae4878.blob +62 -14
- package/ds-canonical/fork/governance.lock +41 -17
- package/ds-canonical/fork/hooks/check_consumer_app_invariants.sh +16 -4
- package/ds-canonical/fork/hooks/check_consumer_code_quality.sh +3 -1
- package/ds-canonical/fork/hooks/check_pattern_invariants.sh +13 -9
- package/ds-canonical/fork/launchers/settings-hooks.json +1 -1
- package/ds-canonical/fork/manifest.json +17 -11
- package/ds-canonical/fork/providers/claude/skills/performance-audit/SKILL.md +1 -1
- package/ds-canonical/fork/providers/codex/hook-config.json +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_consumer_app_invariants.sh +16 -4
- package/ds-canonical/hooks/check_consumer_code_quality.sh +3 -1
- package/ds-canonical/hooks/check_datatable_invariants.sh +4 -1
- package/ds-canonical/hooks/check_pattern_invariants.sh +13 -9
- package/ds-canonical/hooks/check_plugin_fork_health.sh +8 -1
- package/ds-canonical/hooks/check_propose_discipline.sh +8 -1
- package/ds-canonical/hooks/check_storybook_addon_packaging.sh +8 -1
- package/ds-canonical/hooks/check_substantive_edit_approval_preflight.sh +64 -2
- package/ds-canonical/hooks/lib/_overlay_handcraft.sh +5 -2
- package/ds-canonical/hooks/registrations.json +12 -7
- package/ds-canonical/hooks/session_start_governance_check.sh +16 -0
- package/ds-canonical/hooks/tests/test_check_consumer_app_invariants.sh +25 -0
- package/ds-canonical/hooks/tests/test_check_consumer_code_quality.sh +4 -0
- package/ds-canonical/hooks/tests/test_check_consumer_ds_primitive_misuse.sh +14 -0
- package/ds-canonical/hooks/tests/test_check_datatable_invariants.sh +24 -0
- package/ds-canonical/hooks/tests/test_check_overlay_handcraft.sh +32 -0
- package/ds-canonical/hooks/tests/test_check_pattern_invariants.sh +59 -0
- package/ds-canonical/hooks/tests/test_check_plugin_fork_health.sh +32 -1
- package/ds-canonical/hooks/tests/test_check_propose_discipline.sh +21 -0
- package/ds-canonical/hooks/tests/test_check_storybook_addon_packaging.sh +21 -0
- package/ds-canonical/hooks/tests/test_check_substantive_edit_approval_preflight.sh +97 -0
- package/ds-canonical/hooks/tests/test_session_start_governance_check.sh +49 -0
- package/ds-canonical/references/README.md +12 -6
- package/ds-canonical/references/drag-canonical.md +3 -1
- package/ds-canonical/references/focus-canonical.md +26 -13
- package/ds-canonical/references/governance-audit-coverage.md +1 -1
- package/ds-canonical/references/hit-area-canonical.md +361 -0
- package/ds-canonical/references/item-anatomy-recipe.md +4 -2
- package/ds-canonical/references/keyboard-model-canonical.md +559 -0
- package/ds-canonical/rules/meta-patterns.md +4 -4
- package/ds-canonical/rules/self-verify.md +4 -4
- package/ds-canonical/skills/design-system-audit/references/audit-prompts.md +1 -1
- package/ds-canonical/skills/design-system-audit/references/historical-bugs.md +96 -0
- package/ds-canonical/skills/knowledge-prune/SKILL.md +4 -1
- package/ds-canonical/skills/performance-audit/SKILL.md +1 -1
- package/ds-story-manifest.json +19 -7
- package/llms-full.txt +2 -2
- package/llms.txt +2 -2
- package/package.json +3 -1
- package/src/components/Accordion/accordion.anatomy.stories.tsx +3 -3
- package/src/components/Accordion/accordion.spec.md +1 -1
- package/src/components/Accordion/accordion.tsx +8 -3
- package/src/components/AccountMenu/account-menu.spec.md +2 -3
- 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 +67 -24
- 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.anatomy.stories.tsx +1 -1
- package/src/components/AgentPanel/agent-panel.principles.stories.tsx +1 -1
- package/src/components/AgentPanel/agent-panel.spec.md +30 -275
- package/src/components/AgentPanel/agent-panel.stories.tsx +113 -8
- package/src/components/AgentPanel/agent-panel.tsx +59 -22
- package/src/components/Alert/alert.anatomy.stories.tsx +1 -1
- package/src/components/AppShell/app-shell.tsx +1 -1
- package/src/components/AspectRatio/aspect-ratio.anatomy.stories.tsx +10 -15
- package/src/components/Avatar/avatar.anatomy.stories.tsx +23 -26
- package/src/components/Avatar/avatar.principles.stories.tsx +10 -10
- package/src/components/Avatar/avatar.spec.md +45 -6
- package/src/components/Avatar/avatar.stories.tsx +8 -8
- package/src/components/Avatar/avatar.tsx +227 -21
- package/src/components/Badge/badge.anatomy.stories.tsx +16 -21
- package/src/components/Breadcrumb/breadcrumb.anatomy.stories.tsx +3 -3
- package/src/components/Breadcrumb/breadcrumb.spec.md +3 -3
- package/src/components/Breadcrumb/breadcrumb.tsx +5 -3
- package/src/components/BulkActionBar/bulk-action-bar.anatomy.stories.tsx +1 -1
- package/src/components/BulkActionBar/bulk-action-bar.principles.stories.tsx +1 -1
- package/src/components/BulkActionBar/bulk-action-bar.spec.md +1 -0
- package/src/components/BulkActionBar/bulk-action-bar.stories.tsx +57 -2
- package/src/components/BulkActionBar/bulk-action-bar.tsx +32 -2
- package/src/components/Button/button.anatomy.stories.tsx +174 -33
- package/src/components/Button/button.spec.md +23 -8
- package/src/components/Button/button.tsx +116 -21
- package/src/components/Calendar/calendar.anatomy.stories.tsx +34 -11
- package/src/components/Calendar/calendar.principles.stories.tsx +44 -1
- package/src/components/Calendar/calendar.spec.md +97 -20
- package/src/components/Calendar/calendar.stories.tsx +101 -4
- package/src/components/Calendar/calendar.tsx +379 -74
- package/src/components/Carousel/carousel.anatomy.stories.tsx +16 -17
- package/src/components/Carousel/carousel.spec.md +48 -2
- package/src/components/Carousel/carousel.tsx +9 -3
- package/src/components/Chart/chart.anatomy.stories.tsx +7 -5
- package/src/components/Chart/chart.spec.md +32 -7
- package/src/components/Chart/chart.tsx +9 -0
- package/src/components/Checkbox/checkbox.anatomy.stories.tsx +21 -28
- package/src/components/Checkbox/checkbox.spec.md +4 -2
- package/src/components/Checkbox/checkbox.tsx +10 -2
- package/src/components/Chip/chip.anatomy.stories.tsx +1 -1
- package/src/components/Chip/chip.spec.md +2 -1
- package/src/components/Chip/chip.tsx +4 -2
- package/src/components/Combobox/combobox.anatomy.stories.tsx +30 -37
- package/src/components/Combobox/combobox.spec.md +11 -3
- package/src/components/Combobox/combobox.tsx +43 -12
- package/src/components/Command/command.spec.md +11 -0
- package/src/components/Command/command.tsx +143 -10
- package/src/components/DataTable/cell-registry.tsx +5 -1
- package/src/components/DataTable/data-table-column-visibility-panel.tsx +5 -6
- package/src/components/DataTable/data-table-interaction-layer.tsx +27 -14
- 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.anatomy.stories.tsx +1 -1
- package/src/components/DataTable/data-table.css +64 -0
- package/src/components/DataTable/data-table.spec.md +70 -346
- package/src/components/DataTable/data-table.stories.tsx +56 -9
- package/src/components/DataTable/data-table.tsx +392 -103
- package/src/components/DateGrid/date-grid.anatomy.stories.tsx +2 -2
- package/src/components/DateGrid/date-grid.spec.md +163 -1
- package/src/components/DateGrid/date-grid.tsx +270 -7
- package/src/components/DatePicker/date-picker.anatomy.stories.tsx +18 -27
- package/src/components/DatePicker/date-picker.spec.md +6 -2
- package/src/components/DatePicker/date-picker.tsx +20 -1
- package/src/components/Dialog/dialog.spec.md +6 -2
- package/src/components/Dialog/dialog.stories.tsx +18 -9
- package/src/components/Dialog/dialog.tsx +83 -48
- package/src/components/DropdownMenu/dropdown-menu-keyboard.ts +255 -0
- package/src/components/DropdownMenu/dropdown-menu.anatomy.stories.tsx +32 -33
- package/src/components/DropdownMenu/dropdown-menu.spec.md +18 -5
- package/src/components/DropdownMenu/dropdown-menu.stories.tsx +58 -1
- package/src/components/DropdownMenu/dropdown-menu.tsx +70 -33
- package/src/components/Empty/empty.spec.md +1 -2
- package/src/components/Field/field-context.ts +9 -4
- package/src/components/Field/field-controls.spec.md +5 -2
- package/src/components/Field/field-edit-keys.ts +12 -0
- package/src/components/Field/field-wrapper.tsx +76 -10
- package/src/components/Field/field.anatomy.stories.tsx +1 -1
- package/src/components/Field/field.spec.md +11 -6
- package/src/components/Field/field.stories.tsx +24 -1
- package/src/components/Field/field.tsx +10 -4
- package/src/components/FileItem/file-item.anatomy.stories.tsx +20 -10
- package/src/components/FileItem/file-item.principles.stories.tsx +14 -7
- package/src/components/FileItem/file-item.spec.md +50 -48
- package/src/components/FileItem/file-item.stories.tsx +41 -28
- package/src/components/FileItem/file-item.tsx +184 -50
- package/src/components/FileUpload/file-upload.anatomy.stories.tsx +11 -15
- package/src/components/FileUpload/file-upload.spec.md +27 -1
- package/src/components/FileUpload/file-upload.stories.tsx +33 -0
- package/src/components/FileUpload/file-upload.tsx +173 -43
- package/src/components/FileViewer/file-viewer.tsx +9 -4
- package/src/components/HoverCard/hover-card.stories.tsx +2 -2
- package/src/components/InlineEdit/inline-edit.spec.md +2 -0
- package/src/components/InlineEdit/inline-edit.tsx +8 -3
- package/src/components/Input/input.anatomy.stories.tsx +23 -31
- package/src/components/Input/input.spec.md +11 -0
- package/src/components/Input/input.tsx +6 -1
- package/src/components/LinkInput/link-input.anatomy.stories.tsx +18 -21
- package/src/components/LinkInput/link-input.principles.stories.tsx +4 -4
- package/src/components/LinkInput/link-input.spec.md +12 -1
- package/src/components/LinkInput/link-input.tsx +37 -4
- package/src/components/Menu/menu-item.anatomy.stories.tsx +50 -56
- package/src/components/Menu/menu-item.spec.md +1 -1
- package/src/components/Menu/menu-item.stories.tsx +48 -42
- package/src/components/Notice/notice.anatomy.stories.tsx +8 -16
- package/src/components/Notice/notice.spec.md +1 -1
- package/src/components/Notice/notice.tsx +1 -1
- package/src/components/NumberInput/number-input.anatomy.stories.tsx +21 -28
- package/src/components/NumberInput/number-input.spec.md +2 -0
- package/src/components/NumberInput/number-input.tsx +8 -1
- package/src/components/OverflowIndicator/overflow-indicator.anatomy.stories.tsx +28 -48
- package/src/components/OverflowIndicator/overflow-indicator.spec.md +13 -1
- package/src/components/OverflowIndicator/overflow-indicator.stories.tsx +12 -9
- package/src/components/OverflowIndicator/overflow-indicator.tsx +32 -12
- package/src/components/Pagination/pagination.spec.md +1 -0
- package/src/components/Pagination/pagination.tsx +1 -1
- package/src/components/PeoplePicker/avatar-stack-overflow.ts +14 -13
- package/src/components/PeoplePicker/people-picker-helpers.ts +5 -2
- package/src/components/PeoplePicker/people-picker.anatomy.stories.tsx +1 -1
- package/src/components/PeoplePicker/people-picker.spec.md +4 -2
- package/src/components/PeoplePicker/people-picker.tsx +1 -4
- package/src/components/PeoplePicker/person-display.tsx +45 -28
- package/src/components/Popover/popover.anatomy.stories.tsx +1 -1
- package/src/components/Popover/popover.principles.stories.tsx +3 -12
- package/src/components/Popover/popover.spec.md +7 -3
- package/src/components/ProfileCard/profile-card.anatomy.stories.tsx +1 -1
- package/src/components/ProfileCard/profile-card.spec.md +1 -1
- package/src/components/ProfileCard/profile-card.tsx +0 -3
- package/src/components/ProgressBar/progress-bar.anatomy.stories.tsx +11 -21
- package/src/components/ProgressBar/progress-bar.spec.md +1 -1
- package/src/components/RadioGroup/radio-group.anatomy.stories.tsx +17 -24
- package/src/components/RadioGroup/radio-group.spec.md +1 -1
- package/src/components/RadioGroup/radio-group.tsx +22 -4
- package/src/components/Rating/rating.anatomy.stories.tsx +131 -124
- package/src/components/Rating/rating.principles.stories.tsx +69 -43
- package/src/components/Rating/rating.spec.md +142 -63
- package/src/components/Rating/rating.stories.tsx +11 -15
- package/src/components/Rating/rating.tsx +131 -117
- package/src/components/ScrollArea/scroll-area.anatomy.stories.tsx +1 -1
- package/src/components/ScrollArea/scroll-area.principles.stories.tsx +6 -3
- package/src/components/ScrollArea/scroll-area.spec.md +3 -1
- package/src/components/ScrollArea/scroll-area.stories.tsx +5 -2
- package/src/components/ScrollArea/scroll-area.tsx +1 -1
- package/src/components/SegmentedControl/segmented-control.anatomy.stories.tsx +1 -1
- package/src/components/SegmentedControl/segmented-control.spec.md +2 -2
- package/src/components/SegmentedControl/segmented-control.tsx +3 -2
- package/src/components/Select/select.anatomy.stories.tsx +26 -35
- package/src/components/Select/select.spec.md +4 -3
- package/src/components/Select/select.tsx +11 -4
- package/src/components/SelectMenu/select-menu-keyboard.ts +218 -0
- package/src/components/SelectMenu/select-menu.anatomy.stories.tsx +6 -17
- package/src/components/SelectMenu/select-menu.spec.md +12 -4
- package/src/components/SelectMenu/select-menu.tsx +21 -56
- package/src/components/SelectionControl/selection-item.anatomy.stories.tsx +9 -33
- package/src/components/SelectionControl/selection-item.spec.md +2 -2
- package/src/components/SelectionControl/selection-item.tsx +3 -1
- package/src/components/Separator/separator.stories.tsx +4 -4
- package/src/components/Sheet/sheet.principles.stories.tsx +31 -13
- package/src/components/Sidebar/sidebar.anatomy.stories.tsx +1 -1
- package/src/components/Sidebar/sidebar.spec.md +73 -6
- package/src/components/Sidebar/sidebar.stories.tsx +82 -4
- package/src/components/Sidebar/sidebar.tsx +281 -64
- package/src/components/Skeleton/skeleton.spec.md +1 -1
- package/src/components/Slider/slider.anatomy.stories.tsx +1 -1
- package/src/components/Slider/slider.spec.md +6 -5
- package/src/components/Slider/slider.tsx +13 -8
- package/src/components/Steps/steps-state-visuals.spec.md +157 -0
- package/src/components/Steps/steps.anatomy.stories.tsx +1 -0
- package/src/components/Steps/steps.spec.md +59 -142
- package/src/components/Steps/steps.stories.tsx +1 -1
- package/src/components/Steps/steps.tsx +125 -33
- package/src/components/Switch/switch.spec.md +3 -3
- package/src/components/Switch/switch.tsx +6 -1
- package/src/components/Tabs/tabs.spec.md +5 -1
- package/src/components/Tabs/tabs.tsx +42 -3
- package/src/components/Tag/tag.anatomy.stories.tsx +24 -28
- package/src/components/Tag/tag.spec.md +10 -11
- package/src/components/Tag/tag.tsx +3 -3
- package/src/components/Textarea/textarea.tsx +7 -1
- package/src/components/TimePicker/time-columns.tsx +9 -4
- package/src/components/TimePicker/time-picker.spec.md +4 -1
- package/src/components/TimePicker/time-picker.tsx +1 -1
- package/src/components/Tooltip/tooltip.anatomy.stories.tsx +6 -15
- package/src/components/Tooltip/tooltip.principles.stories.tsx +4 -16
- package/src/components/Tooltip/tooltip.spec.md +2 -1
- package/src/components/TreeView/tree-view.anatomy.stories.tsx +35 -13
- package/src/components/TreeView/tree-view.spec.md +67 -38
- package/src/components/TreeView/tree-view.stories.tsx +221 -12
- package/src/components/TreeView/tree-view.tsx +463 -356
- package/src/hooks/use-input-modality.ts +17 -46
- package/src/hooks/use-overflow-items.ts +4 -0
- package/src/hooks/use-truncated.ts +3 -2
- package/src/index.ts +17 -0
- package/src/lib/README.md +2 -0
- package/src/lib/drag-announcements.ts +78 -0
- package/src/lib/focus-after-trigger.ts +102 -0
- package/src/lib/roving-list-keyboard.ts +400 -0
- package/src/lib/utils.ts +5 -0
- package/src/patterns/action-bar/action-bar.spec.md +2 -0
- package/src/patterns/element-anatomy/inline-action.spec.md +16 -12
- package/src/patterns/element-anatomy/item-anatomy.spec.md +98 -6
- package/src/patterns/element-anatomy/item-anatomy.stories.tsx +75 -70
- package/src/patterns/element-anatomy/item-anatomy.tsx +17 -6
- package/src/patterns/element-anatomy/truncated-text.spec.md +1 -0
- package/src/patterns/header-canonical/header-canonical.spec.md +3 -2
- 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 +8 -162
- package/src/patterns/overlay-surface/overlay-surface.tsx +5 -3
- package/src/patterns/resize-handle/resize-handle.spec.md +3 -3
- package/src/styles/base.css +1 -1
- package/src/tokens/categorical-color.ts +6 -1
- package/src/tokens/color/color.spec.md +91 -24
- package/src/tokens/color/color.stories.tsx +50 -2
- package/src/tokens/color/primitives.css +41 -38
- package/src/tokens/color/semantic.css +105 -36
- package/src/tokens/layoutSpace/layoutSpace.spec.md +2 -0
- package/src/tokens/motion/motion.spec.md +31 -8
- package/src/tokens/orphan-tokens.spec.md +1 -1
- package/src/tokens/uiSize/icon-size.ts +1 -1
- package/src/tokens/uiSize/stack-gap.ts +17 -0
- package/src/tokens/uiSize/uiSize.css +12 -0
- package/src/tokens/uiSize/uiSize.spec.md +17 -16
- package/src/tokens/uiSize/uiSize.stories.tsx +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":"sidebar.js","sources":["../../../src/components/Sidebar/sidebar.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(SidebarProvider / Sidebar / SidebarMenu / SidebarGroup / SidebarMenuButton 等 10+ primitives 共享 context + 20+ sidebar-specific features;已是 foundational SSOT spec cap 800)\nimport * as React from \"react\"\nimport { Slot } from \"@radix-ui/react-slot\"\nimport * as CollapsiblePrimitive from \"@radix-ui/react-collapsible\"\nimport { cva, type VariantProps } from \"class-variance-authority\"\nimport type { LucideIcon } from \"lucide-react\"\nimport { ChevronDown, PanelLeft } from \"lucide-react\"\n\nimport { useIsNarrowViewport } from \"@/design-system/hooks/use-is-narrow-viewport\"\nimport { cn } from \"@/lib/utils\"\nimport { Badge } from \"@/design-system/components/Badge/badge\"\nimport { Button } from \"@/design-system/components/Button/button\"\nimport { Input } from \"@/design-system/components/Input/input\"\nimport { ScrollArea } from \"@/design-system/components/ScrollArea/scroll-area\"\nimport { Separator } from \"@/design-system/components/Separator/separator\"\nimport {\n Sheet,\n SheetContent,\n SheetDescription,\n SheetHeader,\n SheetTitle,\n} from \"@/design-system/components/Sheet/sheet\"\nimport { Skeleton } from \"@/design-system/components/Skeleton/skeleton\"\nimport { ChromeHeader } from \"@/design-system/patterns/header-canonical/chrome-header\"\nimport {\n Tooltip,\n TooltipContent,\n TooltipTrigger,\n} from \"@/design-system/components/Tooltip/tooltip\"\n// Row primitive 共用常數與 helpers——單一 source of truth\nimport {\n AVATAR_SIZE,\n ICON_SIZE,\n ItemIcon,\n ItemLabel,\n ItemInlineAction,\n ItemInlineActionButton,\n RowSizeProvider,\n getUniformPrefixSlotStyle,\n ROW_PADDING_BY_SIZE,\n type RowSize,\n type InlineActionConfig,\n} from \"@/design-system/patterns/element-anatomy/item-anatomy\"\n\n/**\n * Sidebar\n * ─────────────────────────────────────────────────────────────\n * 本元件的 item / label 視覺規格刻意對齊 MenuItem(menuItemVariants)\n * ——sidebar、dropdown-menu、select-menu 共用同一條 item-layout 公式:\n *\n * px = var(--layout-space-loose) (sidebar 脈絡;md=16 / lg=24)\n * py = calc((--field-height-md - 1lh) / 2)\n * font: text-body leading-compact font-medium\n * hover / selected: bg-neutral-hover / bg-neutral-selected\n *\n * 這確保 SidebarGroupLabel(header 模式)和 SidebarMenuButton(互動模式)\n * 擁有完全相同的 row height,消除「label 和 items 之間高度落差」的問題。\n */\n\nconst SIDEBAR_COOKIE_NAME = \"sidebar_state\"\nconst SIDEBAR_COOKIE_MAX_AGE = 60 * 60 * 24 * 7\nconst SIDEBAR_KEYBOARD_SHORTCUT = \"b\"\n\n// ── Context ────────────────────────────────────────────────────────────────\n\n// Sidebar 的 size 直接用 item-layout pattern 的 RowSize type(sm/md/lg),\n// 跟所有 row primitives 保持同一套 size 語意\ntype SidebarSize = RowSize\n\ntype SidebarContextProps = {\n state: \"expanded\" | \"collapsed\"\n open: boolean\n setOpen: (open: boolean) => void\n openMobile: boolean\n setOpenMobile: (open: boolean | ((current: boolean) => boolean), opener?: HTMLElement) => void\n mobileOpenerRef: React.MutableRefObject<HTMLElement | null>\n isMobile: boolean\n toggleSidebar: (opener?: HTMLElement) => void\n // 全 sidebar 共用的 size(sm/md/lg)——透過 SidebarProvider 一次設定,\n // 下游所有 SidebarMenuButton / SidebarGroupLabel / SidebarMenuSkeleton 自動繼承。\n size: SidebarSize\n // Single-selection state:整個 sidebar 同時只有一個 active item。\n // SidebarMenuButton 傳 `id` prop,自動從這裡算 isActive、自動 onClick 時 setActiveId。\n // Consumer 可 controlled(傳 activeId + onActiveChange)或 uncontrolled(僅傳\n // defaultActiveId)。Router-driven 的 sidebar 通常 controlled(URL → activeId)。\n activeId: string | undefined\n setActiveId: (id: string) => void\n}\n\nconst SidebarContext = React.createContext<SidebarContextProps | null>(null)\n\nfunction useSidebar() {\n const context = React.useContext(SidebarContext)\n if (!context) {\n throw new Error(\"useSidebar must be used within a SidebarProvider.\")\n }\n return context\n}\n\n// ── Provider ───────────────────────────────────────────────────────────────\n\n// code-quality-allow: long-function — foundational composite main body — 拆 sub-fn 會複雜化 local state / ref / context binding\nconst SidebarProvider = React.forwardRef<\n HTMLDivElement,\n React.ComponentProps<\"div\"> & {\n defaultOpen?: boolean\n open?: boolean\n onOpenChange?: (open: boolean) => void\n /** Sidebar row 元件的預設尺寸(sm/md/lg),propagate 給所有 children。Default: \"md\" */\n size?: SidebarSize\n /** 當前 active item 的 id(controlled)——router-driven sidebar 從 URL 算出來傳進。 */\n activeId?: string\n /** 初始 active id(uncontrolled)。 */\n defaultActiveId?: string\n /** Active id 改變時的 callback(controlled 必傳)。 */\n onActiveChange?: (id: string) => void\n /**\n * 全域 prefix 對齊。**預設 `false`**——只在「sidebar 內有大量 brand logo 跟一般 icon\n * 混用,期待 label 齊左掃視」時 opt-in `true`。\n *\n * **典型 use case**(should opt-in):\n * - Linear / Raycast 風格的 integration 清單:Home / Inbox(lucide icon)\n * + GitHub / Slack / Figma(brand logo,24px)混在同一個 menu\n * - App launcher / workspace switcher / connected apps,brand logo 為主體\n *\n * **不該 opt-in**:\n * - 全 icon 主導覽 + 全 avatar user footer(語意不同層級,該分 group 不該強迫對齊)\n * - 沒有真實混用情境只想要「視覺整齊」(預設行為已經對)\n *\n * 開啟後機制:CSS `:has()` 偵測 sidebar 子樹同時存在 `data-prefix-type=\"icon\"` 和\n * `\"avatar\"`(由 `<ItemIcon>` / `<ItemAvatar>` 自動標記)時,套用固定 24px 槽,\n * 跨 menu / 跨 group 全域 label 對齊。不混用時零成本。\n *\n * 為什麼預設關閉而非 always-on auto:explicit-over-implicit——sidebar 排版行為\n * 應該從寫的 prop 一眼看出,不藏 CSS 魔法。詳見 `sidebar.spec.md`。\n */\n uniformPrefix?: boolean\n }\n>(\n // code-quality-allow: long-function — SidebarProvider 整合 dual-state(open + activeId)+ mobile sync + keyboard handler + context memo + style binding。拆 sub-fn 會打斷 React state binding chain + Context provider 順序語意(M21 prop variant test:拆分降低可讀性,單 fn 內 state lifecycle 一目了然)。當前 97 < cap 200。\n (\n {\n defaultOpen = true,\n open: openProp,\n onOpenChange: setOpenProp,\n size = \"md\",\n activeId: activeIdProp,\n defaultActiveId,\n onActiveChange,\n uniformPrefix = false,\n className,\n style,\n children,\n ...props\n },\n ref\n ) => {\n const [_activeId, _setActiveId] = React.useState<string | undefined>(defaultActiveId)\n const activeId = activeIdProp ?? _activeId\n const setActiveId = React.useCallback(\n (id: string) => {\n if (activeIdProp === undefined) _setActiveId(id)\n onActiveChange?.(id)\n },\n [activeIdProp, onActiveChange]\n )\n const isMobile = useIsNarrowViewport()\n const [openMobile, setOpenMobileState] = React.useState(false)\n const openMobileRef = React.useRef(false)\n const mobileOpenerRef = React.useRef<HTMLElement | null>(null)\n const setOpenMobile = React.useCallback((value: boolean | ((current: boolean) => boolean), opener?: HTMLElement) => {\n const current = openMobileRef.current\n const next = typeof value === \"function\" ? value(current) : value\n if (next && !current && typeof document !== \"undefined\") {\n mobileOpenerRef.current = opener ?? (\n document.activeElement instanceof HTMLElement ? document.activeElement : null\n )\n }\n openMobileRef.current = next\n setOpenMobileState(next)\n }, [])\n\n const [_open, _setOpen] = React.useState(defaultOpen)\n const open = openProp ?? _open\n const setOpen = React.useCallback(\n (value: boolean | ((value: boolean) => boolean)) => {\n const openState = typeof value === \"function\" ? value(open) : value\n // 受控/非受控分岔依「有沒有傳 open prop」判定(同上方 activeId pattern),\n // 不依「有沒有傳 onOpenChange」——否則 uncontrolled + onOpenChange 場景\n // internal state 永不更新,sidebar 凍結(同 SelectMenu R2 已修 bug class)。\n if (openProp === undefined) _setOpen(openState)\n setOpenProp?.(openState)\n document.cookie = `${SIDEBAR_COOKIE_NAME}=${openState}; path=/; max-age=${SIDEBAR_COOKIE_MAX_AGE}`\n },\n [openProp, setOpenProp, open]\n )\n\n const toggleSidebar = React.useCallback((opener?: HTMLElement) => {\n return isMobile\n ? setOpenMobile((o) => !o, opener)\n : setOpen((o) => !o)\n }, [isMobile, setOpen, setOpenMobile])\n\n React.useEffect(() => {\n const handleKeyDown = (event: KeyboardEvent) => {\n if (\n event.key === SIDEBAR_KEYBOARD_SHORTCUT &&\n (event.metaKey || event.ctrlKey)\n ) {\n // 2026-07-05 D4 修:全域攔截加三道 guard——\n // (1) defaultPrevented:app 層 editor 已處理同快捷鍵時不 double-fire;\n // (2) isComposing:IME 組字中不攔(組字期間 key 事件不代表使用者意圖);\n // (3) 可編輯 target:input / textarea / contentEditable 內 Cmd/Ctrl+B 是\n // bold 等編輯語意,快捷鍵讓位(對齊 VS Code / Linear 讓位可編輯區慣例)。\n if (event.defaultPrevented || event.isComposing) return\n const target = event.target\n if (\n target instanceof HTMLElement &&\n target.closest(\n 'input, textarea, [contenteditable]:not([contenteditable=\"false\"])'\n )\n ) {\n return\n }\n event.preventDefault()\n toggleSidebar()\n }\n }\n window.addEventListener(\"keydown\", handleKeyDown)\n return () => window.removeEventListener(\"keydown\", handleKeyDown)\n }, [toggleSidebar])\n\n // code-quality-allow: long-function — SidebarProvider 內 dual-state(open + activeId)+ mobile + handler refs + useMemo context 集中一處;拆 sub-fn 會打斷 useMemo dep stability + Context provider 順序。當前 97 < cap 200。Script naive heuristic 把 `const state = ?` 三元當 fn 起點誤報。\n const state = open ? \"expanded\" : \"collapsed\"\n\n const contextValue = React.useMemo<SidebarContextProps>(\n () => ({\n state,\n open,\n setOpen,\n isMobile,\n openMobile,\n setOpenMobile,\n mobileOpenerRef,\n toggleSidebar,\n size,\n activeId,\n setActiveId,\n }),\n [state, open, setOpen, isMobile, openMobile, setOpenMobile, toggleSidebar, size, activeId, setActiveId]\n )\n\n // 不在此處包 TooltipProvider——shadcn 原版預設 delayDuration=0,會覆蓋\n // 應用層的 delay 設定,造成 sidebar 內部 tooltip 行為跟其他地方不一致。\n // TooltipProvider 應由應用層(Storybook preview.tsx、app root)統一設定。\n // RowSizeProvider 讓整個 sidebar 子樹的 <ItemIcon> / <ItemAvatar> 都能 auto-size,\n // asChild consumer 不需要再手動查 ICON_SIZE / AVATAR_SIZE。\n //\n // ── 全域 prefix 對齊(預設關閉,explicit opt-in)──\n // false(預設):school A,各 menu 自然 prefix 寬度,跨 menu 不對齊\n // true:opt-in CSS `:has()` auto-detect,混用時套用、不混用零成本(school B,Notion 慣例)\n const slotStyle = getUniformPrefixSlotStyle(size)\n const slotValue = slotStyle[\"--item-prefix-slot\" as keyof typeof slotStyle]\n // 2026-05-22 Approach 9 — per-row prefix size mirror(JS const → CSS var):\n // `--item-icon-size` ← ICON_SIZE[size] (sm/md=16, lg=20)\n // `--item-avatar-size` ← AVATAR_SIZE.inline[size] (sm=20, md/lg=24)\n // 供 SidebarMenuButton collapsed pl 公式消費,讓所有 prefix 中心(icon/avatar)鎖回\n // rail center = --sidebar-width-icon/2(per user mental model:rail anchor =\n // GlobalHeader toggle geometry,sidebar 收合所有元素 cx 對齊此 rail center)。\n // Rail anchor 自身不動(--sidebar-menu-icon-size 維持 1rem 固定,per globals.css :root)。\n const wrapperStyle = React.useMemo<React.CSSProperties>(\n () =>\n ({\n \"--item-icon-size\": `${ICON_SIZE[size]}px`,\n \"--item-avatar-size\": `${AVATAR_SIZE.inline[size]}px`,\n ...(uniformPrefix ? { \"--mixed-prefix-slot\": slotValue } : {}),\n ...style,\n } as React.CSSProperties),\n [size, uniformPrefix, slotValue, style]\n )\n\n return (\n <SidebarContext.Provider value={contextValue}>\n <RowSizeProvider value={size}>\n <div\n style={wrapperStyle}\n className={cn(\n \"group/sidebar-wrapper flex min-h-svh w-full\",\n // CSS :has() 偵測 — 只在 uniformPrefix=true(opt-in,預設 false;見 sidebar.spec.md「Sidebar 全域 prefix 對齊」)時掛\n // mixing detected → 同時把 --item-icon-size cascade 到 slot 寬(`!` important 蓋\n // 過 wrapperStyle inline,讓 collapsed pl 公式知道「ItemPrefix wrapper 被撐到\n // 24,effective prefix width = 24 非 16」)。\n uniformPrefix &&\n \"has-[[data-prefix-type=icon]]:has-[[data-prefix-type=avatar]]:[--item-prefix-slot:var(--mixed-prefix-slot)] has-[[data-prefix-type=icon]]:has-[[data-prefix-type=avatar]]:![--item-icon-size:var(--mixed-prefix-slot)]\",\n className\n )}\n ref={ref}\n {...props}\n >\n {children}\n </div>\n </RowSizeProvider>\n </SidebarContext.Provider>\n )\n }\n)\nSidebarProvider.displayName = \"SidebarProvider\"\n\n// ── Sidebar container ──────────────────────────────────────────────────────\n\nconst Sidebar = React.forwardRef<\n HTMLDivElement,\n React.ComponentProps<\"div\"> & {\n side?: \"left\" | \"right\"\n collapsible?: \"offcanvas\" | \"icon\" | \"none\"\n /**\n * Viewport top inset(2026-05-20 ship per AppShell `primary-header` unblock)。\n * 預設 undefined = sidebar 從 viewport top 起算(`top:0 / h:svh`,當前 default)。\n * 提供 CSS value(eg. `'var(--chrome-header-height)'` 或 `'48px'`)時,sidebar 改\n * 從該值起算(`top: viewportInsetTop / height: calc(100svh - viewportInsetTop)`),\n * 讓 global header 不被覆蓋(AppShell primary-header mode 必傳)。\n * 對齊 Mantine `layout=\"default\"` navbar 高度扣 header 慣例。\n *\n * ⚠️ 作用範圍:**僅桌機 `offcanvas` / `icon` 分支**套用 insetStyle。\n * `collapsible=\"none\"`(於 isMobile 前提前 return 固定 div)與 mobile Sheet\n * 兩分支不套此 inset —— 該兩模式下 global header 仍會被覆蓋(未接線)。\n */\n viewportInsetTop?: string\n }\n>(\n (\n {\n side = \"left\",\n collapsible = \"offcanvas\",\n viewportInsetTop,\n className,\n children,\n ...props\n },\n ref\n ) => {\n const { isMobile, state, openMobile, setOpenMobile, mobileOpenerRef } = useSidebar()\n const insetStyle: React.CSSProperties | undefined = viewportInsetTop\n ? { top: viewportInsetTop, height: `calc(100svh - ${viewportInsetTop})` }\n : undefined\n\n if (collapsible === \"none\") {\n return (\n <div\n className={cn(\n \"flex h-full w-[var(--sidebar-width)] flex-col bg-surface text-foreground\",\n className\n )}\n ref={ref}\n {...props}\n >\n {children}\n </div>\n )\n }\n\n if (isMobile) {\n return (\n <Sheet open={openMobile} onOpenChange={setOpenMobile} {...props}>\n <SheetContent\n data-sidebar=\"sidebar\"\n data-mobile=\"true\"\n onCloseAutoFocus={(event) => {\n event.preventDefault()\n const opener = mobileOpenerRef.current\n mobileOpenerRef.current = null\n if (opener?.isConnected) opener.focus({ preventScroll: true })\n }}\n className=\"w-[var(--sidebar-width)] bg-surface p-0 text-foreground [&>button]:hidden\"\n style={\n {\n \"--sidebar-width\": \"var(--sidebar-width-mobile)\",\n } as React.CSSProperties\n }\n side={side}\n >\n <SheetHeader className=\"sr-only\">\n <SheetTitle>Sidebar</SheetTitle>\n <SheetDescription>Displays the mobile sidebar.</SheetDescription>\n </SheetHeader>\n <div className=\"flex h-full w-full flex-col\">{children}</div>\n </SheetContent>\n </Sheet>\n )\n }\n\n return (\n <div\n ref={ref}\n className=\"group peer hidden shrink-0 text-foreground md:block\"\n data-state={state}\n data-collapsible={state === \"collapsed\" ? collapsible : \"\"}\n data-side={side}\n >\n {/* Gap div:佔據 sidebar 實際寬度,推開主內容 */}\n <div\n className={cn(\n \"relative w-[var(--sidebar-width)] min-w-[var(--sidebar-width-min)] bg-transparent transition-[width,min-width] duration-200 ease-linear motion-reduce:duration-0\",\n \"group-data-[collapsible=offcanvas]:!w-0 group-data-[collapsible=offcanvas]:!min-w-0\",\n \"group-data-[side=right]:rotate-180\",\n \"group-data-[collapsible=icon]:!w-[var(--sidebar-width-icon)] group-data-[collapsible=icon]:!min-w-0\"\n )}\n />\n <div\n style={insetStyle}\n className={cn(\n // 2026-05-20 v3:`inset-y-0 h-svh` → 可被 viewportInsetTop prop override(per AppShell primary-header)\n !viewportInsetTop && \"inset-y-0 h-svh\",\n // 2026-05-21 v9 fix(per user 「右側分隔線跑去哪 + sidebar 寬度沒正確變化」):\n // 補 `overflow-x-hidden` — C* refactor 主機制(outer narrows to sidebar-width-icon\n // 時 clip inner 272px overflow,visual right border 顯示,主內容不被遮)。原 C*\n // commit 漏加 className 只 comment 提及。\n \"overflow-x-hidden\",\n \"fixed z-10 hidden w-[var(--sidebar-width)] min-w-[var(--sidebar-width-min)] transition-[left,right,width,min-width] duration-200 ease-linear motion-reduce:duration-0 md:flex\",\n \"group-data-[collapsible=icon]:!min-w-0\",\n side === \"left\"\n ? \"left-0 group-data-[collapsible=offcanvas]:left-[calc(var(--sidebar-width)*-1)]\"\n : \"right-0 group-data-[collapsible=offcanvas]:right-[calc(var(--sidebar-width)*-1)]\",\n \"group-data-[collapsible=icon]:w-[var(--sidebar-width-icon)]\",\n \"group-data-[side=left]:border-r group-data-[side=left]:border-divider\",\n \"group-data-[side=right]:border-l group-data-[side=right]:border-divider\",\n className\n )}\n {...props}\n >\n {/* SidebarInner:永遠 full sidebar-width via inline style(Tailwind JIT 沒 compile\n `min-w-[var(--sidebar-width)]` arbitrary class — 只 generated min-w-sidebar-width-min)。\n Inline style 繞 JIT 確保 272px。Collapsed mode 只是 outer overflow-x:hidden clip 出\n 左側 sidebar-width-icon = visible icon rail。 */}\n <div\n data-sidebar=\"sidebar\"\n style={{ width: 'var(--sidebar-width)', minWidth: 'var(--sidebar-width)' }}\n className=\"flex h-full shrink-0 flex-col bg-surface\"\n >\n {children}\n </div>\n </div>\n </div>\n )\n }\n)\nSidebar.displayName = \"Sidebar\"\n\n// ── Trigger / Rail / Input ─────────────────────────────────────────────────\n\n// SidebarTrigger — 用 Button 原生的 size=\"sm\" iconOnly,\n// 高度自動跟 density 走(field-height-sm @ md=28 / lg=32)\n// 不 override size,避免 density 切換時 trigger 不變\nconst SidebarTrigger = React.forwardRef<\n React.ElementRef<typeof Button>,\n React.ComponentProps<typeof Button>\n>(({ className, onClick, ...props }, ref) => {\n const { toggleSidebar } = useSidebar()\n\n return (\n <Button\n ref={ref}\n data-sidebar=\"trigger\"\n variant=\"text\"\n size=\"sm\"\n iconOnly\n startIcon={PanelLeft}\n aria-label=\"切換側邊欄\" /* i18n-allow: DS default;consumer 經 {...props} override */\n className={className}\n onClick={(event) => {\n const opener = event.currentTarget\n onClick?.(event)\n toggleSidebar(opener)\n }}\n {...props}\n />\n )\n})\nSidebarTrigger.displayName = \"SidebarTrigger\"\n\n// SidebarInput — 用 Input 元件原生高度(跟 density 自動走),不 override\nconst SidebarInput = React.forwardRef<\n React.ElementRef<typeof Input>,\n React.ComponentProps<typeof Input>\n>(({ className, ...props }, ref) => {\n return (\n <Input\n ref={ref}\n data-sidebar=\"input\"\n className={cn(\"w-full\", className)}\n {...props}\n />\n )\n})\nSidebarInput.displayName = \"SidebarInput\"\n\n// ── Shell regions ──────────────────────────────────────────────────────────\n\n// SidebarHeader(固定高度 chrome slot)vs SidebarFooter(content-based):\n// - SidebarHeader:固定高度 `var(--chrome-header-height)`(md=48 / lg=56,density-responsive),\n// 跨元件 chrome header 共享同一個 token,自動跟主內容 page header 對齊\n// - SidebarFooter:高度由內容決定(`shrink-0 py-2 border-t`,不消費 chrome-header-height、\n// 無自身固定高度),見下方 SidebarFooter 定義的 docblock + spec「Chrome 內容的約束」段\n// - 水平 padding 用 loose token(跟 items 的 px 對齊)\n// - 邊框是結構邊界(分隔 fixed/scroll 區),full-width 不內縮\n//\n// 為什麼 Header density-responsive:chrome 裡放的 button 綁定 field-height token,\n// 會隨 density 變大。Chrome 如果不跟著放大,lg density 下 padding 會被擠壓。\nconst SidebarHeader = React.forwardRef<\n HTMLDivElement,\n React.ComponentProps<\"div\"> & { withTabs?: boolean; tabsSlot?: React.ReactNode }\n>(({ className, withTabs, tabsSlot, ...props }, ref) => {\n return (\n // 2026-05-21 v9 — restore main behavior(per user「不應該調整原本的樣式,只有說收起來的時候\n // 要調整 header logo 的位置使其與其下 icon 水平置中」):\n // - Expanded mode:ChromeHeader default `px-loose`(16px L+R),avatar 跟 menu icon 對齊\n // - Collapsed mode:`!px-0 !justify-center` 拿掉 padding + 內容置中,WorkspaceBrand text\n // 已 `group-data-[collapsible=icon]:hidden`,collapsed 只剩 avatar 24px,centered in 48px\n // square → avatar.center = 24 = menu icon center.x ✓\n // 完全 match main 行為(v7 leadingRail / v8 -ml-1 surgical 全撤回)。\n <ChromeHeader\n ref={ref}\n withTabs={withTabs}\n tabsSlot={tabsSlot}\n data-sidebar=\"header\"\n className={cn(\n // 2026-05-21 v14 — rail-derived collapsed padding,full SSOT cascade(per user directive\n // 「avatar尺寸、menu item padding、menu icon尺寸任一變動都要正確連動」+ codex Layer B\n // 比稿共識):\n //\n // 公式 derivation(rail-centered geometric identity):\n // avatar.cx = pl + avatar/2 = (sidebar-width-icon - avatar)/2 + avatar/2 = sidebar-width-icon/2\n // menu icon.cx = loose + icon-size/2\n // sidebar-width-icon = 2*loose + icon-size → sidebar-width-icon/2 = loose + icon-size/2 ✓\n //\n // → 任何 avatar / loose / icon-size 改值,公式自動對齊 menu icon 中心(rail 幾何恆等)。\n //\n // SSOT 連動鏈:\n // --layout-space-loose (density token) ─┐\n // --sidebar-menu-icon-size ├→ --sidebar-width-icon (calc cascade)\n // └→ 公式自動跟 ✓\n // --chrome-header-avatar-size (header-canonical.css local token):公式 var 引用,\n // JS 端透過 WorkspaceBrand RowSizeProvider value=\"md\"\n // + AVATAR_SIZE.inline.md spec-coupled 共識 sync。\n //\n // Collapsed pl 幾何值(rail-centered,--sidebar-width-icon = 2*loose + menu-icon):\n // md density: (48-24)/2 = 12 · lg density: (68-24)/2 = 22\n // (lg width = 2×24+20 = 68,非 stale 64;avatar-size = 24 both densities)\n // 註:已撤回的 v13 `loose-4` 近似在 lg 給 20,與新 rail-幾何精確值 22 不同 —\n // v14 以 SSOT chain 公式取代該近似,md 兩者恰好都是 12。\n \"group-data-[collapsible=icon]:!pl-[calc((var(--sidebar-width-icon)-var(--chrome-header-avatar-size))/2)]\",\n \"group-data-[collapsible=icon]:!pr-0\",\n \"transition-[padding] duration-200 ease-linear motion-reduce:duration-0\",\n className\n )}\n {...props}\n />\n )\n})\nSidebarHeader.displayName = \"SidebarHeader\"\n\n// SidebarFooter — pinned 在 sidebar 底部的 menu group 容器,不是固定高度 chrome slot。\n// 行為跟 SidebarGroup 類似(flex flex-col py-2),但永遠靠底(shrink-0)且有 border-t 分隔。\n// Consumer 放 SidebarMenu + SidebarMenuButton,高度由內容決定(1~N 個 items)。\n// 典型內容:user menu、settings、help、logout 等底部選項群組。\nconst SidebarFooter = React.forwardRef<\n HTMLDivElement,\n React.ComponentProps<\"div\">\n>(({ className, ...props }, ref) => {\n return (\n <div\n ref={ref}\n data-sidebar=\"footer\"\n className={cn(\n \"flex shrink-0 flex-col py-2 border-t border-divider\",\n className\n )}\n {...props}\n />\n )\n})\nSidebarFooter.displayName = \"SidebarFooter\"\n\nconst SidebarSeparator = React.forwardRef<\n React.ElementRef<typeof Separator>,\n React.ComponentProps<typeof Separator>\n>(({ className, ...props }, ref) => {\n // 預設對齊 loose token:分隔兩個 scrollable group 時,跟 item 內容對齊。\n // 分隔 fixed 元件(Header/Footer 與 Content)時,consumer 可傳 className 覆寫成 mx-0。\n return (\n <Separator\n ref={ref}\n data-sidebar=\"separator\"\n className={cn(\n \"mx-[var(--layout-space-loose)] w-auto bg-divider\",\n className\n )}\n {...props}\n />\n )\n})\nSidebarSeparator.displayName = \"SidebarSeparator\"\n\nconst SidebarContent = React.forwardRef<\n HTMLDivElement,\n React.ComponentProps<\"div\">\n>(({ className, children, ...props }, ref) => {\n return (\n <div\n ref={ref}\n data-sidebar=\"content\"\n // SidebarContent 用 ScrollArea 處理長列表 scroll——跨 OS 一致不吃寬度(macOS\n // overlay vs Windows/Linux always-visible 差異見 scroll-area.tsx 註解)。\n // 呼吸空間和分隔線由 SidebarGroup 自己處理(對齊 MenuGroup 的 py-2 + [&+&]:border-t)。\n // ScrollArea Root 本身 overflow-hidden,icon-collapsed 時不會露出 scroll chrome。\n className={cn(\n \"flex min-h-0 flex-1 flex-col\",\n className\n )}\n {...props}\n >\n <ScrollArea className=\"flex-1\">\n <div className=\"flex flex-col\">{children}</div>\n </ScrollArea>\n </div>\n )\n})\nSidebarContent.displayName = \"SidebarContent\"\n\n// ── Group ──────────────────────────────────────────────────────────────────\n\n// SidebarGroupContext——讓 `SidebarGroupLabel` 和 `SidebarGroupContent` 知道當前\n// 所在的 group 是否 collapsible,以自動切換渲染模式(label 尾端 chevron button 變\n// `Collapsible.Trigger`、content 變 `Collapsible.Content`;label 本身維持\n// `role=\"presentation\"` div,不升格為 button)。沒有 context 就是舊行為(plain div)。\ntype SidebarGroupContextValue = {\n collapsible: boolean\n}\nconst SidebarGroupContext = React.createContext<SidebarGroupContextValue | null>(null)\n\nfunction useSidebarGroup() {\n return React.useContext(SidebarGroupContext)\n}\n\n/**\n * SidebarGroup\n *\n * 預設是非互動的 plain group(div)。當 `collapsible` = true 時自動切換成 Radix\n * Collapsible:SidebarGroupLabel 尾端自動渲染 chevron button 作為 `Collapsible.Trigger`\n * (承載 aria-expanded / data-state,依 open state 旋轉),SidebarGroupContent 變 Content;\n * label 本身維持 `role=\"presentation\"` div,不升格為 button。\n *\n * ── API 設計決策 ──\n * 為什麼是 group 層級的 prop 而不是 label 層級?因為「group 是否可收合」是結構層\n * 的決定,影響 group 子 primitive 的渲染模式(label 尾端 chevron 變 trigger button、\n * content 變 animated container)。把 prop 放在 label 上會讓 content 不知道自己該不該被包,\n * 形成跨元件的 prop drilling。放在 group 上用 context 傳遞是 React 的標準做法。\n */\nconst SidebarGroup = React.forwardRef<\n HTMLDivElement,\n React.ComponentProps<\"div\"> & {\n collapsible?: boolean\n defaultOpen?: boolean\n open?: boolean\n onOpenChange?: (open: boolean) => void\n }\n>(({ className, collapsible = false, defaultOpen = true, open, onOpenChange, children, ...props }, ref) => {\n const contextValue = React.useMemo<SidebarGroupContextValue>(\n () => ({ collapsible }),\n [collapsible]\n )\n\n // 非 collapsible 就是 plain div,舊行為完全不變\n const baseClass = cn(\n \"relative flex w-full min-w-0 flex-col py-2\",\n \"[&+&]:before:absolute [&+&]:before:top-0 [&+&]:before:left-[var(--layout-space-loose)] [&+&]:before:right-[var(--layout-space-loose)] [&+&]:before:h-px [&+&]:before:bg-divider [&+&]:before:content-['']\",\n className\n )\n\n if (!collapsible) {\n return (\n <SidebarGroupContext.Provider value={contextValue}>\n <div ref={ref} data-sidebar=\"group\" className={baseClass} {...props}>\n {children}\n </div>\n </SidebarGroupContext.Provider>\n )\n }\n\n // Collapsible 模式:Radix Collapsible.Root 當 group container\n return (\n <SidebarGroupContext.Provider value={contextValue}>\n <CollapsiblePrimitive.Root\n ref={ref}\n data-sidebar=\"group\"\n defaultOpen={defaultOpen}\n open={open}\n onOpenChange={onOpenChange}\n className={baseClass}\n // 註:此 Collapsible.Root 本身**不**自帶 icon 模式隱藏 class(baseClass 無\n // `group-data-[collapsible=icon]:hidden`),icon 模式下預設仍渲染。若要在 icon rail\n // 隱藏整個可收合 group(icon rail 沒空間放展開的 tree),consumer 自行傳\n // `className=\"group-data-[collapsible=icon]:hidden\"` 覆寫。\n {...(props as React.ComponentProps<typeof CollapsiblePrimitive.Root>)}\n >\n {children}\n </CollapsiblePrimitive.Root>\n </SidebarGroupContext.Provider>\n )\n})\nSidebarGroup.displayName = \"SidebarGroup\"\n\nconst SidebarGroupContent = React.forwardRef<\n HTMLDivElement,\n React.ComponentProps<\"div\">\n>(({ className, ...props }, ref) => {\n const group = useSidebarGroup()\n if (group?.collapsible) {\n return (\n <CollapsiblePrimitive.Content\n // Radix 提供 data-state=\"open|closed\",搭配 animate 做展開/收合動畫\n // (若之後需要,可在這個 div 上加 CSS animation)\n data-sidebar=\"group-content\"\n className={cn(\"w-full overflow-hidden\", className)}\n {...(props as React.ComponentProps<typeof CollapsiblePrimitive.Content>)}\n >\n <div ref={ref}>{props.children}</div>\n </CollapsiblePrimitive.Content>\n )\n }\n return (\n <div\n ref={ref}\n data-sidebar=\"group-content\"\n className={cn(\"w-full\", className)}\n {...props}\n />\n )\n})\nSidebarGroupContent.displayName = \"SidebarGroupContent\"\n\n// SidebarGroupLabel — 完全對齊 MenuItem header 的視覺 + TreeView 的 sm/md/lg 尺寸:\n// - 跟 SidebarMenuButton 共用 item-layout 公式(同 py 公式、同 text size、同 icon size)\n// - 唯一差異:font-medium + text-fg-muted + pointer-events-none(header 語意)\n// 這樣 label 和 items 的 row height 在任何 size 下都完全對齊。\n//\n// Icon 模式:整列 display:none(不是 -mt + opacity 0,避免脆弱的 margin 計算)\nconst sidebarGroupLabelVariants = cva(\n [\n // items-start 對齊 item-layout 規則(跟 SidebarMenuButton / TreeItem / MenuItem 一致)\n \"flex w-full items-start gap-2\",\n \"px-[var(--layout-space-loose)]\",\n \"font-medium text-fg-muted\",\n // @focus-suppress N — 不適用(不可操作 → 問題一);承擔者:pointer-events-none 的分組標題,不可操作 → 問題一:不該可聚焦,不用畫\n \"cursor-default select-none pointer-events-none outline-none\",\n // icon 模式:GroupLabel 純視覺分段 → collapsed 連 SR 一併隱藏是刻意(供名責任在各 menu button 的 sr-only label,2026-07-05 D4 修後與 MenuButton 策略分工)\n \"group-data-[collapsible=icon]:hidden\",\n ],\n {\n variants: {\n // 消費 ROW_PADDING_BY_SIZE SSOT(跟 SidebarMenuButton / MenuItem / TreeItem 一致)\n size: ROW_PADDING_BY_SIZE,\n },\n defaultVariants: { size: \"md\" },\n }\n)\n\nconst SidebarGroupLabel = React.forwardRef<\n HTMLDivElement,\n React.ComponentProps<\"div\"> & {\n asChild?: boolean\n /** ARIA label for the expand/collapse chevron (only when group is collapsible). Override for i18n. Default: 「展開或收合」 */\n toggleAriaLabel?: string\n } & VariantProps<typeof sidebarGroupLabelVariants>\n>(({ className, asChild = false, size: sizeProp, children, toggleAriaLabel = '展開或收合' /* i18n-allow: DS default; consumer override via toggleAriaLabel prop */, ...props }, ref) => {\n const { size: contextSize } = useSidebar()\n const size = sizeProp ?? contextSize\n const group = useSidebarGroup()\n\n // Collapsible group:label 本身仍是 plain header(保留原本語意),\n // chevron 是 suffix slot 裡的 inline action button,用 Radix Collapsible.Trigger asChild\n // 包住 `ItemInlineActionButton`——視覺規格完全對齊 patterns/element-anatomy/inline-action.spec.md(Inline Action SSOT),\n // 跟 SidebarMenuButton 的 suffix inline action 同一套 canonical 實作。\n //\n // **為什麼 chevron 是 inline action 而非整個 label 是 trigger**:\n // 1. 同 sidebar 內的 inline action 視覺必須一致(fg-muted → fg-secondary、hover bg、圓角)\n // 2. Linear / Notion / Finder 等世界級 sidebar 都是「label 裝飾、chevron 互動」\n // 3. 整個 label 當 trigger 會把 label 升格為 button,跟非 collapsible group 的 label\n // 語意不一致(一個是 div、一個是 button),無障礙與視覺焦點都會跳動\n if (group?.collapsible) {\n return (\n <div\n ref={ref}\n data-sidebar=\"group-label\"\n role=\"presentation\"\n className={cn(\n sidebarGroupLabelVariants({ size }),\n // Single-line group header:改用 items-center 讓 chevron button(16×16)跟 text 垂直精確置中。\n // cva base 的 `items-start` 是 multi-line safety,單行 header 用不到,明確覆寫成 center。\n \"!items-center\",\n className\n )}\n {...props}\n >\n <span className=\"min-w-0 flex-1 truncate\">{children}</span>\n {/* Chevron:inline action button,Radix Collapsible.Trigger asChild 包住,\n Radix 會 merge onClick + aria-expanded + data-state 到 button。\n ── Suffix 位置的 chevron 用 accordion 慣例 ──\n base = ChevronDown (`v`),open 時 rotate-180° → `^`。\n 對齊 Radix Accordion / shadcn Collapsible / Material Expansion / Linear 的 section header。\n (Prefix 位置的 chevron 如 TreeView 才用 ChevronRight + rotate-90 的 tree disclosure 慣例) */}\n <CollapsiblePrimitive.Trigger asChild>\n <ItemInlineActionButton\n icon={ChevronDown}\n aria-label={toggleAriaLabel}\n className=\"pointer-events-auto ml-auto\"\n iconClassName=\"transition-transform duration-150 motion-reduce:duration-0 [[data-state=open]_&]:rotate-180\"\n />\n </CollapsiblePrimitive.Trigger>\n </div>\n )\n }\n\n // 非 collapsible:plain label(div,pointer-events-none)\n const Comp = asChild ? Slot : \"div\"\n return (\n <Comp\n ref={ref}\n data-sidebar=\"group-label\"\n role=\"presentation\"\n className={cn(sidebarGroupLabelVariants({ size }), className)}\n {...props}\n >\n {children}\n </Comp>\n )\n})\nSidebarGroupLabel.displayName = \"SidebarGroupLabel\"\n\nconst SidebarGroupAction = React.forwardRef<\n HTMLButtonElement,\n React.ComponentProps<\"button\"> & { asChild?: boolean }\n>(({ className, asChild = false, ...props }, ref) => {\n const Comp = asChild ? Slot : \"button\"\n return (\n <Comp\n ref={ref}\n data-sidebar=\"group-action\"\n className={cn(\n \"absolute right-[var(--layout-space-loose)] top-2 flex aspect-square w-5 items-center justify-center rounded-md p-0 text-fg-muted hover:bg-neutral-hover hover:text-fg-secondary [&>svg]:size-4 [&>svg]:shrink-0\",\n \"after:absolute after:-inset-2 after:md:hidden\",\n \"group-data-[collapsible=icon]:hidden\",\n className\n )}\n {...props}\n />\n )\n})\nSidebarGroupAction.displayName = \"SidebarGroupAction\"\n\n// ── Menu ───────────────────────────────────────────────────────────────────\n\n// SidebarMenu — items 容器\n//\n// Prefix 對齊由 `SidebarProvider` 的 `uniformPrefix`(opt-in,預設 false)全域處理(school B,\n// Notion/Linear 慣例):開啟後,整個 sidebar 子樹同時存在 icon 和 avatar prefix 時,自動套用\n// 固定槽,跨 menu 跨 group 對齊。要啟用,在 `<SidebarProvider uniformPrefix>` 全域 opt-in\n// (explicit > implicit,見 sidebar.spec.md「Sidebar 全域 prefix 對齊」段)。SidebarMenu 沒有\n// per-menu 覆寫——沒有真實 use case,YAGNI。\nconst SidebarMenu = React.forwardRef<\n HTMLUListElement,\n React.ComponentProps<\"ul\">\n>(({ className, ...props }, ref) => (\n <ul\n ref={ref}\n data-sidebar=\"menu\"\n // 無 gap:items 連續緊貼(對齊 DropdownMenu / TreeView 的視覺節奏)\n className={cn(\"flex w-full min-w-0 flex-col\", className)}\n {...props}\n />\n))\nSidebarMenu.displayName = \"SidebarMenu\"\n\nconst SidebarMenuItem = React.forwardRef<\n HTMLLIElement,\n React.ComponentProps<\"li\">\n>(({ className, ...props }, ref) => (\n <li\n ref={ref}\n data-sidebar=\"menu-item\"\n className={cn(\"group/menu-item relative\", className)}\n {...props}\n />\n))\nSidebarMenuItem.displayName = \"SidebarMenuItem\"\n\n/**\n * SidebarMenuButton\n *\n * 對齊 MenuItem / TreeItem 的 item-layout,支援 sm/md/lg 三種尺寸:\n * - horizontal: px = var(--layout-space-loose)\n * - vertical: py = calc((--field-height-{size} - 1lh) / 2)\n * - typography: text-body leading-compact(sm/md)/ text-body-lg(lg)+ font-medium\n * - icon: size-4 (sm/md) / size-5 (lg)\n * - hover: bg-neutral-hover\n * - selected: bg-neutral-selected(data-active=true)\n *\n * Icon 模式:button 保持 w-full 填滿 sidebar icon rail,**無 height lock / 無 justify-center**\n * (2026-05-21 v4 C* 撤回 `!h-[...]`——row height 永遠 stable,collapsed 只 outer clip);\n * prefix(icon / avatar)靠 `!pl-[calc((rail - prefix-width)/2)]` 公式置中對齊 rail,\n * label span 以 sr-only 視覺隱藏(a11y tree 保留,2026-07-05 D4——SR 仍讀得到 accessible name)。\n */\nconst sidebarMenuButtonVariants = cva(\n [\n \"peer/menu-button group/menu-button\",\n // items-start(跟 TreeItem / MenuItem 一致的 item-layout 規則):\n // 多行 label 時 prefix 留在第一行不飄移。\n // 單行 label 時(我們的預設情境,truncate = line-clamp-1),效果跟 items-center 完全相同,\n // 因為 prefix 被包在 `h-[1lh] flex items-center` 容器,強制對齊第一行文字中線。\n \"flex w-full items-start gap-2 text-left overflow-hidden\",\n \"px-[var(--layout-space-loose)]\",\n \"font-medium text-fg-secondary\",\n \"cursor-pointer select-none\",\n // 2026-05-21 v4 C* refactor(per codex M31 Layer C 比稿 final architecture):\n // 撤回所有 `group-data-[collapsible=icon]:*` overrides。Sidebar outer overflow-x:hidden\n // 自動 clip menu button 右側 label / badge / action,左側 icon 自然顯示在 sidebar-width-icon\n // range 內。Row geometry 永遠 stable(px-loose + items-start + gap-2 + height auto),\n // 無 discrete property switch → 無 fly / 無 flutter / 無 jitter。\n //\n // 2026-07-05 D4 更新:v4「outer clip、SR 仍讀到」的宣稱在 v5 label display:none 後\n // 已失真;現行 = label 走 sr-only(SR 讀到、供 accessible name,見下方 v5/D4 註解),\n // badge / action / inline-actions 仍 display:none(視覺 + SR 一併隱藏——collapsed 供名\n // 責任在 label,badge 計數屬補充資訊)。Tooltip 仍在 collapsed state 顯示\n // (per buttonWithTooltip 既有 hidden 條件),僅 hover 提示、不供名。\n //\n // 對齊 MUI MiniDrawer + shadcn canonical(width morph + overflow clip + row geometry 不變)。\n // hover 底色瞬間切換,不做過渡(user 2026-09-10 拍板「第三題改成全部瞬間」;SSOT = tokens/motion/motion.spec.md「hover 回饋不做過渡」)\n \"hover:bg-neutral-hover hover:text-foreground\",\n // 鍵盤游標一律畫框、不上底色(focus-canonical 規則二,user 2026-09-09 拍板);選單鈕撐滿側欄寬度 → 內描邊。\n // 2026-09-09 之前非當前項的焦點用 hover 同色底(AI 推導,user 撤回);hover 的底色與文字色只屬於滑鼠。\n \"focus-visible:focus-ring-inset\",\n // 2026-05-31 M24:SidebarMenuButton 主要為 icon+label(currentColor 可改寫)→ 用 semantic\n // text-fg-disabled 對齊 MenuItem primitive(menu-item.tsx:198/221/248 + item-anatomy.tsx:374),\n // 非 opacity-disabled(opacity 保留給圖片/avatar/Switch 等無法改寫內部色者,per color.spec.md:103/118/701)。\n \"disabled:pointer-events-none disabled:text-fg-disabled\",\n \"aria-disabled:pointer-events-none aria-disabled:text-fg-disabled\",\n \"data-[active=true]:bg-neutral-selected data-[active=true]:text-foreground\",\n // 2026-08-11 user 拍板(SSOT = item-anatomy「選中 × 互動疊加」):滑鼠 hover 當前項釘住不變\n //(先前無規則,靠編譯順序運氣;現寫成顯式,釘住 (0,3,0) > hover (0,2,0))。\n // 鍵盤游標停在當前項 = 框疊在選中底色上(框由上方 focus-visible:focus-ring-inset 統一畫,不分當前與否;\n // 2026-09-07「A5畫框」只補了當前項,2026-09-09 user 拍板後全部畫框、焦點不再改底色)。\n \"data-[active=true]:hover:bg-neutral-selected\",\n \"group-has-[[data-sidebar=menu-action]]/menu-item:pr-8\",\n // 2026-05-21 v5 restore label 硬隱藏(user 抓「label 沒消失」):\n // C* outer overflow-x:hidden 理論 clip,但 label.x=40 在 sidebar-width-icon=48 內 → 首字\n // 部分可見。instant 切換非 main animation,跟 C* 「不用 display:none 做主\n // 動畫」不衝突(label 不參與 width 動畫,純 final state)。\n // 2026-07-05 D4 修:v5 的 display:none 讓 accessible-name 計算排除 label\n // (icon aria-hidden、badge 也 hidden)→ collapsed 時 menu button 對 SR 是無名\n // button(WCAG 4.1.2)。改 sr-only:position:absolute + 1px + clip 視覺隱藏但保留\n // a11y tree,button 仍有 accessible name;absolute 移出 flex flow,layout 效果同\n // display:none(icon 置中公式不變)。\n \"group-data-[collapsible=icon]:[&_[data-sidebar=menu-label]]:sr-only\",\n // 2026-05-22 Approach 9 — 收合時所有 prefix center 鎖回 rail center\n // (--sidebar-width-icon/2 = GlobalHeader toggle geometry SSOT)。\n // 公式: pl = (rail - prefix-width) / 2,prefix-width 由 SidebarProvider 注入的\n // --item-icon-size / --item-avatar-size CSS var(JS const mirror)提供。\n // 任何 row size(sm/md/lg)× density(md/lg)× prefix type(icon/avatar)6 cell 全對齊。\n \"group-data-[collapsible=icon]:has-[[data-prefix-type=icon]]:!pl-[calc((var(--sidebar-width-icon)-var(--item-icon-size))/2)]\",\n \"group-data-[collapsible=icon]:has-[[data-prefix-type=avatar]]:!pl-[calc((var(--sidebar-width-icon)-var(--item-avatar-size))/2)]\",\n ],\n {\n variants: {\n // size:消費 ROW_PADDING_BY_SIZE SSOT(item-anatomy.tsx)+ Sidebar-specific collapsed height\n // 前為 3 cva(menu / sidebar / tree)重複同一 py 公式,drift risk 已知。\n // 改用 shared SSOT 後 formula 改一處全同步。\n size: {\n // 2026-05-21 v4 C*:撤回 `group-data-[collapsible=icon]:!h-[...]` height lock\n // (per codex Layer C — row height 永遠 stable,collapsed 只 outer clip 視覺)\n sm: ROW_PADDING_BY_SIZE.sm,\n md: ROW_PADDING_BY_SIZE.md,\n lg: ROW_PADDING_BY_SIZE.lg,\n },\n variant: {\n /** 預設 — 導覽 item,參與 single-selection */\n default: \"\",\n /**\n * Meta 命令 row(例:「查看更多」「載入更多」「Show all」「+ 新增專案」)。\n *\n * 語意上**不是導覽目的地**,是 section 底部的命令。規格:\n * - 文字從 `text-fg-secondary` 退到 `text-fg-muted`(視覺重量下沉,hover 才升到 foreground)\n * - **不該參與 single-selection**:不傳 `id`,TS 階段提醒;執行階段傳了也不會 active\n * - `font-medium` 降為 `font-normal`(更輕,signal「這不是 primary 導覽」)\n * - 世界級對照:Linear \"Show N more\"、Notion \"Show N more\"、Slack \"Show more\"、\n * Gmail Labels \"More\"\n */\n meta: [\n \"font-normal text-fg-muted\",\n // meta variant 不該有 active 態;即使誤傳 isActive 也不啟動\n \"data-[active=true]:bg-transparent data-[active=true]:text-fg-muted\",\n ],\n },\n },\n defaultVariants: { size: \"md\", variant: \"default\" },\n }\n)\n\n// code-quality-allow: long-function — foundational composite main body — 拆 sub-fn 會複雜化 local state / ref / context binding\nconst SidebarMenuButton = React.forwardRef<\n HTMLButtonElement,\n Omit<React.ComponentProps<\"button\">, \"id\"> & {\n asChild?: boolean\n /**\n * Item 的唯一識別(對齊 `SidebarProvider.activeId`)。**強烈建議傳**——\n * 傳了之後 `isActive` 自動從 context 算、`onClick` 自動 setActiveId,\n * 整個 sidebar 的 single-selection 自動成立,不會寫出啞 item。\n */\n id?: string\n /**\n * 手動覆寫 active 狀態(極少用)。預設從 `SidebarProvider.activeId === id` 自動算。\n * 兩者都傳時以 `isActive` 為準。\n */\n isActive?: boolean\n startIcon?: LucideIcon\n tooltip?: string | React.ComponentProps<typeof TooltipContent>\n /**\n * Suffix slot 的 inline actions(宣告式 API,對齊 patterns/element-anatomy/inline-action.spec.md)。\n * Host 自動用 `<ItemInlineAction>` 渲染,consumer 只宣告 intent。\n * Icon 模式下自動隱藏。\n */\n inlineActions?: InlineActionConfig[]\n /**\n * 右側 actions slot(ReactNode)— escape hatch 供 consumer 放自訂元素\n * (如 DropdownMenu trigger / 自訂 popover trigger / 多 tier 動作)。\n *\n * 跟 `inlineActions` 互斥(同時傳 `inlineActionsSlot` 會優先,`inlineActions` 被忽略)。\n * 規則對齊 Input.endSlot canonical:90% case 用 `inlineActions` 宣告式 API,\n * 10% config 表達不出時走 slot。\n *\n * Padding budget:slot mode 預留 1 icon 寬度的 paddingRight(覆寫多 icon 寬度需 consumer 自控 className)。\n * Reveal / collapsed-hide / 絕對定位 chrome 跟 inlineActions 共用,consumer 不需重做。\n */\n inlineActionsSlot?: React.ReactNode\n /**\n * Inline actions 的顯示模式:\n * - `false`(預設):永遠顯示\n * - `\"hover\"`:row hover 時才淡入(TreeView 模式)\n */\n actionsReveal?: false | \"hover\"\n } & VariantProps<typeof sidebarMenuButtonVariants>\n>(\n (\n {\n asChild = false,\n id,\n isActive: isActiveProp,\n size: sizeProp,\n variant = \"default\",\n startIcon: StartIcon,\n tooltip,\n inlineActions,\n inlineActionsSlot,\n actionsReveal = false,\n className,\n children,\n onClick,\n ...props\n },\n ref\n ) => {\n const Comp = asChild ? Slot : \"button\"\n // 沒傳 size 就從 SidebarProvider context 繼承(預設 md)\n const { isMobile, state, size: contextSize, activeId, setActiveId } = useSidebar()\n const size = sizeProp ?? contextSize\n\n // Meta variant 永不參與 single-selection,即使誤傳 id / isActive\n const isMeta = variant === \"meta\"\n\n // Active 狀態:明確傳的 isActive 優先;否則 `activeId === id` 自動算\n // Meta variant 永遠 false\n const isActive = isMeta ? false : (isActiveProp ?? (id !== undefined && activeId === id))\n\n // Click handler:id 存在且非 meta 才 setActiveId(single-selection 自動建立),\n // 同時呼叫 consumer 傳入的 onClick\n // code-quality-allow: long-function — helper fn 結構緊密,拆 sub-fn 會跨 fn 傳 state 反而複雜\n const handleClick = React.useCallback(\n (e: React.MouseEvent<HTMLButtonElement>) => {\n if (!isMeta && id !== undefined) setActiveId(id)\n onClick?.(e)\n },\n [isMeta, id, setActiveId, onClick]\n )\n\n // asChild 時,我們不能額外 wrap span / icon——Slot 要求單一 child。\n // 所以 asChild 的 consumer 自行放 icon + label(記得 prefix 要包在 h-[1lh] 容器,\n // label span 要有 data-sidebar=\"menu-label\" attribute 才能參與 icon 模式自動隱藏)。\n //\n // Label span 在 icon 模式下透過 cva base 的 `[&_[data-sidebar=menu-label]]:sr-only`\n // 視覺隱藏(2026-07-05 D4:sr-only 非 display:none,SR 保留 accessible name;\n // absolute 移出 flex flow),這樣 flex 只剩 icon 一個 in-flow child,置中公式不變。\n // Sidebar 寬度 transition 200ms 是唯一持續動畫,使用者視線跟著寬度走,不會察覺 label 的瞬切。\n // 用 `<ItemIcon>` / `<ItemLabel>` helper,**不直接寫 ItemPrefix wrap StartIcon**——\n // ItemIcon 內部會自動加 `data-prefix-type=\"icon\"`,讓 SidebarProvider 的全域\n // `:has()` prefix-mix 偵測能命中。直接用 ItemPrefix 就會錯過這個 tag,\n // 全域對齊功能對 SidebarMenuButton 路徑失效(曾經發生過的 bug)。\n const content = asChild ? (\n children\n ) : (\n <>\n {StartIcon && <ItemIcon icon={StartIcon} />}\n <ItemLabel>{children}</ItemLabel>\n </>\n )\n\n const hasSlot = !!inlineActionsSlot\n // Host disabled 時 inline actions(config + slot)整段不渲染,paddingRight 預留一併取消\n // (inline-action.spec.md「宿主 disabled 時不渲染」+ sidebar.spec.md「Host disabled 時|不渲染」)。\n // 必須 render 層 guard:suffix 跟 button 是 sibling(避免巢狀 button),\n // native disabled 的 pointer-events-none 蓋不到 sibling,CSS 層無從繼承。\n // aria-disabled 同步 guard——對齊 cva `aria-disabled:` 樣式支線(asChild 非 button host 自傳)。\n const isDisabled =\n !!props.disabled || props[\"aria-disabled\"] === true || props[\"aria-disabled\"] === \"true\"\n const hasActions = !isDisabled && (hasSlot || (!!inlineActions && inlineActions.length > 0))\n\n // 計算 suffix 所佔寬度:N×icon + (N-1)×gap-2(8px),再加 gap-2 跟 label 之間的間隔\n // Slot mode 預設按 1 icon 預留(consumer 寬度自控)\n const iconSz = ICON_SIZE[size ?? \"md\"]\n const n = hasSlot ? 1 : (inlineActions?.length ?? 0)\n const suffixContentWidth = n > 0 ? n * iconSz + (n - 1) * 8 : 0\n // Button 的 paddingRight = loose + suffix 寬度 + gap-2\n // 用 CSS calc 表達 loose token,不硬寫 px(loose 會隨 density 變)\n const buttonPaddingRight = hasActions\n ? `calc(var(--layout-space-loose) + ${suffixContentWidth}px + 0.5rem)`\n : undefined\n\n const button = (\n <Comp\n ref={ref}\n data-sidebar=\"menu-button\"\n data-active={isActive}\n className={cn(sidebarMenuButtonVariants({ size, variant }), className)}\n style={hasActions ? { paddingRight: buttonPaddingRight } : undefined}\n onClick={handleClick}\n {...props}\n >\n {content}\n </Comp>\n )\n\n // Suffix inline actions——絕對定位在 menu-item 右邊,\n // 跟 button 同層(不是 button 的 child,避免巢狀 button)。\n const suffixNode = hasActions ? (\n <span\n data-sidebar=\"menu-inline-actions\"\n className={cn(\n \"absolute top-1/2 -translate-y-1/2 flex items-center gap-2\",\n \"right-[var(--layout-space-loose)]\",\n // Icon 模式隱藏(跟 SidebarMenuBadge / SidebarMenuAction 一致)\n \"group-data-[collapsible=icon]:hidden\",\n // hover-reveal:滑鼠 hover 或鍵盤 focus(但不是 mouse click 的 focus)時顯示。\n // 用 `:has(:focus-visible)` 而非 `:focus-within`——focus-within 會被\n // mouse click 觸發,導致 click 之後 actions 永久顯示直到焦點移走;\n // focus-visible 只在鍵盤 tab 時啟動,mouse click 不會觸發,符合使用者直覺。\n actionsReveal === \"hover\" &&\n \"opacity-0 group-hover/menu-item:opacity-100 group-has-[:focus-visible]/menu-item:opacity-100 transition-opacity duration-150 motion-reduce:duration-0\"\n )}\n >\n {hasSlot\n ? inlineActionsSlot\n : inlineActions!.map((action, i) => (\n <ItemInlineAction key={action.label + i} action={action} />\n ))}\n </span>\n ) : null\n\n // code-quality-allow: long-function — helper fn 結構緊密,拆 sub-fn 會跨 fn 傳 state 反而複雜\n const buttonWithTooltip = tooltip ? (\n (() => {\n const tooltipProps =\n typeof tooltip === \"string\" ? { children: tooltip } : tooltip\n return (\n <Tooltip>\n <TooltipTrigger asChild>{button}</TooltipTrigger>\n <TooltipContent\n side=\"right\"\n align=\"center\"\n hidden={state !== \"collapsed\" || isMobile}\n {...tooltipProps}\n />\n </Tooltip>\n )\n })()\n ) : (\n button\n )\n\n if (!suffixNode) return buttonWithTooltip\n return (\n <>\n {buttonWithTooltip}\n {suffixNode}\n </>\n )\n }\n)\nSidebarMenuButton.displayName = \"SidebarMenuButton\"\n\nconst SidebarMenuAction = React.forwardRef<\n HTMLButtonElement,\n React.ComponentProps<\"button\"> & {\n asChild?: boolean\n showOnHover?: boolean\n }\n>(({ className, asChild = false, showOnHover = false, ...props }, ref) => {\n const Comp = asChild ? Slot : \"button\"\n\n return (\n <Comp\n ref={ref}\n data-sidebar=\"menu-action\"\n className={cn(\n \"absolute right-[var(--layout-space-loose)] top-1/2 -translate-y-1/2 flex aspect-square w-5 items-center justify-center rounded-md p-0 text-fg-muted hover:bg-neutral-hover hover:text-fg-secondary [&>svg]:size-4 [&>svg]:shrink-0\",\n \"after:absolute after:-inset-2 after:md:hidden\",\n \"group-data-[collapsible=icon]:hidden\",\n showOnHover &&\n \"group-focus-within/menu-item:opacity-100 group-hover/menu-item:opacity-100 data-[state=open]:opacity-100 peer-data-[active=true]/menu-button:text-foreground md:opacity-0\",\n className\n )}\n {...props}\n />\n )\n})\nSidebarMenuAction.displayName = \"SidebarMenuAction\"\n\n// SidebarMenuBadge — 重用專案的 Badge 元件,絕對定位在 menu item 右側\n// Consumer 傳入 Badge 的所有 props(count / variant / max 等)\nconst SidebarMenuBadge = React.forwardRef<\n React.ElementRef<typeof Badge>,\n React.ComponentProps<typeof Badge>\n>(({ className, ...props }, ref) => (\n <Badge\n ref={ref}\n data-sidebar=\"menu-badge\"\n className={cn(\n \"pointer-events-none absolute right-[var(--layout-space-loose)] top-1/2 -translate-y-1/2\",\n \"group-data-[collapsible=icon]:hidden\",\n className\n )}\n {...props}\n />\n))\nSidebarMenuBadge.displayName = \"SidebarMenuBadge\"\n\n// SidebarMenuSkeleton — 對齊 SidebarMenuButton 的 item-layout 公式,支援 sm/md/lg\n// 沒傳 size 就從 context 繼承,確保 loading 狀態跟實際 item 同高不跳動\nconst sidebarMenuSkeletonVariants = cva(\n [\n \"flex items-start gap-2\",\n \"px-[var(--layout-space-loose)]\",\n ],\n {\n variants: {\n // 消費 ROW_PADDING_BY_SIZE SSOT + skeleton-icon size modifier(Sidebar-specific)\n // 注:skeleton 無文字,typography class 雖被帶入但無害,保持公式一處同步\n size: {\n sm: [ROW_PADDING_BY_SIZE.sm, \"[&>[data-sidebar=menu-skeleton-icon]]:size-4\"],\n md: [ROW_PADDING_BY_SIZE.md, \"[&>[data-sidebar=menu-skeleton-icon]]:size-4\"],\n lg: [ROW_PADDING_BY_SIZE.lg, \"[&>[data-sidebar=menu-skeleton-icon]]:size-5\"],\n },\n },\n defaultVariants: { size: \"md\" },\n }\n)\n\nconst SidebarMenuSkeleton = React.forwardRef<\n HTMLDivElement,\n React.ComponentProps<\"div\"> & {\n showIcon?: boolean\n } & VariantProps<typeof sidebarMenuSkeletonVariants>\n>(({ className, showIcon = false, size: sizeProp, ...props }, ref) => {\n const { size: contextSize } = useSidebar()\n const size = sizeProp ?? contextSize\n const width = React.useMemo(() => {\n return `${Math.floor(Math.random() * 40) + 50}%`\n }, [])\n\n return (\n <div\n ref={ref}\n data-sidebar=\"menu-skeleton\"\n className={cn(sidebarMenuSkeletonVariants({ size }), className)}\n {...props}\n >\n {showIcon && (\n <Skeleton\n className=\"rounded-md shrink-0\"\n data-sidebar=\"menu-skeleton-icon\"\n />\n )}\n <Skeleton\n className=\"h-4 max-w-[var(--skeleton-width)] flex-1\"\n data-sidebar=\"menu-skeleton-text\"\n style={\n {\n \"--skeleton-width\": width,\n } as React.CSSProperties\n }\n />\n </div>\n )\n})\nSidebarMenuSkeleton.displayName = \"SidebarMenuSkeleton\"\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 sidebarMeta = {\n component: 'Sidebar',\n family: 1, // Family 1(Menu item layout)消費者 — 對齊 sidebar.spec.md frontmatter family: 1(SidebarMenuButton rows 消費 item-anatomy;同 tree-view.tsx 慣例)\n variants: {\n default: { when: '標準導覽 row,參與 single-selection' },\n meta: { when: 'Section 底部命令 row(Show more / 新增),不參與 selection' },\n },\n sizes: {\n sm: { px: 28, when: '次導覽 / 設定頁 / 緊湊空間' },\n md: { px: 32, when: '預設 — 應用程式主導覽' },\n lg: { px: 36, when: '重要主導覽 / icon-prominent workspace switcher' },\n },\n // 'selected' = nav 當前項(data-[active=true] → bg-neutral-selected 灰底家族;2026-07-07 meta 詞彙統一補列);\n // 'active' 移除 — 全檔無 Tailwind 按壓 utility(內嵌 SidebarTrigger 的按壓屬 Button meta,同 Notice/Alert\n // 排除內嵌 dismiss Button 慣例;2026-07-07 對抗稽核抓 doc-claim ≠ code 補修)。\n states: ['default', 'hover', 'selected', 'focus-visible', 'disabled'],\n tokens: {\n bg: ['bg-neutral-hover', 'bg-neutral-selected', 'bg-surface', 'bg-transparent'],\n fg: ['text-fg-muted', 'text-fg-secondary', 'text-foreground'],\n ring: ['ring-ring'],\n },\n defaultSize: 'md',\n} as const\n\nexport {\n Sidebar,\n SidebarContent,\n SidebarFooter,\n SidebarGroup,\n SidebarGroupAction,\n SidebarGroupContent,\n SidebarGroupLabel,\n SidebarHeader,\n SidebarInput,\n SidebarMenu,\n SidebarMenuAction,\n SidebarMenuBadge,\n SidebarMenuButton,\n SidebarMenuItem,\n SidebarMenuSkeleton,\n SidebarProvider,\n SidebarSeparator,\n SidebarTrigger,\n useSidebar,\n}\n"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"sidebar.js","sources":["../../../src/components/Sidebar/sidebar.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(SidebarProvider / Sidebar / SidebarMenu / SidebarGroup / SidebarMenuButton 等 10+ primitives 共享 context + 20+ sidebar-specific features;已是 foundational SSOT spec cap 800)\nimport * as React from \"react\"\nimport { Slot } from \"@radix-ui/react-slot\"\nimport * as CollapsiblePrimitive from \"@radix-ui/react-collapsible\"\nimport { cva, type VariantProps } from \"class-variance-authority\"\nimport type { LucideIcon } from \"lucide-react\"\nimport { ChevronDown, PanelLeft } from \"lucide-react\"\n\nimport { useIsNarrowViewport } from \"@/design-system/hooks/use-is-narrow-viewport\"\nimport { cn } from \"@/lib/utils\"\nimport { Badge } from \"@/design-system/components/Badge/badge\"\nimport { Button } from \"@/design-system/components/Button/button\"\nimport { Input } from \"@/design-system/components/Input/input\"\nimport { ScrollArea } from \"@/design-system/components/ScrollArea/scroll-area\"\nimport { Separator } from \"@/design-system/components/Separator/separator\"\nimport {\n Sheet,\n SheetContent,\n SheetDescription,\n SheetHeader,\n SheetTitle,\n} from \"@/design-system/components/Sheet/sheet\"\nimport { Skeleton } from \"@/design-system/components/Skeleton/skeleton\"\nimport { ChromeHeader } from \"@/design-system/patterns/header-canonical/chrome-header\"\nimport {\n Tooltip,\n TooltipContent,\n TooltipTrigger,\n} from \"@/design-system/components/Tooltip/tooltip\"\n// Row primitive 共用常數與 helpers——單一 source of truth\nimport {\n AVATAR_SIZE,\n ICON_SIZE,\n ItemIcon,\n ItemLabel,\n ItemInlineAction,\n ItemInlineActionButton,\n type ItemInlineActionButtonProps,\n ItemSuffix,\n RowSizeProvider,\n getUniformPrefixSlotStyle,\n ROW_PADDING_BY_SIZE,\n type RowSize,\n type InlineActionConfig,\n} from \"@/design-system/patterns/element-anatomy/item-anatomy\"\n// 「列上有小按鈕的一串」鍵盤路線的唯一判定與執行(與 TreeView / FileUpload / Command 共用;見下方 SidebarMenu)\nimport {\n applyRovingAction,\n isRovingControlReachable,\n isTextEntryElement,\n pickRovingTabStop,\n removeRovingControlsFromTabOrder,\n resolveRovingKey,\n setRovingTabIndex,\n} from \"@/design-system/lib/roving-list-keyboard\"\n\n/**\n * Sidebar\n * ─────────────────────────────────────────────────────────────\n * 本元件的 item / label 視覺規格刻意對齊 MenuItem(menuItemVariants)\n * ——sidebar、dropdown-menu、select-menu 共用同一條 item-layout 公式:\n *\n * px = var(--layout-space-loose) (sidebar 脈絡;md=16 / lg=24)\n * py = calc((--field-height-md - 1lh) / 2)\n * font: text-body leading-compact font-medium\n * hover / selected: bg-neutral-hover / bg-neutral-selected\n *\n * 這確保 SidebarGroupLabel(header 模式)和 SidebarMenuButton(互動模式)\n * 擁有完全相同的 row height,消除「label 和 items 之間高度落差」的問題。\n */\n\nconst SIDEBAR_COOKIE_NAME = \"sidebar_state\"\nconst SIDEBAR_COOKIE_MAX_AGE = 60 * 60 * 24 * 7\nconst SIDEBAR_KEYBOARD_SHORTCUT = \"b\"\n\n// ── Context ────────────────────────────────────────────────────────────────\n\n// Sidebar 的 size 直接用 item-layout pattern 的 RowSize type(sm/md/lg),\n// 跟所有 row primitives 保持同一套 size 語意\ntype SidebarSize = RowSize\n\ntype SidebarContextProps = {\n state: \"expanded\" | \"collapsed\"\n open: boolean\n setOpen: (open: boolean) => void\n openMobile: boolean\n setOpenMobile: (open: boolean | ((current: boolean) => boolean), opener?: HTMLElement) => void\n mobileOpenerRef: React.MutableRefObject<HTMLElement | null>\n isMobile: boolean\n toggleSidebar: (opener?: HTMLElement) => void\n // 全 sidebar 共用的 size(sm/md/lg)——透過 SidebarProvider 一次設定,\n // 下游所有 SidebarMenuButton / SidebarGroupLabel / SidebarMenuSkeleton 自動繼承。\n size: SidebarSize\n // Single-selection state:整個 sidebar 同時只有一個 active item。\n // SidebarMenuButton 傳 `id` prop,自動從這裡算 isActive、自動 onClick 時 setActiveId。\n // Consumer 可 controlled(傳 activeId + onActiveChange)或 uncontrolled(僅傳\n // defaultActiveId)。Router-driven 的 sidebar 通常 controlled(URL → activeId)。\n activeId: string | undefined\n setActiveId: (id: string) => void\n}\n\nconst SidebarContext = React.createContext<SidebarContextProps | null>(null)\n\nfunction useSidebar() {\n const context = React.useContext(SidebarContext)\n if (!context) {\n throw new Error(\"useSidebar must be used within a SidebarProvider.\")\n }\n return context\n}\n\n// ── Provider ───────────────────────────────────────────────────────────────\n\n// code-quality-allow: long-function — foundational composite main body — 拆 sub-fn 會複雜化 local state / ref / context binding\nconst SidebarProvider = React.forwardRef<\n HTMLDivElement,\n React.ComponentProps<\"div\"> & {\n defaultOpen?: boolean\n open?: boolean\n onOpenChange?: (open: boolean) => void\n /** Sidebar row 元件的預設尺寸(sm/md/lg),propagate 給所有 children。Default: \"md\" */\n size?: SidebarSize\n /** 當前 active item 的 id(controlled)——router-driven sidebar 從 URL 算出來傳進。 */\n activeId?: string\n /** 初始 active id(uncontrolled)。 */\n defaultActiveId?: string\n /** Active id 改變時的 callback(controlled 必傳)。 */\n onActiveChange?: (id: string) => void\n /**\n * 全域 prefix 對齊。**預設 `false`**——只在「sidebar 內有大量 brand logo 跟一般 icon\n * 混用,期待 label 齊左掃視」時 opt-in `true`。\n *\n * **典型 use case**(should opt-in):\n * - Linear / Raycast 風格的 integration 清單:Home / Inbox(lucide icon)\n * + GitHub / Slack / Figma(brand logo,24px)混在同一個 menu\n * - App launcher / workspace switcher / connected apps,brand logo 為主體\n *\n * **不該 opt-in**:\n * - 全 icon 主導覽 + 全 avatar user footer(語意不同層級,該分 group 不該強迫對齊)\n * - 沒有真實混用情境只想要「視覺整齊」(預設行為已經對)\n *\n * 開啟後機制:CSS `:has()` 偵測 sidebar 子樹同時存在 `data-prefix-type=\"icon\"` 和\n * `\"avatar\"`(由 `<ItemIcon>` / `<ItemAvatar>` 自動標記)時,套用固定 24px 槽,\n * 跨 menu / 跨 group 全域 label 對齊。不混用時零成本。\n *\n * 為什麼預設關閉而非 always-on auto:explicit-over-implicit——sidebar 排版行為\n * 應該從寫的 prop 一眼看出,不藏 CSS 魔法。詳見 `sidebar.spec.md`。\n */\n uniformPrefix?: boolean\n }\n>(\n // code-quality-allow: long-function — SidebarProvider 整合 dual-state(open + activeId)+ mobile sync + keyboard handler + context memo + style binding。拆 sub-fn 會打斷 React state binding chain + Context provider 順序語意(M21 prop variant test:拆分降低可讀性,單 fn 內 state lifecycle 一目了然)。當前 97 < cap 200。\n (\n {\n defaultOpen = true,\n open: openProp,\n onOpenChange: setOpenProp,\n size = \"md\",\n activeId: activeIdProp,\n defaultActiveId,\n onActiveChange,\n uniformPrefix = false,\n className,\n style,\n children,\n ...props\n },\n ref\n ) => {\n const [_activeId, _setActiveId] = React.useState<string | undefined>(defaultActiveId)\n const activeId = activeIdProp ?? _activeId\n const setActiveId = React.useCallback(\n (id: string) => {\n if (activeIdProp === undefined) _setActiveId(id)\n onActiveChange?.(id)\n },\n [activeIdProp, onActiveChange]\n )\n const isMobile = useIsNarrowViewport()\n const [openMobile, setOpenMobileState] = React.useState(false)\n const openMobileRef = React.useRef(false)\n const mobileOpenerRef = React.useRef<HTMLElement | null>(null)\n const setOpenMobile = React.useCallback((value: boolean | ((current: boolean) => boolean), opener?: HTMLElement) => {\n const current = openMobileRef.current\n const next = typeof value === \"function\" ? value(current) : value\n if (next && !current && typeof document !== \"undefined\") {\n mobileOpenerRef.current = opener ?? (\n document.activeElement instanceof HTMLElement ? document.activeElement : null\n )\n }\n openMobileRef.current = next\n setOpenMobileState(next)\n }, [])\n\n const [_open, _setOpen] = React.useState(defaultOpen)\n const open = openProp ?? _open\n const setOpen = React.useCallback(\n (value: boolean | ((value: boolean) => boolean)) => {\n const openState = typeof value === \"function\" ? value(open) : value\n // 受控/非受控分岔依「有沒有傳 open prop」判定(同上方 activeId pattern),\n // 不依「有沒有傳 onOpenChange」——否則 uncontrolled + onOpenChange 場景\n // internal state 永不更新,sidebar 凍結(同 SelectMenu R2 已修 bug class)。\n if (openProp === undefined) _setOpen(openState)\n setOpenProp?.(openState)\n document.cookie = `${SIDEBAR_COOKIE_NAME}=${openState}; path=/; max-age=${SIDEBAR_COOKIE_MAX_AGE}`\n },\n [openProp, setOpenProp, open]\n )\n\n const toggleSidebar = React.useCallback((opener?: HTMLElement) => {\n return isMobile\n ? setOpenMobile((o) => !o, opener)\n : setOpen((o) => !o)\n }, [isMobile, setOpen, setOpenMobile])\n\n React.useEffect(() => {\n const handleKeyDown = (event: KeyboardEvent) => {\n if (\n event.key === SIDEBAR_KEYBOARD_SHORTCUT &&\n (event.metaKey || event.ctrlKey)\n ) {\n // 2026-07-05 D4 修:全域攔截加三道 guard——\n // (1) defaultPrevented:app 層 editor 已處理同快捷鍵時不 double-fire;\n // (2) isComposing:IME 組字中不攔(組字期間 key 事件不代表使用者意圖);\n // (3) 可編輯 target:input / textarea / contentEditable 內 Cmd/Ctrl+B 是\n // bold 等編輯語意,快捷鍵讓位(對齊 VS Code / Linear 讓位可編輯區慣例)。\n if (event.defaultPrevented || event.isComposing) return\n const target = event.target\n if (\n target instanceof HTMLElement &&\n target.closest(\n 'input, textarea, [contenteditable]:not([contenteditable=\"false\"])'\n )\n ) {\n return\n }\n event.preventDefault()\n toggleSidebar()\n }\n }\n window.addEventListener(\"keydown\", handleKeyDown)\n return () => window.removeEventListener(\"keydown\", handleKeyDown)\n }, [toggleSidebar])\n\n // code-quality-allow: long-function — SidebarProvider 內 dual-state(open + activeId)+ mobile + handler refs + useMemo context 集中一處;拆 sub-fn 會打斷 useMemo dep stability + Context provider 順序。當前 97 < cap 200。Script naive heuristic 把 `const state = ?` 三元當 fn 起點誤報。\n const state = open ? \"expanded\" : \"collapsed\"\n\n const contextValue = React.useMemo<SidebarContextProps>(\n () => ({\n state,\n open,\n setOpen,\n isMobile,\n openMobile,\n setOpenMobile,\n mobileOpenerRef,\n toggleSidebar,\n size,\n activeId,\n setActiveId,\n }),\n [state, open, setOpen, isMobile, openMobile, setOpenMobile, toggleSidebar, size, activeId, setActiveId]\n )\n\n // 不在此處包 TooltipProvider——shadcn 原版預設 delayDuration=0,會覆蓋\n // 應用層的 delay 設定,造成 sidebar 內部 tooltip 行為跟其他地方不一致。\n // TooltipProvider 應由應用層(Storybook preview.tsx、app root)統一設定。\n // RowSizeProvider 讓整個 sidebar 子樹的 <ItemIcon> / <ItemAvatar> 都能 auto-size,\n // asChild consumer 不需要再手動查 ICON_SIZE / AVATAR_SIZE。\n //\n // ── 全域 prefix 對齊(預設關閉,explicit opt-in)──\n // false(預設):school A,各 menu 自然 prefix 寬度,跨 menu 不對齊\n // true:opt-in CSS `:has()` auto-detect,混用時套用、不混用零成本(school B,Notion 慣例)\n const slotStyle = getUniformPrefixSlotStyle(size)\n const slotValue = slotStyle[\"--item-prefix-slot\" as keyof typeof slotStyle]\n // 2026-05-22 Approach 9 — per-row prefix size mirror(JS const → CSS var):\n // `--item-icon-size` ← ICON_SIZE[size] (sm/md=16, lg=20)\n // `--item-avatar-size` ← AVATAR_SIZE.inline[size] (sm=20, md/lg=24)\n // 供 SidebarMenuButton collapsed pl 公式消費,讓所有 prefix 中心(icon/avatar)鎖回\n // rail center = --sidebar-width-icon/2(per user mental model:rail anchor =\n // GlobalHeader toggle geometry,sidebar 收合所有元素 cx 對齊此 rail center)。\n // Rail anchor 自身不動(--sidebar-menu-icon-size 維持 1rem 固定,per globals.css :root)。\n const wrapperStyle = React.useMemo<React.CSSProperties>(\n () =>\n ({\n \"--item-icon-size\": `${ICON_SIZE[size]}px`,\n \"--item-avatar-size\": `${AVATAR_SIZE.inline[size]}px`,\n ...(uniformPrefix ? { \"--mixed-prefix-slot\": slotValue } : {}),\n ...style,\n } as React.CSSProperties),\n [size, uniformPrefix, slotValue, style]\n )\n\n return (\n <SidebarContext.Provider value={contextValue}>\n <RowSizeProvider value={size}>\n <div\n style={wrapperStyle}\n className={cn(\n \"group/sidebar-wrapper flex min-h-svh w-full\",\n // CSS :has() 偵測 — 只在 uniformPrefix=true(opt-in,預設 false;見 sidebar.spec.md「Sidebar 全域 prefix 對齊」)時掛\n // mixing detected → 同時把 --item-icon-size cascade 到 slot 寬(`!` important 蓋\n // 過 wrapperStyle inline,讓 collapsed pl 公式知道「ItemPrefix wrapper 被撐到\n // 24,effective prefix width = 24 非 16」)。\n uniformPrefix &&\n \"has-[[data-prefix-type=icon]]:has-[[data-prefix-type=avatar]]:[--item-prefix-slot:var(--mixed-prefix-slot)] has-[[data-prefix-type=icon]]:has-[[data-prefix-type=avatar]]:![--item-icon-size:var(--mixed-prefix-slot)]\",\n className\n )}\n ref={ref}\n {...props}\n >\n {children}\n </div>\n </RowSizeProvider>\n </SidebarContext.Provider>\n )\n }\n)\nSidebarProvider.displayName = \"SidebarProvider\"\n\n// ── Sidebar container ──────────────────────────────────────────────────────\n\nconst Sidebar = React.forwardRef<\n HTMLDivElement,\n React.ComponentProps<\"div\"> & {\n side?: \"left\" | \"right\"\n collapsible?: \"offcanvas\" | \"icon\" | \"none\"\n /**\n * Viewport top inset(2026-05-20 ship per AppShell `primary-header` unblock)。\n * 預設 undefined = sidebar 從 viewport top 起算(`top:0 / h:svh`,當前 default)。\n * 提供 CSS value(eg. `'var(--chrome-header-height)'` 或 `'48px'`)時,sidebar 改\n * 從該值起算(`top: viewportInsetTop / height: calc(100svh - viewportInsetTop)`),\n * 讓 global header 不被覆蓋(AppShell primary-header mode 必傳)。\n * 對齊 Mantine `layout=\"default\"` navbar 高度扣 header 慣例。\n *\n * ⚠️ 作用範圍:**僅桌機 `offcanvas` / `icon` 分支**套用 insetStyle。\n * `collapsible=\"none\"`(於 isMobile 前提前 return 固定 div)與 mobile Sheet\n * 兩分支不套此 inset —— 該兩模式下 global header 仍會被覆蓋(未接線)。\n */\n viewportInsetTop?: string\n }\n>(\n (\n {\n side = \"left\",\n collapsible = \"offcanvas\",\n viewportInsetTop,\n className,\n children,\n ...props\n },\n ref\n ) => {\n const { isMobile, state, openMobile, setOpenMobile, mobileOpenerRef } = useSidebar()\n const insetStyle: React.CSSProperties | undefined = viewportInsetTop\n ? { top: viewportInsetTop, height: `calc(100svh - ${viewportInsetTop})` }\n : undefined\n\n if (collapsible === \"none\") {\n return (\n <div\n className={cn(\n \"flex h-full w-[var(--sidebar-width)] flex-col bg-surface text-foreground\",\n className\n )}\n ref={ref}\n {...props}\n >\n {children}\n </div>\n )\n }\n\n if (isMobile) {\n return (\n <Sheet open={openMobile} onOpenChange={setOpenMobile} {...props}>\n <SheetContent\n data-sidebar=\"sidebar\"\n data-mobile=\"true\"\n onCloseAutoFocus={(event) => {\n event.preventDefault()\n const opener = mobileOpenerRef.current\n mobileOpenerRef.current = null\n if (opener?.isConnected) opener.focus({ preventScroll: true })\n }}\n className=\"w-[var(--sidebar-width)] bg-surface p-0 text-foreground [&>button]:hidden\"\n style={\n {\n \"--sidebar-width\": \"var(--sidebar-width-mobile)\",\n } as React.CSSProperties\n }\n side={side}\n >\n <SheetHeader className=\"sr-only\">\n <SheetTitle>Sidebar</SheetTitle>\n <SheetDescription>Displays the mobile sidebar.</SheetDescription>\n </SheetHeader>\n <div className=\"flex h-full w-full flex-col\">{children}</div>\n </SheetContent>\n </Sheet>\n )\n }\n\n return (\n <div\n ref={ref}\n className=\"group peer hidden shrink-0 text-foreground md:block\"\n data-state={state}\n data-collapsible={state === \"collapsed\" ? collapsible : \"\"}\n data-side={side}\n >\n {/* Gap div:佔據 sidebar 實際寬度,推開主內容 */}\n <div\n className={cn(\n \"relative w-[var(--sidebar-width)] min-w-[var(--sidebar-width-min)] bg-transparent transition-[width,min-width] duration-200 ease-linear motion-reduce:duration-0\",\n \"group-data-[collapsible=offcanvas]:!w-0 group-data-[collapsible=offcanvas]:!min-w-0\",\n \"group-data-[side=right]:rotate-180\",\n \"group-data-[collapsible=icon]:!w-[var(--sidebar-width-icon)] group-data-[collapsible=icon]:!min-w-0\"\n )}\n />\n <div\n style={insetStyle}\n className={cn(\n // 2026-05-20 v3:`inset-y-0 h-svh` → 可被 viewportInsetTop prop override(per AppShell primary-header)\n !viewportInsetTop && \"inset-y-0 h-svh\",\n // 2026-05-21 v9 fix(per user 「右側分隔線跑去哪 + sidebar 寬度沒正確變化」):\n // 補 `overflow-x-hidden` — C* refactor 主機制(outer narrows to sidebar-width-icon\n // 時 clip inner 272px overflow,visual right border 顯示,主內容不被遮)。原 C*\n // commit 漏加 className 只 comment 提及。\n \"overflow-x-hidden\",\n \"fixed z-10 hidden w-[var(--sidebar-width)] min-w-[var(--sidebar-width-min)] transition-[left,right,width,min-width] duration-200 ease-linear motion-reduce:duration-0 md:flex\",\n \"group-data-[collapsible=icon]:!min-w-0\",\n side === \"left\"\n ? \"left-0 group-data-[collapsible=offcanvas]:left-[calc(var(--sidebar-width)*-1)]\"\n : \"right-0 group-data-[collapsible=offcanvas]:right-[calc(var(--sidebar-width)*-1)]\",\n \"group-data-[collapsible=icon]:w-[var(--sidebar-width-icon)]\",\n \"group-data-[side=left]:border-r group-data-[side=left]:border-divider\",\n \"group-data-[side=right]:border-l group-data-[side=right]:border-divider\",\n className\n )}\n {...props}\n >\n {/* SidebarInner:永遠 full sidebar-width via inline style(Tailwind JIT 沒 compile\n `min-w-[var(--sidebar-width)]` arbitrary class — 只 generated min-w-sidebar-width-min)。\n Inline style 繞 JIT 確保 272px。Collapsed mode 只是 outer overflow-x:hidden clip 出\n 左側 sidebar-width-icon = visible icon rail。 */}\n <div\n data-sidebar=\"sidebar\"\n style={{ width: 'var(--sidebar-width)', minWidth: 'var(--sidebar-width)' }}\n className=\"flex h-full shrink-0 flex-col bg-surface\"\n >\n {children}\n </div>\n </div>\n </div>\n )\n }\n)\nSidebar.displayName = \"Sidebar\"\n\n// ── Trigger / Rail / Input ─────────────────────────────────────────────────\n\n// SidebarTrigger — 用 Button 原生的 size=\"sm\" iconOnly,\n// 高度自動跟 density 走(field-height-sm @ md=28 / lg=32)\n// 不 override size,避免 density 切換時 trigger 不變\nconst SidebarTrigger = React.forwardRef<\n React.ElementRef<typeof Button>,\n React.ComponentProps<typeof Button>\n>(({ className, onClick, ...props }, ref) => {\n const { toggleSidebar } = useSidebar()\n\n return (\n <Button\n ref={ref}\n data-sidebar=\"trigger\"\n variant=\"text\"\n size=\"sm\"\n iconOnly\n startIcon={PanelLeft}\n aria-label=\"切換側邊欄\" /* i18n-allow: DS default;consumer 經 {...props} override */\n className={className}\n onClick={(event) => {\n const opener = event.currentTarget\n onClick?.(event)\n toggleSidebar(opener)\n }}\n {...props}\n />\n )\n})\nSidebarTrigger.displayName = \"SidebarTrigger\"\n\n// SidebarInput — 用 Input 元件原生高度(跟 density 自動走),不 override\nconst SidebarInput = React.forwardRef<\n React.ElementRef<typeof Input>,\n React.ComponentProps<typeof Input>\n>(({ className, ...props }, ref) => {\n return (\n <Input\n ref={ref}\n data-sidebar=\"input\"\n className={cn(\"w-full\", className)}\n {...props}\n />\n )\n})\nSidebarInput.displayName = \"SidebarInput\"\n\n// ── Shell regions ──────────────────────────────────────────────────────────\n\n// SidebarHeader(固定高度 chrome slot)vs SidebarFooter(content-based):\n// - SidebarHeader:固定高度 `var(--chrome-header-height)`(md=48 / lg=56,density-responsive),\n// 跨元件 chrome header 共享同一個 token,自動跟主內容 page header 對齊\n// - SidebarFooter:高度由內容決定(`shrink-0 py-2 border-t`,不消費 chrome-header-height、\n// 無自身固定高度),見下方 SidebarFooter 定義的 docblock + spec「Chrome 內容的約束」段\n// - 水平 padding 用 loose token(跟 items 的 px 對齊)\n// - 邊框是結構邊界(分隔 fixed/scroll 區),full-width 不內縮\n//\n// 為什麼 Header density-responsive:chrome 裡放的 button 綁定 field-height token,\n// 會隨 density 變大。Chrome 如果不跟著放大,lg density 下 padding 會被擠壓。\nconst SidebarHeader = React.forwardRef<\n HTMLDivElement,\n React.ComponentProps<\"div\"> & { withTabs?: boolean; tabsSlot?: React.ReactNode }\n>(({ className, withTabs, tabsSlot, ...props }, ref) => {\n return (\n // 2026-05-21 v9 — restore main behavior(per user「不應該調整原本的樣式,只有說收起來的時候\n // 要調整 header logo 的位置使其與其下 icon 水平置中」):\n // - Expanded mode:ChromeHeader default `px-loose`(16px L+R),avatar 跟 menu icon 對齊\n // - Collapsed mode:`!px-0 !justify-center` 拿掉 padding + 內容置中,WorkspaceBrand text\n // 已 `group-data-[collapsible=icon]:hidden`,collapsed 只剩 avatar 24px,centered in 48px\n // square → avatar.center = 24 = menu icon center.x ✓\n // 完全 match main 行為(v7 leadingRail / v8 -ml-1 surgical 全撤回)。\n <ChromeHeader\n ref={ref}\n withTabs={withTabs}\n tabsSlot={tabsSlot}\n data-sidebar=\"header\"\n className={cn(\n // 2026-05-21 v14 — rail-derived collapsed padding,full SSOT cascade(per user directive\n // 「avatar尺寸、menu item padding、menu icon尺寸任一變動都要正確連動」+ codex Layer B\n // 比稿共識):\n //\n // 公式 derivation(rail-centered geometric identity):\n // avatar.cx = pl + avatar/2 = (sidebar-width-icon - avatar)/2 + avatar/2 = sidebar-width-icon/2\n // menu icon.cx = loose + icon-size/2\n // sidebar-width-icon = 2*loose + icon-size → sidebar-width-icon/2 = loose + icon-size/2 ✓\n //\n // → 任何 avatar / loose / icon-size 改值,公式自動對齊 menu icon 中心(rail 幾何恆等)。\n //\n // SSOT 連動鏈:\n // --layout-space-loose (density token) ─┐\n // --sidebar-menu-icon-size ├→ --sidebar-width-icon (calc cascade)\n // └→ 公式自動跟 ✓\n // --chrome-header-avatar-size (header-canonical.css local token):公式 var 引用,\n // JS 端透過 WorkspaceBrand RowSizeProvider value=\"md\"\n // + AVATAR_SIZE.inline.md spec-coupled 共識 sync。\n //\n // Collapsed pl 幾何值(rail-centered,--sidebar-width-icon = 2*loose + menu-icon):\n // md density: (48-24)/2 = 12 · lg density: (68-24)/2 = 22\n // (lg width = 2×24+20 = 68,非 stale 64;avatar-size = 24 both densities)\n // 註:已撤回的 v13 `loose-4` 近似在 lg 給 20,與新 rail-幾何精確值 22 不同 —\n // v14 以 SSOT chain 公式取代該近似,md 兩者恰好都是 12。\n \"group-data-[collapsible=icon]:!pl-[calc((var(--sidebar-width-icon)-var(--chrome-header-avatar-size))/2)]\",\n \"group-data-[collapsible=icon]:!pr-0\",\n \"transition-[padding] duration-200 ease-linear motion-reduce:duration-0\",\n className\n )}\n {...props}\n />\n )\n})\nSidebarHeader.displayName = \"SidebarHeader\"\n\n// SidebarFooter — pinned 在 sidebar 底部的 menu group 容器,不是固定高度 chrome slot。\n// 行為跟 SidebarGroup 類似(flex flex-col py-2),但永遠靠底(shrink-0)且有 border-t 分隔。\n// Consumer 放 SidebarMenu + SidebarMenuButton,高度由內容決定(1~N 個 items)。\n// 典型內容:user menu、settings、help、logout 等底部選項群組。\nconst SidebarFooter = React.forwardRef<\n HTMLDivElement,\n React.ComponentProps<\"div\">\n>(({ className, ...props }, ref) => {\n return (\n <div\n ref={ref}\n data-sidebar=\"footer\"\n className={cn(\n \"flex shrink-0 flex-col py-2 border-t border-divider\",\n className\n )}\n {...props}\n />\n )\n})\nSidebarFooter.displayName = \"SidebarFooter\"\n\nconst SidebarSeparator = React.forwardRef<\n React.ElementRef<typeof Separator>,\n React.ComponentProps<typeof Separator>\n>(({ className, ...props }, ref) => {\n // 預設對齊 loose token:分隔兩個 scrollable group 時,跟 item 內容對齊。\n // 分隔 fixed 元件(Header/Footer 與 Content)時,consumer 可傳 className 覆寫成 mx-0。\n return (\n <Separator\n ref={ref}\n data-sidebar=\"separator\"\n className={cn(\n \"mx-[var(--layout-space-loose)] w-auto bg-divider\",\n className\n )}\n {...props}\n />\n )\n})\nSidebarSeparator.displayName = \"SidebarSeparator\"\n\nconst SidebarContent = React.forwardRef<\n HTMLDivElement,\n React.ComponentProps<\"div\">\n>(({ className, children, ...props }, ref) => {\n return (\n <div\n ref={ref}\n data-sidebar=\"content\"\n // SidebarContent 用 ScrollArea 處理長列表 scroll——跨 OS 一致不吃寬度(macOS\n // overlay vs Windows/Linux always-visible 差異見 scroll-area.tsx 註解)。\n // 呼吸空間和分隔線由 SidebarGroup 自己處理(對齊 MenuGroup 的 py-2 + [&+&]:border-t)。\n // ScrollArea Root 本身 overflow-hidden,icon-collapsed 時不會露出 scroll chrome。捲動區本身不當 Tab 停靠點(viewportTabIndex={-1};待辦總帳 B9,依 `../ScrollArea/scroll-area.spec.md:103`「內容已有自帶 focusable + 鍵盤處理的元素」時傳 -1):內容是一串串自帶方向鍵的 SidebarMenu,焦點落進任何一項都會把它捲進視野;先前 Tab 第一站是這個捲動區(「混合內容」實測第 1 站),停在上面按 ↓ 焦點不動。\n className={cn(\n \"flex min-h-0 flex-1 flex-col\",\n className\n )}\n {...props}\n >\n <ScrollArea className=\"flex-1\" viewportTabIndex={-1}>\n <div className=\"flex flex-col\">{children}</div>\n </ScrollArea>\n </div>\n )\n})\nSidebarContent.displayName = \"SidebarContent\"\n\n// ── Group ──────────────────────────────────────────────────────────────────\n\n// SidebarGroupContext——讓 `SidebarGroupLabel` 和 `SidebarGroupContent` 知道當前\n// 所在的 group 是否 collapsible,以自動切換渲染模式(label 尾端 chevron button 變\n// `Collapsible.Trigger`、content 變 `Collapsible.Content`;label 本身維持\n// `role=\"presentation\"` div,不升格為 button)。沒有 context 就是舊行為(plain div)。\ntype SidebarGroupContextValue = {\n collapsible: boolean\n}\nconst SidebarGroupContext = React.createContext<SidebarGroupContextValue | null>(null)\n\nfunction useSidebarGroup() {\n return React.useContext(SidebarGroupContext)\n}\n\n/**\n * SidebarGroup\n *\n * 預設是非互動的 plain group(div)。當 `collapsible` = true 時自動切換成 Radix\n * Collapsible:SidebarGroupLabel 尾端自動渲染 chevron button 作為 `Collapsible.Trigger`\n * (承載 aria-expanded / data-state,依 open state 旋轉),SidebarGroupContent 變 Content;\n * label 本身維持 `role=\"presentation\"` div,不升格為 button。\n *\n * ── API 設計決策 ──\n * 為什麼是 group 層級的 prop 而不是 label 層級?因為「group 是否可收合」是結構層\n * 的決定,影響 group 子 primitive 的渲染模式(label 尾端 chevron 變 trigger button、\n * content 變 animated container)。把 prop 放在 label 上會讓 content 不知道自己該不該被包,\n * 形成跨元件的 prop drilling。放在 group 上用 context 傳遞是 React 的標準做法。\n */\nconst SidebarGroup = React.forwardRef<\n HTMLDivElement,\n React.ComponentProps<\"div\"> & {\n collapsible?: boolean\n defaultOpen?: boolean\n open?: boolean\n onOpenChange?: (open: boolean) => void\n }\n>(({ className, collapsible = false, defaultOpen = true, open, onOpenChange, children, ...props }, ref) => {\n const contextValue = React.useMemo<SidebarGroupContextValue>(\n () => ({ collapsible }),\n [collapsible]\n )\n\n // 非 collapsible 就是 plain div,舊行為完全不變\n const baseClass = cn(\n \"relative flex w-full min-w-0 flex-col py-2\",\n \"[&+&]:before:absolute [&+&]:before:top-0 [&+&]:before:left-[var(--layout-space-loose)] [&+&]:before:right-[var(--layout-space-loose)] [&+&]:before:h-px [&+&]:before:bg-divider [&+&]:before:content-['']\",\n className\n )\n\n if (!collapsible) {\n return (\n <SidebarGroupContext.Provider value={contextValue}>\n <div ref={ref} data-sidebar=\"group\" className={baseClass} {...props}>\n {children}\n </div>\n </SidebarGroupContext.Provider>\n )\n }\n\n // Collapsible 模式:Radix Collapsible.Root 當 group container\n return (\n <SidebarGroupContext.Provider value={contextValue}>\n <CollapsiblePrimitive.Root\n ref={ref}\n data-sidebar=\"group\"\n defaultOpen={defaultOpen}\n open={open}\n onOpenChange={onOpenChange}\n className={baseClass}\n // 註:此 Collapsible.Root 本身**不**自帶 icon 模式隱藏 class(baseClass 無\n // `group-data-[collapsible=icon]:hidden`),icon 模式下預設仍渲染。若要在 icon rail\n // 隱藏整個可收合 group(icon rail 沒空間放展開的 tree),consumer 自行傳\n // `className=\"group-data-[collapsible=icon]:hidden\"` 覆寫。\n {...(props as React.ComponentProps<typeof CollapsiblePrimitive.Root>)}\n >\n {children}\n </CollapsiblePrimitive.Root>\n </SidebarGroupContext.Provider>\n )\n})\nSidebarGroup.displayName = \"SidebarGroup\"\n\nconst SidebarGroupContent = React.forwardRef<\n HTMLDivElement,\n React.ComponentProps<\"div\">\n>(({ className, ...props }, ref) => {\n const group = useSidebarGroup()\n if (group?.collapsible) {\n return (\n <CollapsiblePrimitive.Content\n // Radix 提供 data-state=\"open|closed\",搭配 animate 做展開/收合動畫\n // (若之後需要,可在這個 div 上加 CSS animation)\n data-sidebar=\"group-content\"\n className={cn(\"w-full overflow-hidden\", className)}\n {...(props as React.ComponentProps<typeof CollapsiblePrimitive.Content>)}\n >\n <div ref={ref}>{props.children}</div>\n </CollapsiblePrimitive.Content>\n )\n }\n return (\n <div\n ref={ref}\n data-sidebar=\"group-content\"\n className={cn(\"w-full\", className)}\n {...props}\n />\n )\n})\nSidebarGroupContent.displayName = \"SidebarGroupContent\"\n\n// SidebarGroupLabel — 完全對齊 MenuItem header 的視覺 + TreeView 的 sm/md/lg 尺寸:\n// - 跟 SidebarMenuButton 共用 item-layout 公式(同 py 公式、同 text size、同 icon size)\n// - 唯一差異:font-medium + text-fg-muted + pointer-events-none(header 語意)\n// 這樣 label 和 items 的 row height 在任何 size 下都完全對齊。\n//\n// Icon 模式:整列 display:none(不是 -mt + opacity 0,避免脆弱的 margin 計算)\nconst sidebarGroupLabelVariants = cva(\n [\n // items-start 對齊 item-layout 規則(跟 SidebarMenuButton / TreeItem / MenuItem 一致)\n \"flex w-full items-start gap-2\",\n \"px-[var(--layout-space-loose)]\",\n \"font-medium text-fg-muted\",\n // @focus-suppress N — 不適用(不可操作 → 問題一);承擔者:pointer-events-none 的分組標題,不可操作 → 問題一:不該可聚焦,不用畫\n \"cursor-default select-none pointer-events-none outline-none\",\n // icon 模式:GroupLabel 純視覺分段 → collapsed 連 SR 一併隱藏是刻意(供名責任在各 menu button 的 sr-only label,2026-07-05 D4 修後與 MenuButton 策略分工)\n \"group-data-[collapsible=icon]:hidden\",\n ],\n {\n variants: {\n // 消費 ROW_PADDING_BY_SIZE SSOT(跟 SidebarMenuButton / MenuItem / TreeItem 一致)\n size: ROW_PADDING_BY_SIZE,\n },\n defaultVariants: { size: \"md\" },\n }\n)\n\nconst SidebarGroupLabel = React.forwardRef<\n HTMLDivElement,\n React.ComponentProps<\"div\"> & {\n asChild?: boolean\n /** ARIA label for the expand/collapse chevron (only when group is collapsible). Override for i18n. Default: 「展開或收合」 */\n toggleAriaLabel?: string\n } & VariantProps<typeof sidebarGroupLabelVariants>\n>(({ className, asChild = false, size: sizeProp, children, toggleAriaLabel = '展開或收合' /* i18n-allow: DS default; consumer override via toggleAriaLabel prop */, ...props }, ref) => {\n const { size: contextSize } = useSidebar()\n const size = sizeProp ?? contextSize\n const group = useSidebarGroup()\n\n // Collapsible group:label 本身仍是 plain header(保留原本語意),\n // chevron 是 suffix slot 裡的 inline action button,用 Radix Collapsible.Trigger asChild\n // 包住 `ItemInlineActionButton`——視覺規格完全對齊 patterns/element-anatomy/inline-action.spec.md(Inline Action SSOT),\n // 跟 SidebarMenuButton 的 suffix inline action 同一套 canonical 實作。\n //\n // **為什麼 chevron 是 inline action 而非整個 label 是 trigger**:\n // 1. 同 sidebar 內的 inline action 視覺必須一致(fg-muted → fg-secondary、hover bg、圓角)\n // 2. Linear / Finder 等世界級 sidebar 是「label 裝飾、chevron 互動」。**Notion 不是**(2026-09-29 出處更正,待辦總帳 N46):\n // Notion 的側欄說明頁逐字 \"Click on the name of a sidebar section — Teamspaces, Shared, or Private — to collapse it\"\n // (https://www.notion.com/help/navigate-with-the-sidebar)—— 點區段名稱就收合;本 DS 沒有跟,理由是第 1 與第 3 點。\n // 2026-09-29 前這裡把 Notion 列在同一邊,是憑印象寫的(M22)。\n // 3. 整個 label 當 trigger 會把 label 升格為 button,跟非 collapsible group 的 label\n // 語意不一致(一個是 div、一個是 button),無障礙與視覺焦點都會跳動\n if (group?.collapsible) {\n return (\n <div\n ref={ref}\n data-sidebar=\"group-label\"\n role=\"presentation\"\n className={cn(\n sidebarGroupLabelVariants({ size }),\n // Single-line group header:改用 items-center 讓 chevron button(16×16)跟 text 垂直精確置中。\n // cva base 的 `items-start` 是 multi-line safety,單行 header 用不到,明確覆寫成 center。\n \"!items-center\",\n className\n )}\n {...props}\n >\n <span className=\"min-w-0 flex-1 truncate\">{children}</span>\n {/* Chevron:inline action button,Radix Collapsible.Trigger asChild 包住,\n Radix 會 merge onClick + aria-expanded + data-state 到 button。\n ── Suffix 位置的 chevron 用 accordion 慣例 ──\n base = ChevronDown (`v`),open 時 rotate-180° → `^`。\n 對齊 Radix Accordion / shadcn Collapsible / Material Expansion / Linear 的 section header。\n (Prefix 位置的 chevron 如 TreeView 才用 ChevronRight + rotate-90 的 tree disclosure 慣例) */}\n <CollapsiblePrimitive.Trigger asChild>\n <ItemInlineActionButton\n icon={ChevronDown}\n aria-label={toggleAriaLabel}\n className=\"pointer-events-auto ml-auto\"\n iconClassName=\"transition-transform duration-150 motion-reduce:duration-0 [[data-state=open]_&]:rotate-180\"\n />\n </CollapsiblePrimitive.Trigger>\n </div>\n )\n }\n\n // 非 collapsible:plain label(div,pointer-events-none)\n const Comp = asChild ? Slot : \"div\"\n return (\n <Comp\n ref={ref}\n data-sidebar=\"group-label\"\n role=\"presentation\"\n className={cn(sidebarGroupLabelVariants({ size }), className)}\n {...props}\n >\n {children}\n </Comp>\n )\n})\nSidebarGroupLabel.displayName = \"SidebarGroupLabel\"\n\n// ⚠️ 2026-09-24:這兩顆先前是 shadcn 原樣帶進來的**手刻品**(`b7b34721` 把 DS 搬進 npm workspace\n// 時一起進來,之後從未跟著本 DS 的行內動作 canonical 遷移)。它們自己寫死\n// `aspect-square w-5 ... [&>svg]:size-4`,也就是 **16 圖示裝在 20 盒裡** —— 而\n// `patterns/element-anatomy/inline-action.spec.md` 的尺寸表只有兩種組合(16 圖示配 18 底色、\n// 20 圖示配 22 底色),**20 兩種都不是**。同一個檔案 :826 的收合箭頭早就在消費\n// `ItemInlineActionButton`,於是同一個元件裡兩套幾何並存。\n// 依 M23(DS 既有 canonical 優先)與 M30(wrapper 必須繼承 primitive,不得平行宣告)改為委派。\n// API 隨之從 children 改成 `icon` prop —— 這是 breaking change,刻意不留 children 後備:\n// M23(f)「『向後相容』不是把新裁示變成可選的理由」。\n//\n// 命中區 ≡ 懸停底色由 primitive 自己保證(hit-area-canonical「一-4 細則」:行內動作平時沒有可視邊界,\n// 判準是懸停回饋形狀,不是「可視形狀」;`item-anatomy.tsx:730` 那塊**刻意沒有**\n// `pointer-events-none` 的懸停底色 span)。先前這裡另有一圈 `after:-inset-2 after:md:hidden`,\n// 只在 <md 生效、每邊多 8px,實測上下各越出宿主 `<li>` 2px 並蓋掉緊貼的下一列列鈕,已於同日移除。\nconst SidebarGroupAction = React.forwardRef<\n HTMLButtonElement,\n Omit<ItemInlineActionButtonProps, \"size\">\n>(({ className, ...props }, ref) => (\n <ItemInlineActionButton\n ref={ref}\n data-sidebar=\"group-action\"\n className={cn(\n \"absolute right-[var(--layout-space-loose)] top-2\",\n \"group-data-[collapsible=icon]:hidden\",\n className\n )}\n {...props}\n />\n))\nSidebarGroupAction.displayName = \"SidebarGroupAction\"\n\n// ── Menu ───────────────────────────────────────────────────────────────────\n\n// SidebarMenu — items 容器\n//\n// Prefix 對齊由 `SidebarProvider` 的 `uniformPrefix`(opt-in,預設 false)全域處理(school B,\n// Notion/Linear 慣例):開啟後,整個 sidebar 子樹同時存在 icon 和 avatar prefix 時,自動套用\n// 固定槽,跨 menu 跨 group 對齊。要啟用,在 `<SidebarProvider uniformPrefix>` 全域 opt-in\n// (explicit > implicit,見 sidebar.spec.md「Sidebar 全域 prefix 對齊」段)。SidebarMenu 沒有\n// per-menu 覆寫——沒有真實 use case,YAGNI。\n//\n// ── 鍵盤:一串 SidebarMenu = 一個 Tab 停靠點(2026-09-25,待辦總帳 B9 乙)──\n// 決策出處 = governance/planning/2026-09-25-interaction-and-hover-remediation.md B9,user 逐字:\n// 「確定建議符合我們一致的設計語言且不違背世界級的設計就照建議」(附條件同意;條件查證成立記在同檔 B9)。\n// 規格 SSOT = sidebar.spec.md「鍵盤:一串 SidebarMenu = 一個 Tab 停靠點」;按鍵判定與執行 = lib/roving-list-keyboard.ts\n// (2026-09-26 與 TreeView / FileUpload / Command 四份合一,同檔〇節「按鍵規則合併」),此處只剩側欄自己的 DOM:誰是列、誰是動作鈕。\n//\n// 實作 = roving tabindex,**直接改 DOM 的 tabindex 屬性**,不經 React props:\n// - 列上的動作鈕有兩個出口不歸本元件渲染 —— `ItemInlineAction`(item-anatomy.tsx,沒有 tabIndex prop)\n// 與 consumer 的 `inlineActionsSlot`,只能在 DOM 上把它們收成 -1。同一件事只用一種機制(M17),\n// 所以列鈕的 0 / -1 也在同一個函式裡寫(AI 推導的工程取捨)。\n// - 沒有 JS(SSR 首屏、hydration 前)時什麼都不寫 → 退回瀏覽器原生:每顆都可 Tab,不會變成到不了。\n// - ARIA 角色維持 ul / li / button,不改成 grid / tree:狀態是「你現在在這一頁」(aria-current),\n// 不是「在控件裡被挑中」(aria-selected)。\nconst SIDEBAR_MENU_BUTTON = '[data-sidebar=\"menu-button\"]'\n// 列上的動作鈕 = 宣告式 inlineActions / inlineActionsSlot(包在 menu-inline-actions 裡)+ SidebarMenuAction 本身\nconst SIDEBAR_ROW_ACTIONS =\n '[data-sidebar=\"menu-inline-actions\"] :is(button, a[href], [role=\"button\"]), [data-sidebar=\"menu-action\"]'\n\ntype SidebarMenuRow = { button: HTMLElement; actions: HTMLElement[] }\n\n/** 這一串的每一列(只看直接子層 li,依畫面順序)與它自己的動作鈕(依畫面順序;含此刻走不到的,收 -1 時一併收)。 */\nfunction getSidebarMenuRows(menu: HTMLElement): SidebarMenuRow[] {\n const rows: SidebarMenuRow[] = []\n for (const child of Array.from(menu.children)) {\n if (child.tagName !== \"LI\") continue\n const button = child.querySelector<HTMLElement>(SIDEBAR_MENU_BUTTON)\n if (!button) continue\n rows.push({ button, actions: Array.from(child.querySelectorAll<HTMLElement>(SIDEBAR_ROW_ACTIONS)) })\n }\n return rows\n}\n\n// 原生 disabled 本來就聚焦不了 → 跳過(sidebar.spec.md「Disabled item」)。aria-disabled 仍可聚焦,照舊。\nconst isFocusableSidebarTarget = (el: HTMLElement) => !(el as HTMLButtonElement).disabled\n// 畫得出來才走得到:icon 模式的動作鈕、被收起的整組都是 display:none(opacity-0 的滑過才出現鈕不算隱藏)\nconst isRenderedSidebarTarget = (el: HTMLElement) => el.getClientRects().length > 0\n\n/**\n * 哪些列能當落點(方向鍵與 Tab 停靠點共用)。整串看得見時,略過個別被藏起來的列;整串被藏(icon 模式收起的群組)時\n * 不看可見性,免得重新展開後整串 0 顆可 Tab(展開是祖先屬性變化,本串收不到 mutation)。\n */\nfunction sidebarRowNavigability(menu: HTMLElement, rows: SidebarMenuRow[]) {\n const checkRendered =\n isRenderedSidebarTarget(menu) &&\n rows.some((row) => isFocusableSidebarTarget(row.button) && isRenderedSidebarTarget(row.button))\n return (button: HTMLElement) =>\n isFocusableSidebarTarget(button) && (!checkRendered || isRenderedSidebarTarget(button))\n}\n\n/**\n * 整串只留一顆 tabindex=0:上次焦點停過的那一列 → 當前頁(aria-current=\"page\")→ 第一個可用列(X1;\n * 判定 = lib/roving-list-keyboard.ts `pickRovingTabStop`)。動作鈕一律 -1(別項的與本項的都不在 Tab 路上;→ 才進得去)。\n */\nfunction syncSidebarMenuTabStops(menu: HTMLElement, remembered: HTMLElement | null) {\n const rows = getSidebarMenuRows(menu)\n const stop = pickRovingTabStop(rows.map((row) => row.button), {\n remembered,\n isCurrent: (button) => button.getAttribute(\"aria-current\") === \"page\",\n isNavigable: sidebarRowNavigability(menu, rows),\n })\n for (const row of rows) {\n setRovingTabIndex(row.button, row.button === stop ? 0 : -1)\n removeRovingControlsFromTabOrder(row.actions)\n }\n}\n\nconst SidebarMenu = React.forwardRef<\n HTMLUListElement,\n React.ComponentProps<\"ul\">\n>(({ className, onKeyDownCapture, onFocus, ...props }, ref) => {\n const menuRef = React.useRef<HTMLUListElement | null>(null)\n const rememberedRef = React.useRef<HTMLElement | null>(null)\n // 收合 / 展開、手機抽屜切換時重算一次(可見性在這些時刻改變,但本串自己的 DOM 沒變)。\n // 用 useContext 而非 useSidebar():SidebarMenu 單獨渲染(沒有 Provider)時不該丟例外。\n const sidebarContext = React.useContext(SidebarContext)\n const sidebarState = sidebarContext?.state\n const sidebarIsMobile = sidebarContext?.isMobile\n\n const setRefs = React.useCallback(\n (el: HTMLUListElement | null) => {\n menuRef.current = el\n if (typeof ref === \"function\") ref(el)\n else if (ref) (ref as React.MutableRefObject<HTMLUListElement | null>).current = el\n },\n [ref]\n )\n\n // 掛上時、以及收合 / 展開 / 手機切換後同步一次;本串 DOM 的其他變化交給下方 MutationObserver\n React.useLayoutEffect(() => {\n if (menuRef.current) syncSidebarMenuTabStops(menuRef.current, rememberedRef.current)\n }, [sidebarState, sidebarIsMobile])\n\n // 列新增 / 移除、disabled 改變、子層自己重繪而本串沒重繪(例:當前頁換了 → 列鈕的 aria-current 變,\n // 但 SidebarMenu 不訂閱 activeId)\n React.useEffect(() => {\n const menu = menuRef.current\n if (!menu || typeof MutationObserver === \"undefined\") return\n const observer = new MutationObserver(() => syncSidebarMenuTabStops(menu, rememberedRef.current))\n observer.observe(menu, {\n childList: true,\n subtree: true,\n attributes: true,\n attributeFilter: [\"disabled\", \"aria-current\"],\n })\n return () => observer.disconnect()\n }, [])\n\n // 焦點用任何方式進到某一列(Tab 進來 / 方向鍵 / 滑鼠點 / 選單關閉後還焦點)→ 停靠點跟過去,\n // 下次 Tab 回來就回到這裡(同 calendar.tsx 日期鈕的 onFocus 同步)。\n const handleFocus = (event: React.FocusEvent<HTMLUListElement>) => {\n onFocus?.(event)\n const menu = event.currentTarget\n const target = event.target as HTMLElement\n const row = getSidebarMenuRows(menu).find(\n (r) => r.button === target || r.actions.includes(target)\n )\n if (!row) return\n rememberedRef.current = row.button\n syncSidebarMenuTabStops(menu, row.button)\n }\n\n // 用 capture:方向鍵整串歸清單,要搶在列鈕 / 動作鈕自己的處理之前 —— 帳號列是 `DropdownMenuTrigger asChild`、\n // 動作鈕可能是選單觸發鈕(inlineActionsSlot),Radix 會拿 ↓ 開選單;這裡先 preventDefault,Radix 的\n // composeEventHandlers 看到 defaultPrevented 就不執行,開選單照樣用 Enter / Space(AI 推導,見 spec;X6 以此為全宿主的做法)。\n const handleKeyDownCapture = (event: React.KeyboardEvent<HTMLUListElement>) => {\n onKeyDownCapture?.(event)\n const menu = event.currentTarget\n const target = event.target as HTMLElement\n const rows = getSidebarMenuRows(menu)\n const row = rows.find((r) => r.button === target || r.actions.includes(target))\n // 焦點不在列或動作鈕上(consumer 自己放進列裡的其他控件)→ 不接管,讓它自己的鍵盤照舊\n if (!row) return\n const onRow = row.button === target\n const controls = row.actions.filter(isRovingControlReachable)\n const action = resolveRovingKey({\n key: event.key,\n focus: onRow ? \"item\" : \"control\",\n controlCount: controls.length,\n controlIndex: onRow ? -1 : controls.indexOf(target),\n controlIsTextEntry: !onRow && isTextEntryElement(target),\n defaultPrevented: event.defaultPrevented,\n metaKey: event.metaKey,\n ctrlKey: event.ctrlKey,\n altKey: event.altKey,\n shiftKey: event.shiftKey,\n })\n const isNavigable = sidebarRowNavigability(menu, rows)\n applyRovingAction(action, {\n event,\n item: row.button,\n controls,\n items: rows.map((r) => r.button),\n isNavigable,\n })\n }\n\n return (\n <ul\n ref={setRefs}\n data-sidebar=\"menu\"\n // 無 gap:items 連續緊貼(對齊 DropdownMenu / TreeView 的視覺節奏)\n className={cn(\"flex w-full min-w-0 flex-col\", className)}\n onFocus={handleFocus}\n onKeyDownCapture={handleKeyDownCapture}\n {...props}\n />\n )\n})\nSidebarMenu.displayName = \"SidebarMenu\"\n\nconst SidebarMenuItem = React.forwardRef<\n HTMLLIElement,\n React.ComponentProps<\"li\">\n>(({ className, ...props }, ref) => (\n <li\n ref={ref}\n data-sidebar=\"menu-item\"\n className={cn(\"group/menu-item relative\", className)}\n {...props}\n />\n))\nSidebarMenuItem.displayName = \"SidebarMenuItem\"\n\n/**\n * SidebarMenuButton\n *\n * 對齊 MenuItem / TreeItem 的 item-layout,支援 sm/md/lg 三種尺寸:\n * - horizontal: px = var(--layout-space-loose)\n * - vertical: py = calc((--field-height-{size} - 1lh) / 2)\n * - typography: text-body leading-compact(sm/md)/ text-body-lg(lg)+ font-medium\n * - icon: size-4 (sm/md) / size-5 (lg)\n * - hover: bg-neutral-hover\n * - selected: bg-neutral-selected(data-active=true)\n *\n * Icon 模式:button 保持 w-full 填滿 sidebar icon rail,**無 height lock / 無 justify-center**\n * (2026-05-21 v4 C* 撤回 `!h-[...]`——row height 永遠 stable,collapsed 只 outer clip);\n * prefix(icon / avatar)靠 `!pl-[calc((rail - prefix-width)/2)]` 公式置中對齊 rail,\n * label span 以 sr-only 視覺隱藏(a11y tree 保留,2026-07-05 D4——SR 仍讀得到 accessible name)。\n */\nconst sidebarMenuButtonVariants = cva(\n [\n \"peer/menu-button group/menu-button\",\n // items-start(跟 TreeItem / MenuItem 一致的 item-layout 規則):\n // 多行 label 時 prefix 留在第一行不飄移。\n // 單行 label 時(我們的預設情境,truncate = line-clamp-1),效果跟 items-center 完全相同,\n // 因為 prefix 被包在 `h-[1lh] flex items-center` 容器,強制對齊第一行文字中線。\n \"flex w-full items-start gap-2 text-left overflow-hidden\",\n \"px-[var(--layout-space-loose)]\",\n \"font-medium text-fg-secondary\",\n \"cursor-pointer select-none\",\n // 2026-05-21 v4 C* refactor(per codex M31 Layer C 比稿 final architecture):\n // 撤回所有 `group-data-[collapsible=icon]:*` overrides。Sidebar outer overflow-x:hidden\n // 自動 clip menu button 右側 label / badge / action,左側 icon 自然顯示在 sidebar-width-icon\n // range 內。Row geometry 永遠 stable(px-loose + items-start + gap-2 + height auto),\n // 無 discrete property switch → 無 fly / 無 flutter / 無 jitter。\n //\n // 2026-07-05 D4 更新:v4「outer clip、SR 仍讀到」的宣稱在 v5 label display:none 後\n // 已失真;現行 = label 走 sr-only(SR 讀到、供 accessible name,見下方 v5/D4 註解),\n // badge / action / inline-actions 仍 display:none(視覺 + SR 一併隱藏——collapsed 供名\n // 責任在 label,badge 計數屬補充資訊)。Tooltip 仍在 collapsed state 顯示\n // (per buttonWithTooltip 既有 hidden 條件),僅 hover 提示、不供名。\n //\n // 對齊 MUI MiniDrawer + shadcn canonical(width morph + overflow clip + row geometry 不變)。\n // hover 底色瞬間切換,不做過渡(user 2026-09-10 拍板「第三題改成全部瞬間」;SSOT = tokens/motion/motion.spec.md「hover 回饋不做過渡」)\n \"hover:bg-neutral-hover hover:text-foreground\",\n // 巢狀 hover(2026-09-25,user 選「卡片保留、按鈕再亮一層」;SSOT = sidebar.spec.md「Inline actions」段「指標在動作鈕上時(巢狀 hover)」):\n // 指標移到同一列的動作鈕(inlineActions 的 suffix span / SidebarMenuAction)上,列保留上一行的 hover,動作鈕再亮自己那層。\n // 需要這兩行是因為動作鈕是列鈕的**同層兄弟**(避免 button 包 button,見下方 suffixNode 註解)—— 指標在兄弟上時列鈕不算 :hover。\n // - `~` 只往後找:動作必須排在列鈕後面(兩個內建出口都是;SidebarMenuAction 的 peer-data 也早就要求這個順序)。\n // - 外層包 `:where()` 把特異性壓回 (0,1,0),下方 `data-[active=true]` 選中色 (0,2,0) 照樣釘住;不包會是 (0,3,0),選中列指到動作鈕時選中色被 hover 色蓋掉(2026-09-25 Chromium 實測)。\n // - 不掛在 li 的 `group-hover/menu-item`:li 收任意子元素,指到 li 裡的子項目時父列會誤亮(同日實測)。\n // - `[@media(hover:hover)]` 對齊上一行 `hover:` 的媒體條件(Tailwind 4 的 hover: 只在能懸停的裝置生效),觸控裝置不留殘亮。\n // 值與上一行 `hover:` 是同一對;改那一行必須同步改這兩行。\n \"[@media(hover:hover)]:[&:where(:has(~:is([data-sidebar=menu-inline-actions],[data-sidebar=menu-action]):hover))]:bg-neutral-hover\",\n \"[@media(hover:hover)]:[&:where(:has(~:is([data-sidebar=menu-inline-actions],[data-sidebar=menu-action]):hover))]:text-foreground\",\n // 鍵盤游標一律畫框、不上底色(focus-canonical 規則二,user 2026-09-09 拍板);選單鈕撐滿側欄寬度 → 內描邊。\n // 2026-09-09 之前非當前項的焦點用 hover 同色底(AI 推導,user 撤回);hover 的底色與文字色只屬於滑鼠。\n \"focus-visible:focus-ring-inset\",\n // 2026-05-31 M24:SidebarMenuButton 主要為 icon+label(currentColor 可改寫)→ 用 semantic\n // text-fg-disabled 對齊 MenuItem primitive(menu-item.tsx:198/221/248 + item-anatomy.tsx:374),\n // 非 opacity-disabled(opacity 保留給圖片/avatar/Switch 等無法改寫內部色者,per color.spec.md:103/118/701)。\n // 原生 disabled 的 button 本來就不會發 click,不再 pointer-events-none:留著接指標只為了掛禁止符號\n //(hit-area-canonical.md 滑過原則一-3「停用的用禁止符號」;2026-09-29 全站掃出:「混合內容」的「delta(已封存)」游標是箭頭,\n // 因為 pointer-events-none 讓 cursor 由 li 決定)。上方 hover 的底色 / 字色由 disabled:hover 釘住(:disabled:hover 特異性高過 :hover)。\n \"disabled:text-fg-disabled disabled:cursor-not-allowed disabled:hover:bg-transparent disabled:hover:text-fg-disabled\",\n // aria-disabled(asChild 的 <a> 等非 button 宿主)仍靠 pointer-events-none 擋掉點擊 / 導覽 —— 那是它唯一的擋法;\n // 代價是游標由外層決定(箭頭)。要拿掉這裡的 pointer-events-none,得先在 asChild 宿主補 click guard(另案)。\n \"aria-disabled:pointer-events-none aria-disabled:text-fg-disabled\",\n \"data-[active=true]:bg-neutral-selected data-[active=true]:text-foreground\",\n // 2026-08-11 user 拍板(SSOT = item-anatomy「選中 × 互動疊加」):滑鼠 hover 當前項釘住不變\n //(先前無規則,靠編譯順序運氣;現寫成顯式,釘住 (0,3,0) > hover (0,2,0))。\n // 鍵盤游標停在當前項 = 框疊在選中底色上(框由上方 focus-visible:focus-ring-inset 統一畫,不分當前與否;\n // 2026-09-07「A5畫框」只補了當前項,2026-09-09 user 拍板後全部畫框、焦點不再改底色)。\n \"data-[active=true]:hover:bg-neutral-selected\",\n \"group-has-[[data-sidebar=menu-action]]/menu-item:pr-8\",\n // 2026-05-21 v5 restore label 硬隱藏(user 抓「label 沒消失」):\n // C* outer overflow-x:hidden 理論 clip,但 label.x=40 在 sidebar-width-icon=48 內 → 首字\n // 部分可見。instant 切換非 main animation,跟 C* 「不用 display:none 做主\n // 動畫」不衝突(label 不參與 width 動畫,純 final state)。\n // 2026-07-05 D4 修:v5 的 display:none 讓 accessible-name 計算排除 label\n // (icon aria-hidden、badge 也 hidden)→ collapsed 時 menu button 對 SR 是無名\n // button(WCAG 4.1.2)。改 sr-only:position:absolute + 1px + clip 視覺隱藏但保留\n // a11y tree,button 仍有 accessible name;absolute 移出 flex flow,layout 效果同\n // display:none(icon 置中公式不變)。\n \"group-data-[collapsible=icon]:[&_[data-sidebar=menu-label]]:sr-only\",\n // 2026-05-22 Approach 9 — 收合時所有 prefix center 鎖回 rail center\n // (--sidebar-width-icon/2 = GlobalHeader toggle geometry SSOT)。\n // 公式: pl = (rail - prefix-width) / 2,prefix-width 由 SidebarProvider 注入的\n // --item-icon-size / --item-avatar-size CSS var(JS const mirror)提供。\n // 任何 row size(sm/md/lg)× density(md/lg)× prefix type(icon/avatar)6 cell 全對齊。\n \"group-data-[collapsible=icon]:has-[[data-prefix-type=icon]]:!pl-[calc((var(--sidebar-width-icon)-var(--item-icon-size))/2)]\",\n \"group-data-[collapsible=icon]:has-[[data-prefix-type=avatar]]:!pl-[calc((var(--sidebar-width-icon)-var(--item-avatar-size))/2)]\",\n ],\n {\n variants: {\n // size:消費 ROW_PADDING_BY_SIZE SSOT(item-anatomy.tsx)+ Sidebar-specific collapsed height\n // 前為 3 cva(menu / sidebar / tree)重複同一 py 公式,drift risk 已知。\n // 改用 shared SSOT 後 formula 改一處全同步。\n size: {\n // 2026-05-21 v4 C*:撤回 `group-data-[collapsible=icon]:!h-[...]` height lock\n // (per codex Layer C — row height 永遠 stable,collapsed 只 outer clip 視覺)\n sm: ROW_PADDING_BY_SIZE.sm,\n md: ROW_PADDING_BY_SIZE.md,\n lg: ROW_PADDING_BY_SIZE.lg,\n },\n variant: {\n /** 預設 — 導覽 item,參與 single-selection */\n default: \"\",\n /**\n * Meta 命令 row(例:「查看更多」「載入更多」「Show all」「+ 新增專案」)。\n *\n * 語意上**不是導覽目的地**,是 section 底部的命令。規格:\n * - 文字從 `text-fg-secondary` 退到 `text-fg-muted`(視覺重量下沉,hover 才升到 foreground)\n * - **不該參與 single-selection**:不傳 `id`,TS 階段提醒;執行階段傳了也不會 active\n * - `font-medium` 降為 `font-normal`(更輕,signal「這不是 primary 導覽」)\n * - 世界級對照:Linear \"Show N more\"、Notion \"Show N more\"、Slack \"Show more\"、\n * Gmail Labels \"More\"\n */\n meta: [\n \"font-normal text-fg-muted\",\n // meta variant 不該有 active 態;即使誤傳 isActive 也不啟動\n \"data-[active=true]:bg-transparent data-[active=true]:text-fg-muted\",\n ],\n },\n },\n defaultVariants: { size: \"md\", variant: \"default\" },\n }\n)\n\n// code-quality-allow: long-function — foundational composite main body — 拆 sub-fn 會複雜化 local state / ref / context binding\nconst SidebarMenuButton = React.forwardRef<\n HTMLButtonElement,\n Omit<React.ComponentProps<\"button\">, \"id\"> & {\n asChild?: boolean\n /**\n * Item 的唯一識別(對齊 `SidebarProvider.activeId`)。**強烈建議傳**——\n * 傳了之後 `isActive` 自動從 context 算、`onClick` 自動 setActiveId,\n * 整個 sidebar 的 single-selection 自動成立,不會寫出啞 item。\n */\n id?: string\n /**\n * 手動覆寫 active 狀態(極少用)。預設從 `SidebarProvider.activeId === id` 自動算。\n * 兩者都傳時以 `isActive` 為準。\n */\n isActive?: boolean\n startIcon?: LucideIcon\n tooltip?: string | React.ComponentProps<typeof TooltipContent>\n /**\n * Suffix slot 的 inline actions(宣告式 API,對齊 patterns/element-anatomy/inline-action.spec.md)。\n * Host 自動用 `<ItemInlineAction>` 渲染,consumer 只宣告 intent。\n * Icon 模式下自動隱藏。\n */\n inlineActions?: InlineActionConfig[]\n /**\n * 右側 actions slot(ReactNode)— escape hatch 供 consumer 放自訂元素\n * (如 DropdownMenu trigger / 自訂 popover trigger / 多 tier 動作)。\n *\n * 跟 `inlineActions` 互斥(同時傳 `inlineActionsSlot` 會優先,`inlineActions` 被忽略)。\n * 規則對齊 Input.endSlot canonical:90% case 用 `inlineActions` 宣告式 API,\n * 10% config 表達不出時走 slot。\n *\n * Padding budget:slot mode 預留 1 icon 寬度的 paddingRight(覆寫多 icon 寬度需 consumer 自控 className)。\n * Reveal / collapsed-hide / 絕對定位 chrome 跟 inlineActions 共用,consumer 不需重做。\n */\n inlineActionsSlot?: React.ReactNode\n /**\n * Inline actions 的顯示模式:\n * - `false`(預設):永遠顯示\n * - `\"hover\"`:滑過這一列、或鍵盤焦點在這一列時才出現,瞬間出現、不淡入(TreeView 模式;\n * 待辦總帳 L9,sidebar.spec.md「Inline actions」出現時機列)\n */\n actionsReveal?: false | \"hover\"\n } & VariantProps<typeof sidebarMenuButtonVariants>\n>(\n (\n {\n asChild = false,\n id,\n isActive: isActiveProp,\n size: sizeProp,\n variant = \"default\",\n startIcon: StartIcon,\n tooltip,\n inlineActions,\n inlineActionsSlot,\n actionsReveal = false,\n className,\n children,\n onClick,\n ...props\n },\n ref\n ) => {\n const Comp = asChild ? Slot : \"button\"\n // 沒傳 size 就從 SidebarProvider context 繼承(預設 md)\n const { isMobile, state, size: contextSize, activeId, setActiveId } = useSidebar()\n const size = sizeProp ?? contextSize\n\n // Meta variant 永不參與 single-selection,即使誤傳 id / isActive\n const isMeta = variant === \"meta\"\n\n // Active 狀態:明確傳的 isActive 優先;否則 `activeId === id` 自動算\n // Meta variant 永遠 false\n const isActive = isMeta ? false : (isActiveProp ?? (id !== undefined && activeId === id))\n\n // Click handler:id 存在且非 meta 才 setActiveId(single-selection 自動建立),\n // 同時呼叫 consumer 傳入的 onClick\n // code-quality-allow: long-function — helper fn 結構緊密,拆 sub-fn 會跨 fn 傳 state 反而複雜\n const handleClick = React.useCallback(\n (e: React.MouseEvent<HTMLButtonElement>) => {\n if (!isMeta && id !== undefined) setActiveId(id)\n onClick?.(e)\n },\n [isMeta, id, setActiveId, onClick]\n )\n\n // asChild 時,我們不能額外 wrap span / icon——Slot 要求單一 child。\n // 所以 asChild 的 consumer 自行放 icon + label(記得 prefix 要包在 h-[1lh] 容器,\n // label span 要有 data-sidebar=\"menu-label\" attribute 才能參與 icon 模式自動隱藏)。\n //\n // Label span 在 icon 模式下透過 cva base 的 `[&_[data-sidebar=menu-label]]:sr-only`\n // 視覺隱藏(2026-07-05 D4:sr-only 非 display:none,SR 保留 accessible name;\n // absolute 移出 flex flow),這樣 flex 只剩 icon 一個 in-flow child,置中公式不變。\n // Sidebar 寬度 transition 200ms 是唯一持續動畫,使用者視線跟著寬度走,不會察覺 label 的瞬切。\n // 用 `<ItemIcon>` / `<ItemLabel>` helper,**不直接寫 ItemPrefix wrap StartIcon**——\n // ItemIcon 內部會自動加 `data-prefix-type=\"icon\"`,讓 SidebarProvider 的全域\n // `:has()` prefix-mix 偵測能命中。直接用 ItemPrefix 就會錯過這個 tag,\n // 全域對齊功能對 SidebarMenuButton 路徑失效(曾經發生過的 bug)。\n const content = asChild ? (\n children\n ) : (\n <>\n {StartIcon && <ItemIcon icon={StartIcon} />}\n <ItemLabel>{children}</ItemLabel>\n </>\n )\n\n const hasSlot = !!inlineActionsSlot\n // Host disabled 時 inline actions(config + slot)整段不渲染,paddingRight 預留一併取消\n // (inline-action.spec.md「宿主 disabled 時不渲染」+ sidebar.spec.md「Host disabled 時|不渲染」)。\n // 必須 render 層 guard:suffix 跟 button 是 sibling(避免巢狀 button),\n // native disabled 的 pointer-events-none 蓋不到 sibling,CSS 層無從繼承。\n // aria-disabled 同步 guard——對齊 cva `aria-disabled:` 樣式支線(asChild 非 button host 自傳)。\n const isDisabled =\n !!props.disabled || props[\"aria-disabled\"] === true || props[\"aria-disabled\"] === \"true\"\n const hasActions = !isDisabled && (hasSlot || (!!inlineActions && inlineActions.length > 0))\n\n // 計算 suffix 所佔寬度:N×icon + (N-1)×gap-2(8px),再加 gap-2 跟 label 之間的間隔\n // Slot mode 預設按 1 icon 預留(consumer 寬度自控)\n const iconSz = ICON_SIZE[size ?? \"md\"]\n const n = hasSlot ? 1 : (inlineActions?.length ?? 0)\n const suffixContentWidth = n > 0 ? n * iconSz + (n - 1) * 8 : 0\n // Button 的 paddingRight = loose + suffix 寬度 + gap-2\n // 用 CSS calc 表達 loose token,不硬寫 px(loose 會隨 density 變)\n const buttonPaddingRight = hasActions\n ? `calc(var(--layout-space-loose) + ${suffixContentWidth}px + 0.5rem)`\n : undefined\n\n const button = (\n <Comp\n ref={ref}\n data-sidebar=\"menu-button\"\n data-active={isActive}\n // 2026-09-24:先前這裡**只有** data-active 這個純樣式 attribute,螢幕閱讀器讀不到\n // 「你現在人在這一頁」—— 當前項在視覺上有底色,對 SR 卻跟其他項完全一樣。\n // aria-current=\"page\" 是這個狀態的世界級標準寫法,三份一手來源一致:\n // W3C APG Disclosure Navigation 範例(連結上帶 aria-current=\"page\")、\n // GitHub repo 導覽(Code / Issues / PRs 那排)、\n // Primer TreeView(aria-current={isCurrentItem ? 'true' : undefined})。\n // 它與 aria-selected 是兩件事,WAI-ARIA 1.2 aria-current 的 Note 明文說兩者可以並存:\n // current = 你現在人在這一頁(已發生的事實);selected = 這個控件裡被挑中的那一項。\n // 側欄導覽要表達的是「你現在在這一頁」,不是「控件裡被挑中的那一項」,所以用 current 不用 selected。\n // 2026-09-25 起一串 SidebarMenu 只佔一個 Tab 停靠點、方向鍵在裡面走(待辦總帳 B9,見上方 SidebarMenu),\n // 但 ARIA 角色仍是 ul / li / button、沒有宣告 grid / tree 這類 composite 角色 → aria-selected 不適用,這條不變。\n // 純 SR 語意,**零視覺變化**。\n aria-current={isActive ? 'page' : undefined}\n className={cn(sidebarMenuButtonVariants({ size, variant }), className)}\n style={hasActions ? { paddingRight: buttonPaddingRight } : undefined}\n onClick={handleClick}\n {...props}\n >\n {content}\n </Comp>\n )\n\n // Suffix inline actions——絕對定位在 menu-item 右邊,\n // 跟 button 同層(不是 button 的 child,避免巢狀 button)。\n // 容器消費 `ItemSuffix`(item-anatomy.tsx):滑過才出現(`hoverReveal`)的規則只住那一份 ——\n // 2026-09-26 之前這裡另抄了一份同字串,出現方式要跟著 SSOT 改時會漏(M17);它也是「滑過造成的變化一律瞬間」\n //(待辦總帳 L9 / N4(3),motion.spec.md「hover 回饋不做過渡」)的唯一落點。group 用 `menu-item`(上方 SidebarMenuItem 的 group/menu-item)。\n // `:has(:focus-visible)` 而非 `:focus-within`:滑鼠點動作鈕不該讓它常駐,只有鍵盤走到時才顯示(ItemSuffix 同註解)。\n const suffixNode = hasActions ? (\n <ItemSuffix\n data-sidebar=\"menu-inline-actions\"\n hoverReveal={actionsReveal === \"hover\"}\n hoverGroup=\"menu-item\"\n className={cn(\n \"absolute top-1/2 -translate-y-1/2\",\n \"right-[var(--layout-space-loose)]\",\n // Icon 模式隱藏(跟 SidebarMenuBadge / SidebarMenuAction 一致)\n \"group-data-[collapsible=icon]:hidden\",\n // 兩顆動作鈕之間的空隙歸列(2026-09-26,待辦總帳 N53 ①,user:「這些看起來幾乎都是bug」;修法 = R20「讓亮著的地方點得到」):\n // 空隙在滑過時讓列亮著(sidebar.spec.md「Inline actions」段「兩顆動作鈕之間的空隙也算在內」),\n // 點下去卻落在這個容器、列不導覽 —— 看得到亮卻點不到(hit-area-canonical.md「看到亮起來卻點不到」)。\n // 容器本身不接指標,只有裡面可以按的東西接;空隙、以及插槽裡不能按的裝飾都穿到底下的列鈕(列鈕 paddingRight 已讓出這一段)。\n // 指到按鈕上時容器仍是 :hover 的祖先,列的巢狀滑過(上方 cva 的 `~` 選擇器)照舊。\n \"pointer-events-none [&_:is(button,a[href],[role=button],[tabindex])]:pointer-events-auto\",\n )}\n >\n {hasSlot\n ? inlineActionsSlot\n : inlineActions!.map((action, i) => (\n <ItemInlineAction key={action.label + i} action={action} />\n ))}\n </ItemSuffix>\n ) : null\n\n // code-quality-allow: long-function — helper fn 結構緊密,拆 sub-fn 會跨 fn 傳 state 反而複雜\n const buttonWithTooltip = tooltip ? (\n (() => {\n const tooltipProps =\n typeof tooltip === \"string\" ? { children: tooltip } : tooltip\n return (\n <Tooltip>\n <TooltipTrigger asChild>{button}</TooltipTrigger>\n <TooltipContent\n side=\"right\"\n align=\"center\"\n hidden={state !== \"collapsed\" || isMobile}\n {...tooltipProps}\n />\n </Tooltip>\n )\n })()\n ) : (\n button\n )\n\n if (!suffixNode) return buttonWithTooltip\n return (\n <>\n {buttonWithTooltip}\n {suffixNode}\n </>\n )\n }\n)\nSidebarMenuButton.displayName = \"SidebarMenuButton\"\n\n// 同上(見 SidebarGroupAction 上方的遷移說明)。\nconst SidebarMenuAction = React.forwardRef<\n HTMLButtonElement,\n Omit<ItemInlineActionButtonProps, \"size\"> & { showOnHover?: boolean }\n>(({ className, showOnHover = false, ...props }, ref) => (\n <ItemInlineActionButton\n ref={ref}\n data-sidebar=\"menu-action\"\n className={cn(\n \"absolute right-[var(--layout-space-loose)] top-1/2 -translate-y-1/2\",\n \"group-data-[collapsible=icon]:hidden\",\n showOnHover &&\n \"group-focus-within/menu-item:opacity-100 group-hover/menu-item:opacity-100 data-[state=open]:opacity-100 peer-data-[active=true]/menu-button:text-foreground md:opacity-0\",\n // 當前項旁的動作鈕:靜止已是 foreground(上一行),滑過 / 按下**釘住 foreground**,不讓 ItemInlineActionButton 的\n // `hover:/active:text-fg-secondary` (0,2,0) 把它降回一階(2026-09-25 合成情境量測:淺 #252525→#545454、深 #FFFFFF→#E1E1E1;目前 DS 內無 showOnHover 消費者)。\n // 依據:inline-action.spec.md「Icon 色彩」的階梯 root invariant `fg-muted → fg-secondary → foreground`,滑過只准往前一階;\n // 這顆的靜止已站在最高一階 foreground,沒有下一階可升就留在原地,不得往回掉 ——\n // 同列鈕「選中 × 滑過釘住」的寫法(上方 cva 的 `data-[active=true]:hover:…`)。特異性 (0,3,0) > hover:/active: 的 (0,2,0)。\n showOnHover &&\n \"peer-data-[active=true]/menu-button:hover:text-foreground peer-data-[active=true]/menu-button:active:text-foreground\",\n className\n )}\n {...props}\n />\n))\nSidebarMenuAction.displayName = \"SidebarMenuAction\"\n\n// SidebarMenuBadge — 重用專案的 Badge 元件,絕對定位在 menu item 右側\n// Consumer 傳入 Badge 的所有 props(count / variant / max 等)\nconst SidebarMenuBadge = React.forwardRef<\n React.ElementRef<typeof Badge>,\n React.ComponentProps<typeof Badge>\n>(({ className, ...props }, ref) => (\n <Badge\n ref={ref}\n data-sidebar=\"menu-badge\"\n className={cn(\n \"pointer-events-none absolute right-[var(--layout-space-loose)] top-1/2 -translate-y-1/2\",\n \"group-data-[collapsible=icon]:hidden\",\n className\n )}\n {...props}\n />\n))\nSidebarMenuBadge.displayName = \"SidebarMenuBadge\"\n\n// SidebarMenuSkeleton — 對齊 SidebarMenuButton 的 item-layout 公式,支援 sm/md/lg\n// 沒傳 size 就從 context 繼承,確保 loading 狀態跟實際 item 同高不跳動\nconst sidebarMenuSkeletonVariants = cva(\n [\n \"flex items-start gap-2\",\n \"px-[var(--layout-space-loose)]\",\n ],\n {\n variants: {\n // 消費 ROW_PADDING_BY_SIZE SSOT + skeleton-icon size modifier(Sidebar-specific)\n // 注:skeleton 無文字,typography class 雖被帶入但無害,保持公式一處同步\n size: {\n sm: [ROW_PADDING_BY_SIZE.sm, \"[&>[data-sidebar=menu-skeleton-icon]]:size-4\"],\n md: [ROW_PADDING_BY_SIZE.md, \"[&>[data-sidebar=menu-skeleton-icon]]:size-4\"],\n lg: [ROW_PADDING_BY_SIZE.lg, \"[&>[data-sidebar=menu-skeleton-icon]]:size-5\"],\n },\n },\n defaultVariants: { size: \"md\" },\n }\n)\n\nconst SidebarMenuSkeleton = React.forwardRef<\n HTMLDivElement,\n React.ComponentProps<\"div\"> & {\n showIcon?: boolean\n } & VariantProps<typeof sidebarMenuSkeletonVariants>\n>(({ className, showIcon = false, size: sizeProp, ...props }, ref) => {\n const { size: contextSize } = useSidebar()\n const size = sizeProp ?? contextSize\n const width = React.useMemo(() => {\n return `${Math.floor(Math.random() * 40) + 50}%`\n }, [])\n\n return (\n <div\n ref={ref}\n data-sidebar=\"menu-skeleton\"\n className={cn(sidebarMenuSkeletonVariants({ size }), className)}\n {...props}\n >\n {showIcon && (\n <Skeleton\n className=\"rounded-md shrink-0\"\n data-sidebar=\"menu-skeleton-icon\"\n />\n )}\n <Skeleton\n className=\"h-4 max-w-[var(--skeleton-width)] flex-1\"\n data-sidebar=\"menu-skeleton-text\"\n style={\n {\n \"--skeleton-width\": width,\n } as React.CSSProperties\n }\n />\n </div>\n )\n})\nSidebarMenuSkeleton.displayName = \"SidebarMenuSkeleton\"\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 sidebarMeta = {\n component: 'Sidebar',\n family: 1, // Family 1(Menu item layout)消費者 — 對齊 sidebar.spec.md frontmatter family: 1(SidebarMenuButton rows 消費 item-anatomy;同 tree-view.tsx 慣例)\n variants: {\n default: { when: '標準導覽 row,參與 single-selection' },\n meta: { when: 'Section 底部命令 row(Show more / 新增),不參與 selection' },\n },\n sizes: {\n sm: { px: 28, when: '次導覽 / 設定頁 / 緊湊空間' },\n md: { px: 32, when: '預設 — 應用程式主導覽' },\n lg: { px: 36, when: '重要主導覽 / icon-prominent workspace switcher' },\n },\n // 'selected' = nav 當前項(data-[active=true] → bg-neutral-selected 灰底家族;2026-07-07 meta 詞彙統一補列);\n // 'active' 移除 — 全檔無 Tailwind 按壓 utility(內嵌 SidebarTrigger 的按壓屬 Button meta,同 Notice/Alert\n // 排除內嵌 dismiss Button 慣例;2026-07-07 對抗稽核抓 doc-claim ≠ code 補修)。\n states: ['default', 'hover', 'selected', 'focus-visible', 'disabled'],\n tokens: {\n bg: ['bg-neutral-hover', 'bg-neutral-selected', 'bg-surface', 'bg-transparent'],\n fg: ['text-fg-muted', 'text-fg-secondary', 'text-foreground'],\n ring: ['focus-ring-inset'],\n },\n defaultSize: 'md',\n} as const\n\nexport {\n Sidebar,\n SidebarContent,\n SidebarFooter,\n SidebarGroup,\n SidebarGroupAction,\n SidebarGroupContent,\n SidebarGroupLabel,\n SidebarHeader,\n SidebarInput,\n SidebarMenu,\n SidebarMenuAction,\n SidebarMenuBadge,\n SidebarMenuButton,\n SidebarMenuItem,\n SidebarMenuSkeleton,\n SidebarProvider,\n SidebarSeparator,\n SidebarTrigger,\n useSidebar,\n}\n"],"names":[],"mappings":"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"}
|
|
@@ -39,7 +39,7 @@ export declare const sliderMeta: {
|
|
|
39
39
|
};
|
|
40
40
|
readonly states: readonly ["default", "hover", "active", "focus-visible", "disabled"];
|
|
41
41
|
readonly tokens: {
|
|
42
|
-
readonly bg: readonly ["bg-
|
|
42
|
+
readonly bg: readonly ["bg-disabled", "bg-primary", "bg-secondary", "bg-on-emphasis", "bg-canvas"];
|
|
43
43
|
readonly fg: readonly [];
|
|
44
44
|
readonly ring: readonly [];
|
|
45
45
|
};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"slider.d.ts","sourceRoot":"","sources":["../../../src/components/Slider/slider.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAA;AAC9B,OAAO,KAAK,eAAe,MAAM,wBAAwB,CAAA;AACzD,OAAO,EAAO,KAAK,YAAY,EAAE,MAAM,0BAA0B,CAAA;AAIjE;;;;;;;;;;;GAWG;AAEH,QAAA,MAAM,kBAAkB;;8EA8BvB,CAAA;AAED,MAAM,WAAW,WAGf,SAAQ,IAAI,CAAC,KAAK,CAAC,wBAAwB,CAAC,OAAO,eAAe,CAAC,IAAI,CAAC,EAAE,UAAU,GAAG,aAAa,CAAC,EACnG,YAAY,CAAC,OAAO,kBAAkB,CAAC;CAAG;AAG9C,QAAA,MAAM,MAAM,
|
|
1
|
+
{"version":3,"file":"slider.d.ts","sourceRoot":"","sources":["../../../src/components/Slider/slider.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAA;AAC9B,OAAO,KAAK,eAAe,MAAM,wBAAwB,CAAA;AACzD,OAAO,EAAO,KAAK,YAAY,EAAE,MAAM,0BAA0B,CAAA;AAIjE;;;;;;;;;;;GAWG;AAEH,QAAA,MAAM,kBAAkB;;8EA8BvB,CAAA;AAED,MAAM,WAAW,WAGf,SAAQ,IAAI,CAAC,KAAK,CAAC,wBAAwB,CAAC,OAAO,eAAe,CAAC,IAAI,CAAC,EAAE,UAAU,GAAG,aAAa,CAAC,EACnG,YAAY,CAAC,OAAO,kBAAkB,CAAC;CAAG;AAG9C,QAAA,MAAM,MAAM,qFA0JV,CAAA;AAKF,eAAO,MAAM,UAAU;;;;;;;;;;;;;;;;;;;;;;;;CAkBb,CAAA;AAEV,OAAO,EAAE,MAAM,EAAE,kBAAkB,EAAE,CAAA"}
|
|
@@ -59,8 +59,10 @@ const Slider = React.forwardRef(({ className, size, value, defaultValue, "aria-l
|
|
|
59
59
|
/* @__PURE__ */ jsx(SliderPrimitive.Track, { className: cn(
|
|
60
60
|
"relative grow overflow-hidden rounded-full h-1",
|
|
61
61
|
// Rest:bg-secondary(n-3,「微淡可辨」的 subtle fill,跟 Tag neutral / Badge low 同級)
|
|
62
|
-
// Disabled:bg-
|
|
63
|
-
|
|
62
|
+
// Disabled:bg-disabled(n-2)。2026-09-25 由 bg-muted 改(待辦總帳 C2 / R12 W3):這層底只在元件停用時出現,
|
|
63
|
+
// 是元件的停用狀態色,依 color.spec.md「Static Subtle Background」段邊界句「component disabled bg 不走 muted」
|
|
64
|
+
// 走 --bg-disabled;--muted 是靜態非互動 surface 的 token。兩者同值 neutral-2,畫面零變化。
|
|
65
|
+
"bg-secondary data-[disabled]:bg-disabled"
|
|
64
66
|
), children: /* @__PURE__ */ jsx(
|
|
65
67
|
SliderPrimitive.Range,
|
|
66
68
|
{
|
|
@@ -78,7 +80,10 @@ const Slider = React.forwardRef(({ className, size, value, defaultValue, "aria-l
|
|
|
78
80
|
className: cn(
|
|
79
81
|
"block h-4 w-4 shrink-0 rounded-full cursor-grab",
|
|
80
82
|
"bg-on-emphasis border-2 border-primary",
|
|
81
|
-
|
|
83
|
+
// 只過渡陰影(hover / 按壓時的高度),不再過渡外框色:hover 外框一律瞬間(tokens/motion/motion.spec.md「hover 回饋不做過渡」;
|
|
84
|
+
// 2026-09-26 由底色延伸到外框,待辦總帳 L9 / N4(3));按壓的外框是同一組屬性,一併瞬間(同 Button 按壓,button.tsx 無過渡)。
|
|
85
|
+
// 原 transition-all 連外框與停用底色一起過渡。陰影升級不在 2026-09-26 同意範圍(motion.spec.md「不在本規則」),維持 150ms。
|
|
86
|
+
"transition-shadow duration-150 motion-reduce:duration-0",
|
|
82
87
|
// Hover:border 升 hover 階(light mode 淺一階 lift,= Button primary hover 變色邏輯)
|
|
83
88
|
// + elevation 陰影。2026-07-06 修語:原註解寫「加深」但 primary-hover 比 base 淺,
|
|
84
89
|
// 措辭與 token 相反 — token 不遷就錯註解,註解修正。
|
|
@@ -128,7 +133,7 @@ const sliderMeta = {
|
|
|
128
133
|
},
|
|
129
134
|
states: ["default", "hover", "active", "focus-visible", "disabled"],
|
|
130
135
|
tokens: {
|
|
131
|
-
bg: ["bg-
|
|
136
|
+
bg: ["bg-disabled", "bg-primary", "bg-secondary", "bg-on-emphasis", "bg-canvas"],
|
|
132
137
|
fg: [],
|
|
133
138
|
ring: []
|
|
134
139
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"slider.js","sources":["../../../src/components/Slider/slider.tsx"],"sourcesContent":["import * as React from 'react'\nimport * as SliderPrimitive from '@radix-ui/react-slider'\nimport { cva, type VariantProps } from 'class-variance-authority'\nimport { cn } from '@/lib/utils'\nimport { useFieldContext, useResolvedFieldDisabled, useResolvedFieldMode } from '@/design-system/components/Field/field-context'\n\n/**\n * Slider — 數值範圍選取器\n *\n * 基於 Radix Slider primitive,橋接設計系統 token。詳細設計原則見 `slider.spec.md`。\n *\n * ── 核心設計 ──\n * 1. **視覺單一**:track 厚度、thumb 直徑、thumb 邊框都是固定值,不隨 `size` 變\n * 2. **`size` 只控容器外高**:對齊 Field family 的 `h-field-*` tier,讓 Slider 能跟\n * Input / NumberInput / Select 在 Field 內並排對齊\n * 3. **Range mode 免費**:Radix 原生支援 `value: number[]`,傳多值自動多 thumb\n * 4. **Hover / active 用 elevation 陰影**:不用色變,避免暗示「這是 button」\n */\n\nconst sliderRootVariants = cva(\n // 容器外層:水平置中 + relative(Radix 會絕對定位內部元素)\n // flex items-center 讓 track+thumb 在任何 field-height 下都垂直置中\n //\n // ── Disabled 策略:灰階 token swap(對齊 Button / Checkbox)──\n // Slider 的藍色 range 是美學視覺,不是 semantic state——使用者從 disabled\n // slider 需要辨識的是 thumb 位置 + range 長度,這兩者不依賴顏色。失去藍色\n // 沒有資訊損失。\n //\n // 跟 Switch 的差別:Switch 的 on/off 是純顏色差異(沒有形狀差異),所以必須\n // 靠 opacity 保留色彩身分。Slider 的位置/長度是形狀差異,不需要保留顏色身分,\n // 跟 Checkbox(checkmark 形狀 = semantic 載體)同類,應該走灰階。\n //\n // 詳細判準見 `slider.spec.md` 的「Disabled 策略」章節。\n [\n 'relative flex items-center w-full min-w-0 touch-none select-none',\n 'data-[disabled]:cursor-not-allowed',\n ],\n {\n variants: {\n size: {\n sm: 'h-field-sm',\n md: 'h-field-md',\n lg: 'h-field-lg',\n },\n },\n defaultVariants: {\n size: 'md',\n },\n },\n)\n\nexport interface SliderProps\n // 'orientation' 一併 Omit(2026-07-07 deep-audit A.1b 殘項):spec「不支援 vertical」宣稱的\n // 型別面機械封鎖 — DS 樣式(track h-1 / w-full / items-center)假設 horizontal,透傳 vertical 壞版。\n extends Omit<React.ComponentPropsWithoutRef<typeof SliderPrimitive.Root>, 'children' | 'orientation'>,\n VariantProps<typeof sliderRootVariants> {}\n\n// code-quality-allow: long-function — foundational composite(thumb-count 推導 + Field disabled 整合 + multi-mode render)\nconst Slider = React.forwardRef<\n React.ElementRef<typeof SliderPrimitive.Root>,\n SliderProps\n>(({ className, size, value, defaultValue, 'aria-label': ariaLabel, ...props }, ref) => {\n // Field 家族整合:被 <Field mode=\"disabled\"> 包裹時自動 disabled(per slider.spec.md「Slider 作為 Field\n // 家族整合時繼承其 canonical」)。Slider 已有完整 data-[disabled] 視覺,故只需把 fieldCtx disabled 接上。\n // 2026-06-08 SSOT:讀 useResolvedFieldDisabled()(fieldCtx.disabled)→ <Field disabled> 與 <Field mode=\"disabled\"> 都生效\n // 2026-06-12 補:<Field mode=\"readonly\"> → 鎖互動保留視覺(readonly ≠ disabled,不降色)\n const fieldDisabled = useResolvedFieldDisabled()\n const fieldMode = useResolvedFieldMode({ mode: undefined, disabled: undefined, readOnly: undefined })\n const fieldReadonly = fieldMode === 'readonly'\n // 2026-06-10 a11y:Field 內 Slider thumb(role=slider)無 accessible name(deep-audit axe 抓 aria-input-field-name)\n // → 預設接 FieldLabel(aria-labelledby),consumer ariaLabel 優先。對齊 rating/time-picker labelId 接線。\n const fieldLabelId = useFieldContext()?.labelId\n // Degenerate-input 硬化(2026-07 deep-audit d7i20 / d8i7):value / defaultValue 傳空陣列\n // 時,Radix 無對應值 → thumb display:none,留下沒有可操作 thumb 的空軌道。正規化為 `[min]`\n // (min 未傳時 Radix 預設 0),保證至少一個坐落在 min 的可操作 thumb —— 對齊 spec API\n //「value / defaultValue 必為非空陣列」契約,且不影響任何有效輸入。\n const minValue = (props as { min?: number }).min ?? 0\n const safeValue =\n Array.isArray(value) && value.length === 0 ? [minValue] : value\n const safeDefaultValue =\n Array.isArray(defaultValue) && defaultValue.length === 0 ? [minValue] : defaultValue\n // 推導要渲染幾個 thumb:controlled 用 value,uncontrolled 用 defaultValue,\n // 都沒有時 fallback 單 thumb(Radix 預設行為)\n const thumbCount =\n (Array.isArray(safeValue) && safeValue.length) ||\n (Array.isArray(safeDefaultValue) && safeDefaultValue.length) ||\n 1\n\n return (\n <SliderPrimitive.Root\n ref={ref}\n value={safeValue}\n defaultValue={safeDefaultValue}\n className={cn(sliderRootVariants({ size }), fieldReadonly && 'pointer-events-none', className)}\n {...props}\n disabled={(props as { disabled?: boolean }).disabled || fieldDisabled}\n // <Field mode=\"readonly\"> cascade(2026-06-12 補):鎖互動、保留正常視覺(readonly\n // 不降色;值仍可讀)。pointer-events-none 擋滑鼠,thumb tabIndex=-1 擋鍵盤。\n data-readonly={fieldReadonly || undefined}\n >\n {/*\n Track — rest 用 bg-secondary(n-3,「微淡可辨」),disabled 用 bg-muted(n-2,退化)。\n 跟 Tag neutral / Badge low 同家族。\n */}\n <SliderPrimitive.Track className={cn(\n 'relative grow overflow-hidden rounded-full h-1',\n // Rest:bg-secondary(n-3,「微淡可辨」的 subtle fill,跟 Tag neutral / Badge low 同級)\n // Disabled:bg-muted(n-2,「disabled-like 退化」底色)\n 'bg-secondary data-[disabled]:bg-muted',\n )}>\n {/*\n Range — 填滿段。\n\n ── Range 色 = Thumb border 色(語意一致) ──\n Rest 兩者都是 `primary`,disabled 兩者都是 `border`(n-5)。為什麼要一致?\n Range 是「填充視覺」,Thumb border 是「thumb 的輪廓線」——視覺上 thumb\n 的 border 剛好是 range 的延續(thumb 坐落在 range 的端點上,border 跟\n range 在色彩上融為一體,看起來像「range 包住 thumb」而不是「thumb 浮在\n range 上」)。兩個 token 綁在一起 —— **同色綁定 scope = rest 與 disabled 兩態**;\n 互動態(hover/active/focus)只動 thumb border 色階,range 填色恆 primary\n (詳下方 Thumb 註解 2026-07-06 retract)。\n */}\n <SliderPrimitive.Range\n className={cn(\n 'absolute h-full bg-primary',\n 'data-[disabled]:bg-border',\n )}\n />\n </SliderPrimitive.Track>\n\n {/*\n Thumb — N 個(由 thumbCount 決定)。\n 白底 + 2px 邊框,邊框色 = Range 色(**一致綁定**):\n - Rest: `border-primary` ↔ Range `bg-primary`\n - Disabled: `border-border` ↔ Range `bg-border`\n 這個一致性讓 thumb border 跟 range 融為一體,看起來像「range 包住 thumb」\n 的連續視覺。**同色綁定 scope = rest 與 disabled 兩態**;互動態(hover/active/focus)\n 只動 thumb border 色階(+ elevation),range 填色恆定 —— 2026-07-06 修正原\n 「不論 state 永遠同色」過度宣稱(hover/active 一直都只動 thumb,claim 與 code 不符)。\n\n **Thumb bg 兩態(2026-06-12 user 拍板,深色模式破洞修正)**:\n - Rest/hover/active:`bg-on-emphasis`(固定白、深淺主題不反轉)— 對齊自家\n Switch thumb(switch.tsx bg-on-emphasis)+ Radix Themes(thumb 字面 white\n 無 dark override)+ Apple iOS(深色模式旋鈕仍白)。原 `bg-surface` 在深色\n = 8% 白半透明 → thumb 變深色破洞且 track 穿透。\n - Disabled:`bg-canvas`(不透明頁面背景色)— 沉回背景表達「不可動」,\n = Radix disabled thumb 用 gray-1(app background)同款;不透明故 track\n 不穿透。不可用 bg-muted(曾踩 thumb 跟 track 同色融合 bug;canvas 與\n track 的 muted 隔 n-5 邊框 + 不同值,不融)。\n */}\n {Array.from({ length: thumbCount }).map((_, i) => (\n <SliderPrimitive.Thumb\n key={i}\n // 2026-09-07 **WCAG 2.1.1(Level A)修**:原本非唯讀時傳 `undefined`,本意是\n // 「不管、讓 Radix 用它的預設 0」——但 Radix 是 `tabIndex: disabled ? void 0 : 0`\n // **之後**才 spread 我們的 props(`react-slider/dist/index.mjs:440-441` 實查),\n // 所以 `undefined` 是把它**覆蓋掉**,React 於是不輸出 tabindex 屬性。\n // 實測:全 DS 每一個 slider thumb 的 `tabIndex` 都是 -1,連按 15 次 Tab 焦點\n // 始終停在 body —— **滑桿完全不能用鍵盤操作**。\n // 教訓同 M2:消費第三方元件要驗真實 DOM,不能假設「傳 undefined = 不干預」。\n tabIndex={fieldReadonly ? -1 : 0}\n aria-readonly={fieldReadonly || undefined}\n className={cn(\n 'block h-4 w-4 shrink-0 rounded-full cursor-grab',\n 'bg-on-emphasis border-2 border-primary',\n 'transition-all duration-150 motion-reduce:duration-0',\n // Hover:border 升 hover 階(light mode 淺一階 lift,= Button primary hover 變色邏輯)\n // + elevation 陰影。2026-07-06 修語:原註解寫「加深」但 primary-hover 比 base 淺,\n // 措辭與 token 相反 — token 不遷就錯註解,註解修正。\n 'hover:border-primary-hover hover:[box-shadow:var(--elevation-100)]',\n // Active(按壓拖曳):深一階 primary-active(= Button active 邏輯 button.tsx active:*-active\n // 對照組;原誤用 hover 階 = 全 DS 唯一 active-用-hover 偏移,2026-07-06 雙向全掃修正)\n 'active:cursor-grabbing active:border-primary-active active:[box-shadow:var(--elevation-200)]',\n // Focus(2026-09-07 C6 修):原本是「跟 hover 同視覺,不加 ring 或 halo」——\n // 但把手**平常就是藍邊**(上面 `border-2 border-primary`),聚焦只換成 primary-hover,\n // 實測兩色的對比只有 **1.46:1(淺)/ 1.33:1(深)**:等於看不出來,\n // 而且與 hover 完全同色 —— 鍵盤使用者分不出「我在這裡」與「滑鼠經過」。\n // WCAG 2.4.7 要的是**看得見**的焦點指示;把手是可操作元件,依 focus-canonical\n // 問題一「可操作 → 必須畫」,而且它不屬「明確不用畫框」四類的任何一類。\n // 改法是**拿掉 outline-none**,讓全域外描邊畫上去(元件不需要自己寫任何東西)。\n //\n // 2026-09-07 再修:連 `focus-visible:border-primary-hover` 一起刪。\n // 留著的話同一顆把手上會有**兩個**焦點指示(外描邊 + 邊框變色),\n // 違反 focus-canonical「一個項目只有一個指示器」;而且那個變色與 hover 同色,\n // 反而讓「鍵盤在這裡」與「滑鼠經過」看起來一樣。\n // 上面的 `hover:border-primary-hover` 保留 —— 它表達的是 hover,那是另一件事。\n // Disabled:border 跟 Range 一起退成 border(n-5),bg 沉回 canvas(不透明背景色)\n 'data-[disabled]:cursor-not-allowed data-[disabled]:border-border data-[disabled]:bg-canvas',\n 'data-[disabled]:hover:[box-shadow:none]',\n )}\n // aria-label 策略(對齊 WAI-ARIA APG multi-thumb slider + Radix 原生語意標籤):\n // - consumer 傳 ariaLabel:單 thumb → 原樣;多 thumb → `{label} (N)` 區分\n // - 沒傳 ariaLabel:回傳 undefined,讓 Radix getLabel 提供語意標籤\n // (2 thumb → Minimum/Maximum;>2 thumb → Value N of M;單 thumb → 無)\n // 比硬寫 `Thumb N` 更語意化,且讓「繼承 Radix a11y 預設」名實相符\n aria-label={\n ariaLabel\n ? thumbCount > 1\n ? `${ariaLabel} (${i + 1})`\n : ariaLabel\n : undefined\n }\n aria-labelledby={!ariaLabel && fieldLabelId ? fieldLabelId : undefined}\n />\n ))}\n </SliderPrimitive.Root>\n )\n})\nSlider.displayName = 'Slider'\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 sliderMeta = {\n component: 'Slider',\n family: null, // self-contained(對齊 spec;同 rating.tsx 慣例),\n variants: {},\n // size 只控容器外高,對齊 Field family h-field-*(md density:28/32/36px,uiSize.css)。\n // track 厚度 / thumb 直徑固定不隨 size 變,故無 iconSize / typography。\n // fieldHeight key 對齊 compile-stories.mjs 消費 schema(同 Button/Checkbox/Switch)。\n sizes: {\n sm: { fieldHeight: 28, when: 'Toolbar / inline 編輯' },\n md: { fieldHeight: 32, when: '預設 — Form / cell inline edit' },\n lg: { fieldHeight: 36, when: 'Marketing / 高 touch 區' },\n },\n states: ['default', 'hover', 'active', 'focus-visible', 'disabled'],\n tokens: {\n bg: ['bg-muted', 'bg-primary', 'bg-secondary', 'bg-on-emphasis', 'bg-canvas'],\n fg: [],\n ring: [],\n },\n} as const\n\nexport { Slider, sliderRootVariants }\n"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"slider.js","sources":["../../../src/components/Slider/slider.tsx"],"sourcesContent":["import * as React from 'react'\nimport * as SliderPrimitive from '@radix-ui/react-slider'\nimport { cva, type VariantProps } from 'class-variance-authority'\nimport { cn } from '@/lib/utils'\nimport { useFieldContext, useResolvedFieldDisabled, useResolvedFieldMode } from '@/design-system/components/Field/field-context'\n\n/**\n * Slider — 數值範圍選取器\n *\n * 基於 Radix Slider primitive,橋接設計系統 token。詳細設計原則見 `slider.spec.md`。\n *\n * ── 核心設計 ──\n * 1. **視覺單一**:track 厚度、thumb 直徑、thumb 邊框都是固定值,不隨 `size` 變\n * 2. **`size` 只控容器外高**:對齊 Field family 的 `h-field-*` tier,讓 Slider 能跟\n * Input / NumberInput / Select 在 Field 內並排對齊\n * 3. **Range mode 免費**:Radix 原生支援 `value: number[]`,傳多值自動多 thumb\n * 4. **Hover / active 用 elevation 陰影**:不用色變,避免暗示「這是 button」\n */\n\nconst sliderRootVariants = cva(\n // 容器外層:水平置中 + relative(Radix 會絕對定位內部元素)\n // flex items-center 讓 track+thumb 在任何 field-height 下都垂直置中\n //\n // ── Disabled 策略:灰階 token swap(對齊 Button / Checkbox)──\n // Slider 的藍色 range 是美學視覺,不是 semantic state——使用者從 disabled\n // slider 需要辨識的是 thumb 位置 + range 長度,這兩者不依賴顏色。失去藍色\n // 沒有資訊損失。\n //\n // 跟 Switch 的差別:Switch 的 on/off 是純顏色差異(沒有形狀差異),所以必須\n // 靠 opacity 保留色彩身分。Slider 的位置/長度是形狀差異,不需要保留顏色身分,\n // 跟 Checkbox(checkmark 形狀 = semantic 載體)同類,應該走灰階。\n //\n // 詳細判準見 `slider.spec.md` 的「Disabled 策略」章節。\n [\n 'relative flex items-center w-full min-w-0 touch-none select-none',\n 'data-[disabled]:cursor-not-allowed',\n ],\n {\n variants: {\n size: {\n sm: 'h-field-sm',\n md: 'h-field-md',\n lg: 'h-field-lg',\n },\n },\n defaultVariants: {\n size: 'md',\n },\n },\n)\n\nexport interface SliderProps\n // 'orientation' 一併 Omit(2026-07-07 deep-audit A.1b 殘項):spec「不支援 vertical」宣稱的\n // 型別面機械封鎖 — DS 樣式(track h-1 / w-full / items-center)假設 horizontal,透傳 vertical 壞版。\n extends Omit<React.ComponentPropsWithoutRef<typeof SliderPrimitive.Root>, 'children' | 'orientation'>,\n VariantProps<typeof sliderRootVariants> {}\n\n// code-quality-allow: long-function — foundational composite(thumb-count 推導 + Field disabled 整合 + multi-mode render)\nconst Slider = React.forwardRef<\n React.ElementRef<typeof SliderPrimitive.Root>,\n SliderProps\n>(({ className, size, value, defaultValue, 'aria-label': ariaLabel, ...props }, ref) => {\n // Field 家族整合:被 <Field mode=\"disabled\"> 包裹時自動 disabled(per slider.spec.md「Slider 作為 Field\n // 家族整合時繼承其 canonical」)。Slider 已有完整 data-[disabled] 視覺,故只需把 fieldCtx disabled 接上。\n // 2026-06-08 SSOT:讀 useResolvedFieldDisabled()(fieldCtx.disabled)→ <Field disabled> 與 <Field mode=\"disabled\"> 都生效\n // 2026-06-12 補:<Field mode=\"readonly\"> → 鎖互動保留視覺(readonly ≠ disabled,不降色)\n const fieldDisabled = useResolvedFieldDisabled()\n const fieldMode = useResolvedFieldMode({ mode: undefined, disabled: undefined, readOnly: undefined })\n const fieldReadonly = fieldMode === 'readonly'\n // 2026-06-10 a11y:Field 內 Slider thumb(role=slider)無 accessible name(deep-audit axe 抓 aria-input-field-name)\n // → 預設接 FieldLabel(aria-labelledby),consumer ariaLabel 優先。對齊 rating/time-picker labelId 接線。\n const fieldLabelId = useFieldContext()?.labelId\n // Degenerate-input 硬化(2026-07 deep-audit d7i20 / d8i7):value / defaultValue 傳空陣列\n // 時,Radix 無對應值 → thumb display:none,留下沒有可操作 thumb 的空軌道。正規化為 `[min]`\n // (min 未傳時 Radix 預設 0),保證至少一個坐落在 min 的可操作 thumb —— 對齊 spec API\n //「value / defaultValue 必為非空陣列」契約,且不影響任何有效輸入。\n const minValue = (props as { min?: number }).min ?? 0\n const safeValue =\n Array.isArray(value) && value.length === 0 ? [minValue] : value\n const safeDefaultValue =\n Array.isArray(defaultValue) && defaultValue.length === 0 ? [minValue] : defaultValue\n // 推導要渲染幾個 thumb:controlled 用 value,uncontrolled 用 defaultValue,\n // 都沒有時 fallback 單 thumb(Radix 預設行為)\n const thumbCount =\n (Array.isArray(safeValue) && safeValue.length) ||\n (Array.isArray(safeDefaultValue) && safeDefaultValue.length) ||\n 1\n\n return (\n <SliderPrimitive.Root\n ref={ref}\n value={safeValue}\n defaultValue={safeDefaultValue}\n className={cn(sliderRootVariants({ size }), fieldReadonly && 'pointer-events-none', className)}\n {...props}\n disabled={(props as { disabled?: boolean }).disabled || fieldDisabled}\n // <Field mode=\"readonly\"> cascade(2026-06-12 補):鎖互動、保留正常視覺(readonly\n // 不降色;值仍可讀)。pointer-events-none 擋滑鼠,thumb tabIndex=-1 擋鍵盤。\n data-readonly={fieldReadonly || undefined}\n >\n {/*\n Track — rest 用 bg-secondary(n-3,「微淡可辨」),disabled 用 bg-disabled(n-2,元件停用狀態色)。\n rest 跟 Tag neutral / Badge low 同家族。\n */}\n <SliderPrimitive.Track className={cn(\n 'relative grow overflow-hidden rounded-full h-1',\n // Rest:bg-secondary(n-3,「微淡可辨」的 subtle fill,跟 Tag neutral / Badge low 同級)\n // Disabled:bg-disabled(n-2)。2026-09-25 由 bg-muted 改(待辦總帳 C2 / R12 W3):這層底只在元件停用時出現,\n // 是元件的停用狀態色,依 color.spec.md「Static Subtle Background」段邊界句「component disabled bg 不走 muted」\n // 走 --bg-disabled;--muted 是靜態非互動 surface 的 token。兩者同值 neutral-2,畫面零變化。\n 'bg-secondary data-[disabled]:bg-disabled',\n )}>\n {/*\n Range — 填滿段。\n\n ── Range 色 = Thumb border 色(語意一致) ──\n Rest 兩者都是 `primary`,disabled 兩者都是 `border`(n-5)。為什麼要一致?\n Range 是「填充視覺」,Thumb border 是「thumb 的輪廓線」——視覺上 thumb\n 的 border 剛好是 range 的延續(thumb 坐落在 range 的端點上,border 跟\n range 在色彩上融為一體,看起來像「range 包住 thumb」而不是「thumb 浮在\n range 上」)。兩個 token 綁在一起 —— **同色綁定 scope = rest 與 disabled 兩態**;\n 互動態(hover/active/focus)只動 thumb border 色階,range 填色恆 primary\n (詳下方 Thumb 註解 2026-07-06 retract)。\n */}\n <SliderPrimitive.Range\n className={cn(\n 'absolute h-full bg-primary',\n 'data-[disabled]:bg-border',\n )}\n />\n </SliderPrimitive.Track>\n\n {/*\n Thumb — N 個(由 thumbCount 決定)。\n 白底 + 2px 邊框,邊框色 = Range 色(**一致綁定**):\n - Rest: `border-primary` ↔ Range `bg-primary`\n - Disabled: `border-border` ↔ Range `bg-border`\n 這個一致性讓 thumb border 跟 range 融為一體,看起來像「range 包住 thumb」\n 的連續視覺。**同色綁定 scope = rest 與 disabled 兩態**;互動態(hover/active/focus)\n 只動 thumb border 色階(+ elevation),range 填色恆定 —— 2026-07-06 修正原\n 「不論 state 永遠同色」過度宣稱(hover/active 一直都只動 thumb,claim 與 code 不符)。\n\n **Thumb bg 兩態(2026-06-12 user 拍板,深色模式破洞修正)**:\n - Rest/hover/active:`bg-on-emphasis`(固定白、深淺主題不反轉)— 對齊自家\n Switch thumb(switch.tsx bg-on-emphasis)+ Radix Themes(thumb 字面 white\n 無 dark override)+ Apple iOS(深色模式旋鈕仍白)。原 `bg-surface` 在深色\n = 8% 白半透明 → thumb 變深色破洞且 track 穿透。\n - Disabled:`bg-canvas`(不透明頁面背景色)— 沉回背景表達「不可動」,\n = Radix disabled thumb 用 gray-1(app background)同款;不透明故 track\n 不穿透。不可用 bg-muted / bg-disabled(兩者與 track 停用底同值 n-2,曾踩 thumb 跟 track\n 同色融合 bug;canvas 與 track 的停用底隔 n-5 邊框 + 不同值,不融)。\n */}\n {Array.from({ length: thumbCount }).map((_, i) => (\n <SliderPrimitive.Thumb\n key={i}\n // 2026-09-07 **WCAG 2.1.1(Level A)修**:原本非唯讀時傳 `undefined`,本意是\n // 「不管、讓 Radix 用它的預設 0」——但 Radix 是 `tabIndex: disabled ? void 0 : 0`\n // **之後**才 spread 我們的 props(`react-slider/dist/index.mjs:440-441` 實查),\n // 所以 `undefined` 是把它**覆蓋掉**,React 於是不輸出 tabindex 屬性。\n // 實測:全 DS 每一個 slider thumb 的 `tabIndex` 都是 -1,連按 15 次 Tab 焦點\n // 始終停在 body —— **滑桿完全不能用鍵盤操作**。\n // 教訓同 M2:消費第三方元件要驗真實 DOM,不能假設「傳 undefined = 不干預」。\n tabIndex={fieldReadonly ? -1 : 0}\n aria-readonly={fieldReadonly || undefined}\n className={cn(\n 'block h-4 w-4 shrink-0 rounded-full cursor-grab',\n 'bg-on-emphasis border-2 border-primary',\n // 只過渡陰影(hover / 按壓時的高度),不再過渡外框色:hover 外框一律瞬間(tokens/motion/motion.spec.md「hover 回饋不做過渡」;\n // 2026-09-26 由底色延伸到外框,待辦總帳 L9 / N4(3));按壓的外框是同一組屬性,一併瞬間(同 Button 按壓,button.tsx 無過渡)。\n // 原 transition-all 連外框與停用底色一起過渡。陰影升級不在 2026-09-26 同意範圍(motion.spec.md「不在本規則」),維持 150ms。\n 'transition-shadow duration-150 motion-reduce:duration-0',\n // Hover:border 升 hover 階(light mode 淺一階 lift,= Button primary hover 變色邏輯)\n // + elevation 陰影。2026-07-06 修語:原註解寫「加深」但 primary-hover 比 base 淺,\n // 措辭與 token 相反 — token 不遷就錯註解,註解修正。\n 'hover:border-primary-hover hover:[box-shadow:var(--elevation-100)]',\n // Active(按壓拖曳):深一階 primary-active(= Button active 邏輯 button.tsx active:*-active\n // 對照組;原誤用 hover 階 = 全 DS 唯一 active-用-hover 偏移,2026-07-06 雙向全掃修正)\n 'active:cursor-grabbing active:border-primary-active active:[box-shadow:var(--elevation-200)]',\n // Focus(2026-09-07 C6 修):原本是「跟 hover 同視覺,不加 ring 或 halo」——\n // 但把手**平常就是藍邊**(上面 `border-2 border-primary`),聚焦只換成 primary-hover,\n // 實測兩色的對比只有 **1.46:1(淺)/ 1.33:1(深)**:等於看不出來,\n // 而且與 hover 完全同色 —— 鍵盤使用者分不出「我在這裡」與「滑鼠經過」。\n // WCAG 2.4.7 要的是**看得見**的焦點指示;把手是可操作元件,依 focus-canonical\n // 問題一「可操作 → 必須畫」,而且它不屬「明確不用畫框」四類的任何一類。\n // 改法是**拿掉 outline-none**,讓全域外描邊畫上去(元件不需要自己寫任何東西)。\n //\n // 2026-09-07 再修:連 `focus-visible:border-primary-hover` 一起刪。\n // 留著的話同一顆把手上會有**兩個**焦點指示(外描邊 + 邊框變色),\n // 違反 focus-canonical「一個項目只有一個指示器」;而且那個變色與 hover 同色,\n // 反而讓「鍵盤在這裡」與「滑鼠經過」看起來一樣。\n // 上面的 `hover:border-primary-hover` 保留 —— 它表達的是 hover,那是另一件事。\n // Disabled:border 跟 Range 一起退成 border(n-5),bg 沉回 canvas(不透明背景色)\n 'data-[disabled]:cursor-not-allowed data-[disabled]:border-border data-[disabled]:bg-canvas',\n 'data-[disabled]:hover:[box-shadow:none]',\n )}\n // aria-label 策略(對齊 WAI-ARIA APG multi-thumb slider + Radix 原生語意標籤):\n // - consumer 傳 ariaLabel:單 thumb → 原樣;多 thumb → `{label} (N)` 區分\n // - 沒傳 ariaLabel:回傳 undefined,讓 Radix getLabel 提供語意標籤\n // (2 thumb → Minimum/Maximum;>2 thumb → Value N of M;單 thumb → 無)\n // 比硬寫 `Thumb N` 更語意化,且讓「繼承 Radix a11y 預設」名實相符\n aria-label={\n ariaLabel\n ? thumbCount > 1\n ? `${ariaLabel} (${i + 1})`\n : ariaLabel\n : undefined\n }\n aria-labelledby={!ariaLabel && fieldLabelId ? fieldLabelId : undefined}\n />\n ))}\n </SliderPrimitive.Root>\n )\n})\nSlider.displayName = 'Slider'\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 sliderMeta = {\n component: 'Slider',\n family: null, // self-contained(對齊 spec;同 rating.tsx 慣例),\n variants: {},\n // size 只控容器外高,對齊 Field family h-field-*(md density:28/32/36px,uiSize.css)。\n // track 厚度 / thumb 直徑固定不隨 size 變,故無 iconSize / typography。\n // fieldHeight key 對齊 compile-stories.mjs 消費 schema(同 Button/Checkbox/Switch)。\n sizes: {\n sm: { fieldHeight: 28, when: 'Toolbar / inline 編輯' },\n md: { fieldHeight: 32, when: '預設 — Form / cell inline edit' },\n lg: { fieldHeight: 36, when: 'Marketing / 高 touch 區' },\n },\n states: ['default', 'hover', 'active', 'focus-visible', 'disabled'],\n tokens: {\n bg: ['bg-disabled', 'bg-primary', 'bg-secondary', 'bg-on-emphasis', 'bg-canvas'],\n fg: [],\n ring: [],\n },\n} as const\n\nexport { Slider, sliderRootVariants }\n"],"names":[],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"steps.d.ts","sourceRoot":"","sources":["../../../src/components/Steps/steps.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAA;AAE9B,OAAO,EAAO,KAAK,YAAY,EAAE,MAAM,0BAA0B,CAAA;
|
|
1
|
+
{"version":3,"file":"steps.d.ts","sourceRoot":"","sources":["../../../src/components/Steps/steps.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAA;AAE9B,OAAO,EAAO,KAAK,YAAY,EAAE,MAAM,0BAA0B,CAAA;AAOjE,MAAM,MAAM,SAAS,GAAG,IAAI,GAAG,IAAI,GAAG,IAAI,CAAA;AAE1C,MAAM,MAAM,gBAAgB,GAAG,UAAU,GAAG,YAAY,CAAA;AACxD,MAAM,MAAM,cAAc,GAAG,eAAe,GAAG,UAAU,CAAA;AACzD,MAAM,MAAM,gBAAgB,GAAG,UAAU,GAAG,WAAW,GAAG,SAAS,GAAG,WAAW,GAAG,OAAO,CAAA;AAmM3F,QAAA,MAAM,iBAAiB;;8EAQrB,CAAA;AAEF,MAAM,WAAW,UACf,SAAQ,IAAI,CAAC,KAAK,CAAC,cAAc,CAAC,gBAAgB,CAAC,EAAE,UAAU,GAAG,cAAc,CAAC,EAC/E,YAAY,CAAC,OAAO,iBAAiB,CAAC;IACxC,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,aAAa,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAA;IACvC,eAAe,CAAC,EAAE,MAAM,EAAE,CAAA;IAC1B,WAAW,CAAC,EAAE,MAAM,EAAE,CAAA;IACtB,MAAM,CAAC,EAAE,OAAO,CAAA;IAChB,IAAI,CAAC,EAAE,SAAS,CAAA;IAChB,WAAW,CAAC,EAAE,gBAAgB,CAAA;IAC9B,SAAS,CAAC,EAAE,cAAc,CAAA;IAC1B,eAAe,CAAC,EAAE,KAAK,GAAG,MAAM,GAAG,MAAM,EAAE,CAAA;IAC3C;yFACqF;IACrF,QAAQ,CAAC,EAAE,MAAM,EAAE,CAAA;IACnB,mDAAmD;IACnD,gBAAgB,CAAC,EAAE,CAAC,QAAQ,EAAE,MAAM,EAAE,KAAK,IAAI,CAAA;CAChD;AAGD,QAAA,MAAM,KAAK,qFA4IV,CAAA;AAYD,MAAM,WAAW,aACf,SAAQ,IAAI,CAAC,KAAK,CAAC,cAAc,CAAC,aAAa,CAAC,EAAE,OAAO,CAAC;IAC1D,KAAK,EAAE,MAAM,CAAA;IACb,KAAK,CAAC,EAAE,OAAO,CAAA;IACf,QAAQ,CAAC,EAAE,OAAO,CAAA;CACnB;AAGD,QAAA,MAAM,gBAAgB;;;8EAgBpB,CAAA;AAGF,QAAA,MAAM,QAAQ,qFAyEb,CAAA;AA0ZD,MAAM,WAAW,cAAe,SAAQ,KAAK,CAAC,cAAc,CAAC,eAAe,CAAC;CAAG;AAGhF,QAAA,MAAM,SAAS,wFA4Bd,CAAA;AAKD,MAAM,WAAW,oBAAqB,SAAQ,KAAK,CAAC,cAAc,CAAC,eAAe,CAAC;CAAG;AAEtF,QAAA,MAAM,eAAe,8FA2BpB,CAAA;AAKD,MAAM,WAAW,gBAAiB,SAAQ,KAAK,CAAC,cAAc,CAAC,cAAc,CAAC;CAAG;AAEjF,QAAA,MAAM,WAAW,yFAchB,CAAA;AAOD,eAAO,MAAM,SAAS;;;;;;;;;;;;;;;;;;;;;;;;CAeZ,CAAA;AAEV,OAAO,EAAE,KAAK,EAAE,QAAQ,EAAE,SAAS,EAAE,eAAe,EAAE,WAAW,EAAE,iBAAiB,EAAE,gBAAgB,EAAE,CAAA"}
|