@qijenchen/design-system 0.1.0-beta.145 → 0.1.0-beta.146
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 +4 -2
- package/dist/components/Accordion/accordion.js.map +1 -1
- package/dist/components/AgentPanel/agent-panel-fab.d.ts.map +1 -1
- package/dist/components/AgentPanel/agent-panel-fab.js +23 -5
- package/dist/components/AgentPanel/agent-panel-fab.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 +22 -21
- 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 +1 -1
- package/dist/components/Breadcrumb/breadcrumb.js +2 -2
- package/dist/components/Breadcrumb/breadcrumb.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 +63 -14
- package/dist/components/Button/button.js.map +1 -1
- package/dist/components/Calendar/calendar.d.ts +26 -7
- package/dist/components/Calendar/calendar.d.ts.map +1 -1
- package/dist/components/Calendar/calendar.js +195 -35
- 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 +1 -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 +27 -7
- 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 +97 -3
- 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 +1 -1
- package/dist/components/DataTable/cell-registry.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.d.ts +8 -5
- package/dist/components/DataTable/data-table.d.ts.map +1 -1
- package/dist/components/DataTable/data-table.js +94 -32
- 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.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.map +1 -1
- package/dist/components/Field/field-context.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 +1 -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 +4 -2
- 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 +19 -4
- package/dist/components/LinkInput/link-input.js.map +1 -1
- package/dist/components/NumberInput/number-input.d.ts.map +1 -1
- package/dist/components/NumberInput/number-input.js +5 -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.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/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 +92 -78
- 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.js +2 -1
- 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/Sidebar/sidebar.d.ts +7 -9
- package/dist/components/Sidebar/sidebar.d.ts.map +1 -1
- package/dist/components/Sidebar/sidebar.js +165 -49
- 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 +41 -23
- 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.js +1 -1
- package/dist/components/Tag/tag.js.map +1 -1
- package/dist/components/Textarea/textarea.d.ts.map +1 -1
- package/dist/components/Textarea/textarea.js +4 -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 +5 -1
- 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 +216 -208
- 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/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +20 -2
- package/dist/index.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/roving-list-keyboard.d.ts +193 -0
- package/dist/lib/roving-list-keyboard.d.ts.map +1 -0
- package/dist/lib/roving-list-keyboard.js +168 -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/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/hooks.json +1 -1
- package/ds-canonical/fork/consumer/lock.json +11 -8
- package/ds-canonical/fork/consumer/managed-files/423729840acdd43c812f17c245b871716c6b60f76e4d3e94864d0e34a815bf8d.blob +9 -0
- package/ds-canonical/fork/consumer/managed-files/5a0d134a2645114c8caac20458070b00a7f0fd55b3e8805b7325cb4cf52fa15e.blob +110 -0
- package/ds-canonical/fork/consumer/managed-files/5af0a46760af338d51fd0f7950675f8e97f7d2f09386b8818fcd7a58e6654e75.blob +720 -0
- package/ds-canonical/fork/consumer/managed-files/68bfcddff9c4a16388c248cd34fb66163e9e64b3d809a48ef3984f6ae4ff6d12.blob +211 -0
- package/ds-canonical/fork/consumer/managed-files/92e4828cdcb96ff0d01daa16f6885f24045a8e72820665650f2a1890dc3a446c.blob +42 -2
- package/ds-canonical/fork/consumer/managed-files/e71c65f45c6af7ce12b6b3c0d8028f185da5b719e2050c9b2a6b243d41ae4878.blob +62 -14
- package/ds-canonical/fork/governance.lock +34 -10
- 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/codex/hook-config.json +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 +43 -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 +87 -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 +25 -12
- package/ds-canonical/references/hit-area-canonical.md +359 -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 +3 -3
- package/ds-canonical/skills/design-system-audit/references/audit-prompts.md +1 -1
- package/ds-canonical/skills/design-system-audit/references/historical-bugs.md +69 -0
- package/ds-canonical/skills/knowledge-prune/SKILL.md +4 -1
- package/ds-story-manifest.json +10 -4
- package/package.json +1 -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 +4 -2
- package/src/components/AccountMenu/account-menu.spec.md +1 -1
- package/src/components/AgentPanel/agent-panel-fab.tsx +26 -7
- package/src/components/AgentPanel/agent-panel.anatomy.stories.tsx +1 -1
- package/src/components/AgentPanel/agent-panel.spec.md +23 -8
- package/src/components/AgentPanel/agent-panel.stories.tsx +1 -1
- package/src/components/AgentPanel/agent-panel.tsx +51 -19
- 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 +2 -2
- 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.stories.tsx +4 -2
- package/src/components/Button/button.anatomy.stories.tsx +145 -33
- package/src/components/Button/button.spec.md +18 -5
- package/src/components/Button/button.tsx +65 -14
- package/src/components/Calendar/calendar.anatomy.stories.tsx +31 -11
- package/src/components/Calendar/calendar.principles.stories.tsx +39 -1
- package/src/components/Calendar/calendar.spec.md +92 -17
- package/src/components/Calendar/calendar.stories.tsx +25 -4
- package/src/components/Calendar/calendar.tsx +346 -66
- package/src/components/Carousel/carousel.anatomy.stories.tsx +16 -17
- package/src/components/Carousel/carousel.spec.md +47 -1
- package/src/components/Carousel/carousel.tsx +4 -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 +1 -1
- package/src/components/Chip/chip.tsx +4 -2
- package/src/components/Combobox/combobox.anatomy.stories.tsx +28 -36
- package/src/components/Combobox/combobox.spec.md +11 -3
- package/src/components/Combobox/combobox.tsx +25 -8
- package/src/components/Command/command.spec.md +11 -0
- package/src/components/Command/command.tsx +134 -5
- package/src/components/DataTable/cell-registry.tsx +2 -1
- package/src/components/DataTable/data-table-interaction-layer.tsx +27 -14
- 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 +61 -5
- package/src/components/DataTable/data-table.stories.tsx +55 -8
- package/src/components/DataTable/data-table.tsx +241 -53
- 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 +5 -1
- package/src/components/DatePicker/date-picker.tsx +20 -1
- package/src/components/Dialog/dialog.spec.md +1 -1
- package/src/components/Dialog/dialog.stories.tsx +9 -5
- package/src/components/Dialog/dialog.tsx +3 -1
- package/src/components/DropdownMenu/dropdown-menu-keyboard.ts +255 -0
- package/src/components/DropdownMenu/dropdown-menu.anatomy.stories.tsx +25 -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/Field/field-context.ts +4 -2
- package/src/components/Field/field-controls.spec.md +2 -2
- 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 +10 -5
- package/src/components/Field/field.stories.tsx +24 -1
- package/src/components/Field/field.tsx +7 -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 +4 -2
- 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 +34 -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/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 +5 -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.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 -1
- 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 +5 -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/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 +130 -123
- package/src/components/Rating/rating.principles.stories.tsx +69 -43
- package/src/components/Rating/rating.spec.md +140 -61
- package/src/components/Rating/rating.stories.tsx +11 -15
- package/src/components/Rating/rating.tsx +125 -115
- 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 +1 -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 +3 -2
- 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 +8 -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/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 +71 -5
- package/src/components/Sidebar/sidebar.stories.tsx +3 -4
- package/src/components/Sidebar/sidebar.tsx +271 -62
- package/src/components/Slider/slider.anatomy.stories.tsx +1 -1
- package/src/components/Slider/slider.spec.md +5 -5
- package/src/components/Slider/slider.tsx +13 -8
- package/src/components/Steps/steps.anatomy.stories.tsx +1 -0
- package/src/components/Steps/steps.spec.md +69 -21
- package/src/components/Steps/steps.stories.tsx +1 -1
- package/src/components/Steps/steps.tsx +97 -29
- 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 +1 -1
- package/src/components/Tag/tag.tsx +1 -1
- package/src/components/Textarea/textarea.tsx +3 -1
- package/src/components/TimePicker/time-columns.tsx +5 -1
- package/src/components/TimePicker/time-picker.spec.md +3 -0
- 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 +1 -1
- package/src/components/TreeView/tree-view.anatomy.stories.tsx +35 -13
- package/src/components/TreeView/tree-view.spec.md +64 -38
- package/src/components/TreeView/tree-view.stories.tsx +135 -10
- package/src/components/TreeView/tree-view.tsx +382 -351
- package/src/hooks/use-input-modality.ts +17 -46
- package/src/index.ts +17 -0
- package/src/lib/README.md +2 -0
- package/src/lib/focus-after-trigger.ts +102 -0
- package/src/lib/roving-list-keyboard.ts +380 -0
- package/src/lib/utils.ts +5 -0
- package/src/patterns/element-anatomy/inline-action.spec.md +14 -10
- package/src/patterns/element-anatomy/item-anatomy.spec.md +93 -3
- package/src/patterns/element-anatomy/item-anatomy.stories.tsx +75 -70
- package/src/patterns/element-anatomy/item-anatomy.tsx +17 -6
- package/src/patterns/header-canonical/header-canonical.spec.md +1 -1
- package/src/patterns/overlay-surface/overlay-surface.spec.md +10 -9
- package/src/patterns/overlay-surface/overlay-surface.tsx +4 -2
- package/src/patterns/resize-handle/resize-handle.spec.md +1 -1
- package/src/styles/base.css +1 -1
- package/src/tokens/categorical-color.ts +6 -1
- package/src/tokens/color/color.spec.md +79 -20
- package/src/tokens/color/color.stories.tsx +50 -2
- package/src/tokens/color/primitives.css +41 -38
- package/src/tokens/color/semantic.css +92 -36
- package/src/tokens/motion/motion.spec.md +26 -8
- 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 +16 -15
- package/src/tokens/uiSize/uiSize.stories.tsx +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"item-anatomy.js","sources":["../../../src/patterns/element-anatomy/item-anatomy.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 SSOT for Family 1+2 row primitives + all item-anatomy helpers(ItemContent / ItemIcon / ItemPrefix / ItemSuffix / ItemInlineAction 等),拆檔會讓 primitive 跨檔 import 滿天飛\nimport * as React from \"react\"\nimport type { LucideIcon } from \"lucide-react\"\nimport { cva } from \"class-variance-authority\"\nimport { cn, lineClampClass } from \"@/lib/utils\"\nimport { Avatar, type AvatarProps } from \"@/design-system/components/Avatar/avatar\"\nimport {\n Tooltip,\n TooltipTrigger,\n TooltipContent,\n} from \"@/design-system/components/Tooltip/tooltip\"\nimport { useTruncated } from \"@/design-system/hooks/use-truncated\"\n\n// ── InlineActionConfig ─────────────────────────────────────────────────────\n// 宣告式 API:consumer 只宣告 intent,host 根據 size tier 自動渲染。\n// 跨 Family 消費(Field family + Row family),SSOT 住 patterns/element-anatomy/\n// 對齊 inline-action.spec.md「API 設計」節。\n//\n// Canonical 實作:`ItemInlineAction`(本檔內)。\nexport interface InlineActionConfig {\n icon: LucideIcon\n /** aria-label,同時作為 tooltip 來源 */\n label: string\n /**\n * 點擊 handler。可選接收 React 事件物件——有需要時可呼叫 `stopPropagation()` 避免\n * 事件冒泡到父層(例如 Select 清除按鈕在 popover trigger 內,不想觸發 popover open)。\n */\n onClick: (e?: React.MouseEvent<HTMLButtonElement>) => void\n}\n\n/**\n * Item Layout — 共用工具與常數\n *\n * 這個檔案不是元件,是 `item-anatomy.spec.md` 的 code-level 實作。\n * 所有 row primitives(`MenuItem` / `TreeItem` / `SidebarMenuButton` /\n * `SelectionItem`)**必須 import 本 module 的 ICON_SIZE 和 helpers**,\n * 不要各自複製常數或 wrapper className——單一 source of truth,避免不一致。\n *\n * 詳細規則見 `item-anatomy.spec.md`。\n */\n\n// ── Types ──────────────────────────────────────────────────────────────────\n\n/** Row primitive 的 size 變體,對齊 `--field-height-*` token family */\nexport type RowSize = \"sm\" | \"md\" | \"lg\"\n\n// ── Constants ──────────────────────────────────────────────────────────────\n\n/**\n * Icon 尺寸(px)per row size。\n *\n * - sm / md: 16px(標準 row icon)\n * - lg: 20px(對齊 text-body-lg 的視覺重量)\n *\n * **用法**:透過 Lucide icon 的 `size` prop 直接傳入,不要用 CSS `[&>svg]:size-*`\n * selector——當 icon 被包在 `<ItemPrefix>` 的 `h-[1lh]` wrapper 內時,\n * `>` 直接子選擇器會失效,Lucide icon 會 fallback 到預設 24px。\n *\n * ```tsx\n * import { ICON_SIZE } from \"@/design-system/patterns/element-anatomy/item-anatomy\"\n *\n * const iconPx = ICON_SIZE[size] // size: RowSize\n * <MyLucideIcon size={iconPx} />\n * ```\n */\nexport const ICON_SIZE: Record<RowSize, number> = {\n sm: 16,\n md: 16,\n lg: 20,\n}\n\n/**\n * Avatar 尺寸(px)per row size,分 inline / block 兩種模式。\n *\n * - **inline**(無 description,大宗情境)→ 對齊第一行 label,用 `h-[1lh]` 容器\n * sm: 20, md: 24, lg: 24\n * - **block**(有 description,avatar 跨越 label + desc 兩行)→ 對齊文字塊中心\n * sm: 32, md: 32, lg: 40\n *\n * **用法**:row context 的 consumer(包含 `asChild` pattern 的 Sidebar / Tree\n * consumer)**必須**用 `<ItemAvatar>` / `<ItemIcon>` helper 元件(自動查\n * RowSizeContext 對表),**禁止** `import { AVATAR_SIZE }` 手動查表、**不可硬寫\n * `size={24}`** — 硬寫會讓 sm 變體的 avatar 尺寸錯誤、跟 ICON_SIZE 對齊規則脫鉤。\n * 詳 item-anatomy.spec.md「Helper 程式化封裝 + review 防線」段。\n *\n * ```tsx\n * import { ItemAvatar } from \"@/design-system/patterns/element-anatomy/item-anatomy\"\n *\n * <ItemAvatar alt=\"Alan\" color=\"blue\" /> // 自動查 AVATAR_SIZE.inline[rowSize]\n * ```\n *\n * Canonical 消費者:MenuItem 直接 import 本檔 `AVATAR_SIZE`(menu-item.tsx,消費 .block/.inline;re-export 已完成,MenuItem 無內部查表)。\n */\nexport const AVATAR_SIZE = {\n inline: { sm: 20, md: 24, lg: 24 },\n block: { sm: 32, md: 32, lg: 40 },\n} as const satisfies Record<\"inline\" | \"block\", Record<RowSize, number>>\n\n/**\n * 回傳 uniform prefix slot 的 inline style(CSS variable form)。\n *\n * 用法:Row-primitive 頂層容器(例如 `<SidebarProvider uniformPrefix>` 的 wrapper)\n * 把這個 style 套在 `style` prop 上(預先放在 `--mixed-prefix-slot` 候選位置,\n * 由外層 CSS `:has()` 條件化套到 `--item-prefix-slot`),子樹內所有 `<ItemPrefix>` /\n * `<ItemIcon>` / `<ItemAvatar>` 會自動用固定寬槽對齊。\n *\n * 槽寬 = `AVATAR_SIZE.inline[size]`(20/24/24 @ sm/md/lg)——以最大的 inline\n * prefix(avatar)為基準,icon 在槽內 `justify-center`。\n *\n * ```tsx\n * <ul style={getUniformPrefixSlotStyle(size)}>\n * <ItemIcon icon={Folder} /> ← 16px icon 在 24px 槽內置中\n * <ItemAvatar alt=\"GitHub\" /> ← 24px logo 填滿槽\n * </ul>\n * ```\n */\nexport function getUniformPrefixSlotStyle(size: RowSize): React.CSSProperties {\n return { \"--item-prefix-slot\": `${AVATAR_SIZE.inline[size]}px` } as React.CSSProperties\n}\n\n/**\n * Inline action(suffix 裡的 hover-action icon button)hover 背景尺寸(px)。\n * 規則:icon size + 2px(每邊 +1px,用 absolute positioning 溢出不影響排版)。\n * 對齊 `inline-action.spec.md`(canonical SSOT,2026-04-24 自 item-anatomy.spec 抽出)。\n */\n// code-quality-allow: dead-export — public constant — DS API surface,consumer 可引(即使當前 internal-only)\nexport const INLINE_ACTION_HOVER_BG_SIZE: Record<RowSize, number> = {\n sm: 18,\n md: 18,\n lg: 22,\n}\n\n/**\n * `ROW_PADDING_BY_SIZE` — Family 1+2 row primitive 的 size → padding + typography 公式 SSOT\n *\n * Formula(canonical):`py = (field-height - 1lh) / 2`,對齊 1em text 到 field-height center。\n *\n * **消費者**:menuItemVariants / sidebarMenuButtonVariants / treeItemVariants / 未來 row primitive\n * **Rationale**:前先 3 cva 各自寫同一公式 → drift risk(item-anatomy.spec 有明文「SidebarMenuButton\n * 獨立實作風險」)。抽本 export 後改**一處全同步**,risk 消失(對齊 Meta-Pattern M17「SSOT 必可傳播」)。\n *\n * 2026-04-24 D Phase 1+2 consolidation 建立。\n */\nexport const ROW_PADDING_BY_SIZE: Record<RowSize, string> = {\n sm: 'text-body leading-compact py-[calc((var(--field-height-sm)-1lh)/2)]',\n md: 'text-body leading-compact py-[calc((var(--field-height-md)-1lh)/2)]',\n lg: 'text-body-lg leading-compact py-[calc((var(--field-height-lg)-1lh)/2)]',\n}\n\n// ── Helper components ──────────────────────────────────────────────────────\n\n/**\n * `<ItemPrefix>` — Row primitive 的 prefix slot wrapper。\n *\n * 把 prefix(icon / avatar / checkbox / indicator)包進 `h-[1lh]` 容器,\n * 強制對齊第一行文字中線。這是 item-layout 的底層規則(詳見 spec「Prefix 垂直對齊」)。\n *\n * **為什麼**:\n * - Row primitive 的 outer flex 是 `items-start`(多行 label 時 prefix 不飄移)\n * - 單行時 prefix 透過這個 wrapper 強制對齊第一行中線,視覺效果 = `items-center`\n * - 不同尺寸的 prefix(icon 16、Avatar 24、Checkbox 20)都能穩定置中\n *\n * 用法:\n * ```tsx\n * <div className=\"flex items-start gap-2\">\n * <ItemPrefix>\n * <Avatar size={24} />\n * </ItemPrefix>\n * <span className=\"min-w-0 flex-1 truncate\">{label}</span>\n * </div>\n * ```\n *\n * asChild 的 consumer(例如 `<SidebarMenuButton asChild>`)**必須自己**用這個 wrapper\n * 包 prefix,不能省略。\n */\nexport const ItemPrefix = React.forwardRef<\n HTMLSpanElement,\n React.HTMLAttributes<HTMLSpanElement>\n>(({ className, ...props }, ref) => (\n <span\n ref={ref}\n className={cn(\n \"h-[1lh] shrink-0 flex items-center\",\n // Uniform prefix slot:讀取 `--item-prefix-slot` CSS variable,\n // fallback `auto`(= 預設行為,不佔額外寬度、不 center)。\n // Row-primitive 頂層容器(例如 `<SidebarProvider uniformPrefix>` 的 wrapper)\n // 若設定這個 var,小尺寸 prefix(icon 16px)會在固定寬槽(24px @ md)內置中,\n // 與大尺寸 prefix(app logo 24px)對齊,達成 label 齊左。\n // 詳見 item-anatomy.spec.md「Uniform prefix slot」節。\n \"min-w-[var(--item-prefix-slot,auto)] justify-center\",\n className\n )}\n {...props}\n />\n))\nItemPrefix.displayName = \"ItemPrefix\"\n\n/**\n * `itemPrefixAlignVariants` — Prefix 對齊高度的 cva SSOT(for 複雜 prefix 結構)。\n *\n * ── 存在的理由 ──\n * `<ItemPrefix>` primitive 預設單一 child(icon / avatar 任一)+ inline 對齊。\n * 但**複雜 prefix 結構**(e.g. MenuItem 可同時含 checkbox + icon/avatar 多 child)\n * 或**block mode**(大 prefix + description 對齊塊中心)需要自建 wrapper cva。\n * 本 cva 把 block formula SSOT 化,讓 MenuItem 等 consumer 共用,\n * 改值一處(token or cva)→ 所有 block-prefix consumer 同步。\n *\n * ── Align 變體(對齊 item-anatomy.spec.md「24px 閾值對齊規則」) ──\n * - `\"inline\"`:`h-[1lh]`,小 prefix(≤ 24px)對齊 label 第一行\n * - `\"block-sm\"` / `\"block-md\"`:大 prefix + scanning desc(sm/md menu),\n * 高度 = label(1lh) + gap(token) + desc(caption × 1.3)\n * - `\"block-lg\"`:大 prefix + scanning desc(lg menu),desc 改為 body × 1.3\n *\n * ── Consumer ──\n * MenuItem(inline / block-sm / block-md / block-lg)。其他 row primitive 簡單情境\n * 直接用 `<ItemPrefix>` 就好,不需此 cva。\n *\n * ── SSOT 傳播 ──\n * gap 用 `var(--item-gap-label-desc-scanning[-lg])` token(bare 變體已不存在,4 個 mode×size 變體);font-size 用 token-awareness\n * (`var(--font-caption-size)` / `var(--font-body-size)`)。改 token → 公式同步。\n */\n// code-quality-allow: long-function — cva variant/styles table — 拆 fn 會失去 type inference + 跨 fn 傳 config 反而難讀\nexport const itemPrefixAlignVariants = cva(\n \"flex items-center gap-2 shrink-0\",\n {\n variants: {\n align: {\n inline: \"h-[1lh]\",\n // sm/md desc = text-caption (12px × 1.3 = 15.6px)\n // block-sm / block-md:scanning mode(body + caption 組合)→ 用 scanning mode 專屬 gap token\n \"block-sm\":\n \"h-[calc(1lh+var(--item-gap-label-desc-scanning)+var(--font-caption-size)*1.3)]\",\n \"block-md\":\n \"h-[calc(1lh+var(--item-gap-label-desc-scanning)+var(--font-caption-size)*1.3)]\",\n // block-lg:scanning-lg mode(body-lg + body compact 組合)→ 用 scanning-lg mode 專屬 gap token\n \"block-lg\":\n \"h-[calc(1lh+var(--item-gap-label-desc-scanning-lg)+var(--font-body-size)*1.3)]\",\n },\n },\n defaultVariants: {\n align: \"inline\",\n },\n }\n)\n\n// ── 單行截斷偵測 measure(ItemLabel / ItemContent 共用)──────────────────────\n// 截斷 → tooltip 顯完整內容、僅實際截斷時顯示(owner:components/Tooltip/tooltip.spec.md:32;\n// 引擎 SSOT:patterns/element-anatomy/truncated-text.spec.md)。不消費 `<TruncatedText>`——\n// trigger 需自控(ItemLabel 要 forwardRef + data-sidebar attr + props spread;ItemContent 要保留\n// labelTruncate / clamp className 原樣)→ 走 spec 指定解:public `useTruncated` 自組\n// (同 components/InlineEdit/inline-edit.tsx「單行純值 view 截斷 tooltip」段與 components/Tag 的用法)。\n// 兩種「單行截斷」機制都要偵測(MenuItem 的 clamp 走 className escape,量 DOM 才能全蓋):\n// - `truncate`(nowrap)→ scrollWidth 溢出(useTruncated 預設量法)\n// - `line-clamp-1`(MenuItem labelMaxLines/descMaxLines=1、descriptionClamp=1;文字先 wrap\n// 再垂直裁)→ scrollWidth 不溢出,只能量 scrollHeight;以「可視高度 ≈ 1 行」gate 限定單行\n// clamp。多行 clamp(≥2 行)刻意不顯 tooltip:truncated-text.spec.md「何時不用」(a) 明文\n// 排除多行,DS 尚無多行截斷↔補全 canonical(owner 決策前維持排除)—— gate 機械保證。\nconst measureSingleLineClipped = (el: HTMLElement): boolean => {\n if (el.scrollWidth > el.clientWidth) return true\n const lineHeight = parseFloat(getComputedStyle(el).lineHeight)\n if (Number.isNaN(lineHeight)) return false // line-height:normal 等不可量 → 只認橫向溢出\n const isSingleVisualLine = el.clientHeight < lineHeight * 1.5\n return isSingleVisualLine && el.scrollHeight > el.clientHeight + 1\n}\n\n/**\n * `<ItemLabel>` — Row primitive 的 label span。\n *\n * 預設 `min-w-0 flex-1 truncate`——單行截斷,佔滿剩餘 flex 空間。\n * 截斷時 hover 顯 tooltip 補全(截斷才顯,tooltip.spec.md:32;內建,consumer 免自接)。\n * 加 `data-sidebar=\"menu-label\"` attribute 讓 sidebar icon 模式的 CSS selector 能命中。\n *\n * 用法:\n * ```tsx\n * <ItemLabel>{label}</ItemLabel>\n * ```\n */\nexport const ItemLabel = React.forwardRef<\n HTMLSpanElement,\n React.HTMLAttributes<HTMLSpanElement>\n>(({ className, children, ...props }, forwardedRef) => {\n const { ref: truncationRef, isTruncated } = useTruncated<HTMLSpanElement>({\n measure: measureSingleLineClipped,\n deps: [children],\n })\n return (\n // 永遠 wrap + `open` 控制(truncated-text.spec.md canonical:條件 wrap 會 remount span、\n // ref 對不上真 DOM)。hover-only:trigger = 不可聚焦 span,不加 tabIndex —— menu row 的\n // roving focus 在宿主 row 上,label 加 tab stop 會破壞 APG menu 鍵盤導航;先例 = Tag 帶\n // 內建截斷 tooltip 渲染於 SelectMenu option 內(select.tsx renderLabel → tag.tsx),\n // menu 內無抑制。鍵盤/SR 無損失:row accessible name 本就含完整文字。\n <Tooltip open={isTruncated ? undefined : false}>\n <TooltipTrigger asChild>\n <span\n ref={(el) => {\n truncationRef.current = el\n if (typeof forwardedRef === \"function\") forwardedRef(el)\n else if (forwardedRef) forwardedRef.current = el\n }}\n data-sidebar=\"menu-label\"\n className={cn(\"min-w-0 flex-1 truncate\", className)}\n {...props}\n >\n {children}\n </span>\n </TooltipTrigger>\n <TooltipContent>{children}</TooltipContent>\n </Tooltip>\n )\n})\nItemLabel.displayName = \"ItemLabel\"\n\n/**\n * `<ItemContent>` — Row primitive 的 label + optional description 內容區(SSOT)。\n *\n * ── 存在的唯一理由 ──\n * 封裝「flex-col + label + description + canonical label/description gap」結構,\n * 避免 13+ 消費者各自 hard-code `mt-0.5`。改 token 一處,全 DS 同步。\n *\n * ── 截斷 → tooltip(內建,consumer 免自接)──\n * label(`truncate` 或 `line-clamp-1` escape)與單行截斷 description 在實際截斷時 hover 顯\n * tooltip 補全(截斷才顯,tooltip.spec.md:32);多行 clamp(≥2 行)排除 —— 見\n * `measureSingleLineClipped` 註解(truncated-text.spec.md「何時不用」(a))。\n *\n * ── Consumer 偏離 canonical ──\n * 消費端若有**確切合理理由**不用 `<ItemContent>` / 需自訂 label/desc 行為,必在\n * 該元件 `spec.md` 明文寫下 rationale(對齊 item-anatomy canonical「偏離必明文」)。\n * 合法偏離範例:\n * - MenuItem 的 `leading-compact + text-caption` scanning-mode typography\n * → MenuItem spec 明文 rationale + 消費 `--item-gap-label-desc-scanning[-lg]` token 直接用\n * - SelectionItem 的 control slot 跟 block formula 綁定\n * → SelectionItem spec 明文 rationale\n *\n * ── Props ──\n * - `label`:label 內容(ReactNode,必填)\n * - `description`:description 內容(ReactNode,optional)\n * - `mode`:typography tier(scanning vs reading,世界級對齊 Material `dense` / Carbon `size` / Ant `size`)\n * - `\"reading\"`(預設):繼承 parent `text-body`(14px / 1.5 leading),舒適閱讀(Family 2 / Material body-large 派)\n * - `\"scanning\"`:desc 縮為 `text-caption`(12px)+ `leading-compact`(1.3),緊湊掃視\n * (Family 1 Menu item / Material body-medium dense 派)\n * - `descriptionTone`:desc 顏色語意\n * - `\"secondary\"`(預設):`text-fg-secondary`\n * - `\"error\"`:`text-error-text`\n * - `\"muted\"`:`text-fg-muted`\n * - `\"disabled\"`:`text-fg-disabled`(對齊 M24 state precedence:disabled > muted > emphasis)\n * - `descriptionWrap`:desc 多行 wrap(預設 true)/ false = truncate\n * - `labelClassName` / `descriptionClassName`:escape hatches(明文 rationale 才用)\n *\n * ── World-class benchmark ──\n * 6 家 DS(Material / Polaris / Atlassian / Apple HIG / Carbon / Ant)一致用\n * 14/20 vs 16/24 兩擋 body 表達 scanning vs reading。API 兩派:(A)密度 prop\n * Material `dense` / Carbon `size` / Ant `size`(本 DS 採 A 派);(B)typography\n * token 手選 Polaris / Atlassian / Apple。\n */\n\nexport interface ItemContentProps extends Omit<React.HTMLAttributes<HTMLDivElement>, \"title\"> {\n label: React.ReactNode\n description?: React.ReactNode\n /**\n * Typography mode — **leading 行為**(跟 size 正交的另一維):\n * - `\"reading\"`(預設):default leading(1.5)— Family 2 List item 舒適閱讀\n * - `\"scanning\"`:`leading-compact`(1.3)+ desc 降一 tier —— Menu item / Step 掃視\n */\n mode?: \"reading\" | \"scanning\"\n /**\n * Size tier — **label 字級**(跟 mode 正交的另一維):\n * - `\"md\"`(預設):label `text-body`(14)\n * - `\"lg\"`:label `text-body-lg`(16)— desc 永遠比 label 小一 tier(body)\n *\n * 2 × 2 = 4 typography 組合,對應 4 tokens:\n * - md + reading → body(14/1.5) + body(14/1.5)\n * - md + scanning → body(14/1.3) + caption(12/1.3)\n * - lg + reading → body-lg(16/1.5) + body(14/1.5)\n * - lg + scanning → body-lg(16/1.3) + body(14/1.3)\n */\n size?: \"md\" | \"lg\"\n descriptionTone?: \"secondary\" | \"error\" | \"muted\" | \"disabled\"\n descriptionWrap?: boolean\n /**\n * Description 多行截斷(line-clamp-N)。undefined = 不 clamp(自由 wrap)。\n * Tailwind line-clamp utilities 支援 1-6。\n * `1`(單行 clamp)截斷時 hover 顯 tooltip 補全(截斷才顯,tooltip.spec.md:32);\n * `>= 2`(多行)不配 tooltip —— truncated-text.spec.md「何時不用」(a) 排除多行,\n * DS 尚無多行截斷↔補全 canonical(owner 決策前維持排除)。\n */\n descriptionClamp?: number\n /**\n * Description `break-words`(default `false`,僅在 consumer 需要時 opt-in)。\n * 使用情境:SelectionItem / Steps 這類 description 可能塞長英文 token 需 break。\n */\n descriptionBreakWords?: boolean\n /**\n * Label truncate (default `true`, 對齊 row-item 大宗 idiom)。\n * Opt-out(`false`)for card-like consumers(e.g. ProfileCard label 允許 wrap)。\n * World-class 對照:Material `ListItemText primaryTypographyProps.noWrap` default true。\n */\n labelTruncate?: boolean\n labelClassName?: string\n descriptionClassName?: string\n}\n\n// code-quality-allow: long-function — foundational composite main body — 拆 sub-fn 會複雜化 local state / ref / context binding\nexport const ItemContent = React.forwardRef<HTMLDivElement, ItemContentProps>(\n (\n {\n label,\n description,\n mode = \"reading\",\n size = \"md\",\n descriptionTone = \"secondary\",\n descriptionWrap = true,\n descriptionClamp,\n descriptionBreakWords = false,\n labelTruncate = true,\n labelClassName,\n descriptionClassName,\n className,\n ...props\n },\n ref,\n ) => {\n const toneClass = {\n secondary: \"text-fg-secondary\",\n error: \"text-error-text\",\n muted: \"text-fg-muted\",\n disabled: \"text-fg-disabled\",\n }[descriptionTone]\n\n // Typography desc class — 2 維 cross-product (mode × size):\n // ┌───────────┬────────────────────────────┬────────────────────────────┐\n // │ │ size=\"md\"(body label 14) │ size=\"lg\"(body-lg label 16)│\n // ├───────────┼────────────────────────────┼────────────────────────────┤\n // │ reading │ 繼承 body(14/1.5) │ body(14)default leading │\n // │ scanning │ caption(12)+ compact │ body(14)+ compact │\n // └───────────┴────────────────────────────┴────────────────────────────┘\n const isLg = size === \"lg\"\n const modeClass =\n mode === \"scanning\"\n ? isLg\n ? \"text-[length:var(--font-body-size)] leading-compact\"\n : \"text-[length:var(--font-caption-size)] leading-compact\"\n : isLg\n ? \"text-[length:var(--font-body-size)]\"\n : \"\"\n\n const clampClass = lineClampClass(descriptionClamp) ?? \"\"\n\n // 截斷 → tooltip(截斷才顯,tooltip.spec.md:32)。量測走 measureSingleLineClipped(檔內\n // SSOT 註解):label 蓋 labelTruncate `truncate` + MenuItem labelClassName `line-clamp-1`\n // escape 兩路;description 只在單行截斷(`descriptionClamp === 1` 或 !descriptionWrap 的\n // truncate 分支,含 className escape)顯示——多行 clamp(≥2)由量測 gate 機械排除\n // (truncated-text.spec.md「何時不用」(a):本 primitive 不處理多行)。\n const { ref: labelRef, isTruncated: labelClipped } = useTruncated<HTMLSpanElement>({\n measure: measureSingleLineClipped,\n deps: [label],\n })\n const { ref: descriptionRef, isTruncated: descriptionClipped } = useTruncated<HTMLSpanElement>({\n measure: measureSingleLineClipped,\n deps: [description],\n })\n\n return (\n <div\n ref={ref}\n className={cn(\"flex flex-col min-w-0 flex-1\", className)}\n {...props}\n >\n {/* 永遠 wrap + open 控制(truncated-text.spec.md canonical);hover-only 不加 tabIndex\n (menu row roving focus 見 ItemLabel 註解)。 */}\n <Tooltip open={labelClipped ? undefined : false}>\n <TooltipTrigger asChild>\n <span ref={labelRef} className={cn(labelTruncate && \"truncate\", labelClassName)}>\n {label}\n </span>\n </TooltipTrigger>\n <TooltipContent>{label}</TooltipContent>\n </Tooltip>\n {description && (\n <Tooltip open={descriptionClipped ? undefined : false}>\n <TooltipTrigger asChild>\n <span\n ref={descriptionRef}\n className={cn(\n // Typography-mode-aware gap token(2026-04-23):\n // (mode, size)2 維正交 → 4 token 對應 4 typography 組合\n mode === \"scanning\"\n ? isLg\n ? \"mt-[var(--item-gap-label-desc-scanning-lg)]\"\n : \"mt-[var(--item-gap-label-desc-scanning)]\"\n : isLg\n ? \"mt-[var(--item-gap-label-desc-reading-lg)]\"\n : \"mt-[var(--item-gap-label-desc-reading)]\",\n modeClass,\n toneClass,\n clampClass,\n descriptionBreakWords && \"break-words\",\n !descriptionWrap && \"truncate\",\n descriptionClassName,\n )}\n >\n {description}\n </span>\n </TooltipTrigger>\n <TooltipContent>{description}</TooltipContent>\n </Tooltip>\n )}\n </div>\n )\n },\n)\nItemContent.displayName = \"ItemContent\"\n\n// ── Row size context ──────────────────────────────────────────────────────\n//\n// 作用:讓 row primitive(Sidebar / SelectMenu / Tree / DropdownMenu)只 propagate\n// 一次 size,整個子樹內的 <ItemIcon> / <ItemAvatar> 都自動查到正確尺寸。\n//\n// **存在的唯一理由**:消除 asChild pattern 下 consumer 硬寫 `size={24}` 的可能性。\n// 過去 consumer 必須 `useSidebar().size` 再手動傳 `AVATAR_SIZE.inline[size]`,\n// 每個 consumer 都是一次漂移機會(曾發生:sm 欄 avatar 顯示 24 而非 20,破壞規格)。\n// 現在 consumer 只寫 `<ItemAvatar alt=\"...\" />`,尺寸由 context 自動決定。\n//\n// Row primitive 實作者的責任:在元件內部用 `<RowSizeProvider value={size}>` 包裹\n// children(不論 asChild 或非 asChild 路徑),這樣任何 descendant 的 ItemIcon /\n// ItemAvatar 都會拿到正確尺寸。\n\nconst RowSizeContext = React.createContext<RowSize | null>(null)\n\n/** @internal — L3 row-size primitive;app code 不直接 import,由 MenuItem/Row/Field 等內部消費(inline-action.spec.md)。 */\nexport const RowSizeProvider = RowSizeContext.Provider\n\n/**\n * 讀取當前 row size。在 row primitive 子樹外呼叫會回 fallback(預設 \"md\")。\n * Row primitive 實作者不該呼叫這個——你們已經知道自己的 size。\n * 這個 hook 是給 `<ItemIcon>` / `<ItemAvatar>` 和 asChild consumer 的逃生艙用。\n */\nexport function useRowSize(fallback: RowSize = \"md\"): RowSize {\n return React.useContext(RowSizeContext) ?? fallback\n}\n\n// ── Prefab prefix elements ────────────────────────────────────────────────\n//\n// `<ItemIcon>` 和 `<ItemAvatar>` 是「prefix slot 的完成品元件」——自帶 ItemPrefix\n// wrapper + 自動查 ICON_SIZE / AVATAR_SIZE。\n//\n// **asChild consumer 必須用這兩個元件,禁止手動寫 `<Avatar size={N} />`**。\n// 禁令理由見 item-anatomy.spec.md「Avatar 尺寸選擇」節。\n\nexport interface ItemIconProps\n extends Omit<React.SVGAttributes<SVGSVGElement>, \"children\"> {\n /** Lucide icon 元件(不是 JSX element) */\n icon: LucideIcon\n}\n\n/**\n * `<ItemIcon icon={Folder} />` — 自動查 `ICON_SIZE[rowSize]`,包在 `ItemPrefix` 內。\n * 永遠對齊第一行 label(item-layout 的 icon 永遠 inline)。\n */\nexport const ItemIcon = React.forwardRef<SVGSVGElement, ItemIconProps>(\n ({ icon: Icon, className, ...props }, ref) => {\n const size = useRowSize()\n return (\n // data-prefix-type=\"icon\" 讓 row-primitive 容器的 `:has()` selector 能偵測類型,\n // 用於 `<SidebarProvider uniformPrefix>` opt-in 後的混用 auto-detection\n <ItemPrefix data-prefix-type=\"icon\">\n <Icon\n ref={ref}\n size={ICON_SIZE[size]}\n className={cn(\"shrink-0\", className)}\n aria-hidden\n {...props}\n />\n </ItemPrefix>\n )\n }\n)\nItemIcon.displayName = \"ItemIcon\"\n\nexport interface ItemAvatarProps extends Omit<AvatarProps, \"size\"> {\n /**\n * 對齊模式(選填,預設 `\"inline\"`):\n * - `\"inline\"`:視覺重量輕的 avatar,對齊第一行 label(20/24/24 @ sm/md/lg)\n * - `\"block\"`:視覺重量重的 avatar,對齊 label + description 文字塊(32/32/40 @ sm/md/lg)\n *\n * 預設 inline——大宗情境(扁平 row、footer user、主清單)都用這個。\n * 只有 avatar 是 item 主體(人物卡、顯著身份辨識)才用 block。\n *\n * 詳見 `item-anatomy.spec.md`「Avatar 尺寸選擇」節,含 24px 對齊模式閾值規則。\n */\n mode?: \"inline\" | \"block\"\n}\n\n/**\n * `<ItemAvatar alt=\"Alan\" color=\"blue\" />` — 自動查 `AVATAR_SIZE[mode][rowSize]`,\n * 包在 `ItemPrefix` 內。\n *\n * asChild consumer 的標準用法——不需知道 size,不需 import AVATAR_SIZE,\n * 不可能硬寫錯誤尺寸。\n */\n// code-quality-allow: long-function — forwardRef body 含 size×color×hoverCard 三正交 prop 渲染分流,拆出 sub-fn 反增 prop drilling\nexport const ItemAvatar = React.forwardRef<HTMLDivElement, ItemAvatarProps>(\n ({ mode = \"inline\", ...props }, ref) => {\n const size = useRowSize()\n return (\n // data-prefix-type=\"avatar\" 讓 row-primitive 容器的 `:has()` selector 能偵測類型,\n // 用於 `<SidebarProvider uniformPrefix>` opt-in 後的混用 auto-detection\n <ItemPrefix data-prefix-type=\"avatar\">\n <Avatar ref={ref} size={AVATAR_SIZE[mode][size]} {...props} />\n </ItemPrefix>\n )\n }\n)\nItemAvatar.displayName = \"ItemAvatar\"\n\n// ── Inline action (suffix slot) ───────────────────────────────────────────\n//\n// 共用 row-primitive 的 suffix inline action 渲染——消除過去 Input / NumberInput /\n// Tag / LinkInput / Combobox 各自複製 18 行 JSX 的狀況(見 `inline-action.spec.md`)。\n//\n// 規格(完全對齊 inline-action.spec.md — canonical SSOT,2026-04-24 自 item-anatomy.spec 抽出):\n// - Icon 尺寸 = ICON_SIZE[rowSize] (16/16/20)\n// - Hover bg 尺寸 = icon + 2 (18/18/22,INLINE_ACTION_HOVER_BG_SIZE)\n// - Hover bg 圓角 = rounded-md (sm/md)/ rounded-md (lg)\n// - Icon 顏色:fg-muted → fg-secondary on hover/active\n// - 必須 <button type=\"button\">,aria-label = label,Tooltip(同 label)\n// - cursor-pointer\n\n// ── Low-level:ItemInlineActionButton ──\n//\n// Root 是 `<button>`(不是 Tooltip wrapper),可直接塞進 Radix `asChild` pattern\n// 例如 `<Collapsible.Trigger asChild>`, `<Popover.Trigger asChild>` 等。\n//\n// 自動從 `RowSizeContext` 查:\n// - 寬高 = ICON_SIZE[size]\n// - Hover bg 尺寸 = INLINE_ACTION_HOVER_BG_SIZE[size]\n// - Hover bg 圓角 = rounded-md (sm/md) / rounded-md (lg)\n// - Icon color = fg-muted → fg-secondary on hover/active\n//\n// 接受任何 button props 並 spread(讓 Radix Slot 可以 merge onClick / data-state 等)\n\n/** @internal — internal ItemInlineActionButton 的 props;隨其元件同走 subpath,不進 root barrel front-door。 */\nexport interface ItemInlineActionButtonProps\n extends Omit<React.ButtonHTMLAttributes<HTMLButtonElement>, \"children\"> {\n /** Lucide icon 元件 */\n icon: LucideIcon\n /** 可選:額外加在 icon 上的 className(例如 rotate transition) */\n iconClassName?: string\n /**\n * Size 明確覆寫(用於 fields / Tag 等**不在 `RowSizeProvider` 樹內**的 host):\n * 傳了就以此為準,否則從 `RowSizeContext` 讀(fallback \"md\")。\n *\n * 使用時機:\n * - Row primitive 子樹(Sidebar / TreeView / SelectMenu / DropdownMenu)→ **不傳**,吃 context\n * - Field / Tag(自有 size 系統,沒包 RowSizeProvider)→ **必傳** `size={hostSize}`\n */\n size?: RowSize\n /**\n * Hover / active bg className override(chromatic host 專用,如 Tag solid variant)。\n *\n * 預設 `group-hover/action:bg-neutral-hover group-active/action:bg-neutral-active`。\n * 提供後**完全取代** hover + active 雙態 className,但**不影響** rest 態(永遠 bg-transparent)\n * 與 overlay-trigger 態(`group-data-[state=open]/action:bg-neutral-hover` — 維持 host hover bg)。\n *\n * Consumer 須**同時**包含 hover + active 兩態 selector,例:\n * `'group-hover/action:bg-[var(--my-hover)] group-active/action:bg-[var(--my-active)]'`\n *\n * Use case:Tag solid(blue/green/red 等)需 hover bg 跟 host 色相一致(非 neutral)。\n */\n hoverBgClassName?: string\n /**\n * 標記本 button 是 **overlay trigger**(DropdownMenu / Popover / Tooltip 透過 `asChild` 包覆)。\n *\n * `true` 時:Radix overlay open 期間(`data-state=\"open\"`)button 維持 host hover 樣式\n * — 視覺鎖,讓 user 追溯 floating panel 來源(對齊 shadcn / Radix Themes / Material)。\n *\n * `false` 時(default):無 data-state=open 樣式 — 適用 in-place 互動如 `Collapsible.Trigger`\n * (展開內容接在 trigger 下方,不需追溯)、drag handle、dismiss X 等。\n *\n * Canonical 詳 `inline-action.spec.md`「Overlay trigger canonical」段。\n *\n * @default false\n */\n overlayTrigger?: boolean\n}\n\n/** @internal — L3 inline-action primitive;app code 禁直接 import,經 MenuItem/Row endActions 消費(inline-action.spec.md)。 */\nexport const ItemInlineActionButton = React.forwardRef<\n HTMLButtonElement,\n ItemInlineActionButtonProps\n>(({ icon: Icon, className, iconClassName, style, type = \"button\", size: sizeProp, hoverBgClassName, overlayTrigger = false, ...rest }, ref) => {\n const contextSize = useRowSize()\n const size = sizeProp ?? contextSize\n const iconPx = ICON_SIZE[size]\n const hoverBgPx = INLINE_ACTION_HOVER_BG_SIZE[size]\n return (\n <button\n ref={ref}\n type={type}\n className={cn(\n // 焦點框往外(= 什麼都不寫,吃 styles/base.css 全域規則)。2026-09-10 逐站點實測翻案:\n // 26 個真實站點 × sm/md/lg,四周最小淨空 5–192px(最小值 4.00 = Breadcrumb 兩側的分隔符,\n // canonical「4.00 算放得下」),DPR2 逐像素數框帶 → 零裁切、零遮蓋。列不會裁到它:截斷是\n // label 自己的 `truncate`(兄弟節點),不是祖先。原註解「往外 +2px 實測上下各被裁 1px」\n // 只在 **Tag 宿主**成立(h-6 + 1px 邊框 + overflow-hidden → 淨空 3px,sm 1px),\n // 依 focus-canonical「同一個底層元件被另一個元件放進貼邊位置,往內由那個外層元件承擔」,\n // 往內的 class 搬到 `tag.tsx` TagDismiss,不由 primitive 整個翻內。\n \"group/action relative grid place-content-center shrink-0 cursor-pointer\",\n // 弱化 icon hover 階梯 SSOT(2026-07-30 user 拍板):fg-muted(neutral-7)→ 一階 fg-secondary\n // (neutral-8),不跳到 foreground(neutral-9)。inline-action.spec.md「Icon 色彩」+\n // tokens/color/semantic.css:53 為 rule owner;世界級對照 Fluent 2\n // (neutralForeground3Hover === neutralForeground2 值)/ MUI Chip deleteIcon(.26 → .4)。\n // hover 底色瞬間切換,不做過渡(user 2026-09-10 拍板「第三題改成全部瞬間」;SSOT = tokens/motion/motion.spec.md「hover 回饋不做過渡」)\n \"text-fg-muted hover:text-fg-secondary active:text-fg-secondary\",\n // Overlay trigger active state — 只在 consumer 顯式宣告 overlayTrigger=true 時生效\n // (canonical 2026-05-05:Collapsible / drag / dismiss 等 in-place 互動 ≠ overlay,\n // 不應 leak 視覺 lock。詳 inline-action.spec.md「Overlay trigger canonical」)\n overlayTrigger && \"data-[state=open]:text-fg-secondary\",\n // 2026-09-07:刪掉本地 focus-visible:outline-2 + outline-ring —— 與全域\n // styles/base.css:44-47 逐字等價(同 2px、同 --ring、offset 也吃全域的 2px),\n // 且本區塊無 outline-none,全域本來就生效。刪除為零視覺變化。\n className\n )}\n style={{ width: iconPx, height: iconPx, ...style }}\n {...rest}\n >\n <span\n aria-hidden\n className={cn(\n \"absolute pointer-events-none\",\n \"rounded-md\",\n \"bg-transparent\",\n hoverBgClassName ?? \"group-hover/action:bg-neutral-hover group-active/action:bg-neutral-active\",\n // Overlay 開啟 = 維持 host hover bg(只在 overlayTrigger=true 時生效)\n overlayTrigger && \"group-data-[state=open]/action:bg-neutral-hover\"\n )}\n style={{\n width: hoverBgPx,\n height: hoverBgPx,\n top: \"50%\",\n left: \"50%\",\n transform: \"translate(-50%, -50%)\",\n }}\n />\n <Icon size={iconPx} className={cn(\"relative\", iconClassName)} aria-hidden />\n </button>\n )\n})\nItemInlineActionButton.displayName = \"ItemInlineActionButton\"\n\n// ── High-level:ItemInlineAction ──\n//\n// 宣告式 API:consumer 給 `{ icon, label, onClick }`,元件自動處理 Tooltip +\n// aria-label。適用於 row primitive 的 suffix slot(SidebarMenuButton inlineActions 等)。\n//\n// 內部用 `ItemInlineActionButton`——兩層共用同一套視覺規格、同一個 RowSizeContext 查表。\n// 需要接 Radix asChild(例如 collapsible trigger)時改用 `ItemInlineActionButton`,\n// 那層沒有 Tooltip 包裝,Root 就是 button,可直接塞進 Radix Slot。\n\n/** @internal — internal ItemInlineAction 的 props;隨其元件同走 subpath,不進 root barrel front-door。 */\nexport interface ItemInlineActionProps {\n action: InlineActionConfig\n /** Size 覆寫(見 `ItemInlineActionButtonProps.size`)——Field Controls / Tag 必傳 */\n size?: RowSize\n}\n\n/** @internal — L3 inline-action primitive;app code 禁直接 import,經 row 元件消費(inline-action.spec.md)。 */\nexport const ItemInlineAction = React.forwardRef<\n HTMLButtonElement,\n ItemInlineActionProps\n>(({ action, size }, ref) => (\n <Tooltip>\n <TooltipTrigger asChild>\n <ItemInlineActionButton\n ref={ref}\n icon={action.icon}\n aria-label={action.label}\n onClick={action.onClick}\n size={size}\n />\n </TooltipTrigger>\n <TooltipContent>{action.label}</TooltipContent>\n </Tooltip>\n))\nItemInlineAction.displayName = \"ItemInlineAction\"\n\n// ── Item suffix slot ──────────────────────────────────────────────────────\n//\n// `<ItemSuffix>` — row primitive 的 suffix 容器。對齊 item-layout spec:\n// - `h-[1lh]`:suffix 永遠對齊第一行 label(跟 prefix 解耦)\n// - `ml-auto`:靠右\n// - `gap-2`:多個 inline action 之間的標準間距(inline-action.spec.md canonical SSOT)\n// - `hoverReveal` opt-in:opacity 0→1 on 父層 row hover/focus-visible\n//\n// `hoverGroup`(2026-05-05 v8 SSOT 升級):row primitive group selector 參數化。\n// 先前 `/menu-item` hardcode → TreeView(`/tree-item`)/ FileItem(`/row`)/\n// DataTable(`/row`)無法消費,被迫自刻 suffix slot(M1 違反)。\n// 參數化後 4 個 row primitive 都走同一條 SSOT。Tailwind JIT 需 static class,\n// 故用 lookup table 而非動態字串(避免 JIT scan 失敗)。\n\nconst SUFFIX_HOVER_REVEAL_BY_GROUP = {\n \"menu-item\":\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 \"tree-item\":\n \"opacity-0 group-hover/tree-item:opacity-100 group-has-[:focus-visible]/tree-item:opacity-100 transition-opacity duration-150 motion-reduce:duration-0\",\n row: \"opacity-0 group-hover/row:opacity-100 group-has-[:focus-visible]/row:opacity-100 transition-opacity duration-150 motion-reduce:duration-0\",\n} as const\n\nexport type ItemSuffixHoverGroup = keyof typeof SUFFIX_HOVER_REVEAL_BY_GROUP\n\nexport interface ItemSuffixProps extends React.HTMLAttributes<HTMLSpanElement> {\n /**\n * Hover-reveal:預設隱藏,父層 row hover / keyboard focus-visible 時才淡入。\n * 對齊 TreeView / SidebarMenuButton inline action 行為。預設 false(永遠顯示,如 Badge)。\n *\n * 用 `group-has-[:focus-visible]` 而非 `group-focus-within`——後者會被 mouse click 觸發,\n * 導致 click 後 suffix 永久顯示直到焦點移走。focus-visible 只在鍵盤 tab 時啟動。\n */\n hoverReveal?: boolean\n /**\n * 父層 row 的 group selector(consumer 必加 `group/<name>` 到 row wrapper)。\n * 預設 `'menu-item'`(對齊 MenuItem / Sidebar)。其他 row primitive:\n * `'tree-item'`(TreeView)/ `'row'`(DataTable / FileItem)。\n *\n * 加新 row primitive(且需 hover-reveal)時:在 `SUFFIX_HOVER_REVEAL_BY_GROUP`\n * 加 entry,Tailwind JIT 才能 scan 到 static class。\n */\n hoverGroup?: ItemSuffixHoverGroup\n}\n\nexport const ItemSuffix = React.forwardRef<HTMLSpanElement, ItemSuffixProps>(\n ({ className, hoverReveal = false, hoverGroup = \"menu-item\", ...props }, ref) => (\n <span\n ref={ref}\n className={cn(\n \"h-[1lh] shrink-0 ml-auto flex items-center gap-2\",\n hoverReveal && SUFFIX_HOVER_REVEAL_BY_GROUP[hoverGroup],\n className\n )}\n {...props}\n />\n )\n)\nItemSuffix.displayName = \"ItemSuffix\"\n"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"item-anatomy.js","sources":["../../../src/patterns/element-anatomy/item-anatomy.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 SSOT for Family 1+2 row primitives + all item-anatomy helpers(ItemContent / ItemIcon / ItemPrefix / ItemSuffix / ItemInlineAction 等),拆檔會讓 primitive 跨檔 import 滿天飛\nimport * as React from \"react\"\nimport type { LucideIcon } from \"lucide-react\"\nimport { cva } from \"class-variance-authority\"\nimport { cn, lineClampClass } from \"@/lib/utils\"\nimport { Avatar, type AvatarProps } from \"@/design-system/components/Avatar/avatar\"\nimport {\n Tooltip,\n TooltipTrigger,\n TooltipContent,\n} from \"@/design-system/components/Tooltip/tooltip\"\nimport { useTruncated } from \"@/design-system/hooks/use-truncated\"\n\n// ── InlineActionConfig ─────────────────────────────────────────────────────\n// 宣告式 API:consumer 只宣告 intent,host 根據 size tier 自動渲染。\n// 跨 Family 消費(Field family + Row family),SSOT 住 patterns/element-anatomy/\n// 對齊 inline-action.spec.md「API 設計」節。\n//\n// Canonical 實作:`ItemInlineAction`(本檔內)。\nexport interface InlineActionConfig {\n icon: LucideIcon\n /** aria-label,同時作為 tooltip 來源 */\n label: string\n /**\n * 點擊 handler。可選接收 React 事件物件——有需要時可呼叫 `stopPropagation()` 避免\n * 事件冒泡到父層(例如 Select 清除按鈕在 popover trigger 內,不想觸發 popover open)。\n */\n onClick: (e?: React.MouseEvent<HTMLButtonElement>) => void\n}\n\n/**\n * Item Layout — 共用工具與常數\n *\n * 這個檔案不是元件,是 `item-anatomy.spec.md` 的 code-level 實作。\n * 所有 row primitives(`MenuItem` / `TreeItem` / `SidebarMenuButton` /\n * `SelectionItem`)**必須 import 本 module 的 ICON_SIZE 和 helpers**,\n * 不要各自複製常數或 wrapper className——單一 source of truth,避免不一致。\n *\n * 詳細規則見 `item-anatomy.spec.md`。\n */\n\n// ── Types ──────────────────────────────────────────────────────────────────\n\n/** Row primitive 的 size 變體,對齊 `--field-height-*` token family */\nexport type RowSize = \"sm\" | \"md\" | \"lg\"\n\n// ── Constants ──────────────────────────────────────────────────────────────\n\n/**\n * Icon 尺寸(px)per row size。\n *\n * - sm / md: 16px(標準 row icon)\n * - lg: 20px(對齊 text-body-lg 的視覺重量)\n *\n * **用法**:透過 Lucide icon 的 `size` prop 直接傳入,不要用 CSS `[&>svg]:size-*`\n * selector——當 icon 被包在 `<ItemPrefix>` 的 `h-[1lh]` wrapper 內時,\n * `>` 直接子選擇器會失效,Lucide icon 會 fallback 到預設 24px。\n *\n * ```tsx\n * import { ICON_SIZE } from \"@/design-system/patterns/element-anatomy/item-anatomy\"\n *\n * const iconPx = ICON_SIZE[size] // size: RowSize\n * <MyLucideIcon size={iconPx} />\n * ```\n */\nexport const ICON_SIZE: Record<RowSize, number> = {\n sm: 16,\n md: 16,\n lg: 20,\n}\n\n/**\n * Avatar 尺寸(px)per row size,分 inline / block 兩種模式。\n *\n * - **inline**(無 description,大宗情境)→ 對齊第一行 label,用 `h-[1lh]` 容器\n * sm: 20, md: 24, lg: 24\n * - **block**(有 description,avatar 跨越 label + desc 兩行)→ 對齊文字塊中心\n * sm: 32, md: 32, lg: 40\n *\n * **用法**:row context 的 consumer(包含 `asChild` pattern 的 Sidebar / Tree\n * consumer)**必須**用 `<ItemAvatar>` / `<ItemIcon>` helper 元件(自動查\n * RowSizeContext 對表),**禁止** `import { AVATAR_SIZE }` 手動查表、**不可硬寫\n * `size={24}`** — 硬寫會讓 sm 變體的 avatar 尺寸錯誤、跟 ICON_SIZE 對齊規則脫鉤。\n * 詳 item-anatomy.spec.md「Helper 程式化封裝 + review 防線」段。\n *\n * ```tsx\n * import { ItemAvatar } from \"@/design-system/patterns/element-anatomy/item-anatomy\"\n *\n * <ItemAvatar alt=\"Alan\" color=\"blue\" /> // 自動查 AVATAR_SIZE.inline[rowSize]\n * ```\n *\n * Canonical 消費者:MenuItem 直接 import 本檔 `AVATAR_SIZE`(menu-item.tsx,消費 .block/.inline;re-export 已完成,MenuItem 無內部查表)。\n */\nexport const AVATAR_SIZE = {\n inline: { sm: 20, md: 24, lg: 24 },\n block: { sm: 32, md: 32, lg: 40 },\n} as const satisfies Record<\"inline\" | \"block\", Record<RowSize, number>>\n\n/**\n * 回傳 uniform prefix slot 的 inline style(CSS variable form)。\n *\n * 用法:Row-primitive 頂層容器(例如 `<SidebarProvider uniformPrefix>` 的 wrapper)\n * 把這個 style 套在 `style` prop 上(預先放在 `--mixed-prefix-slot` 候選位置,\n * 由外層 CSS `:has()` 條件化套到 `--item-prefix-slot`),子樹內所有 `<ItemPrefix>` /\n * `<ItemIcon>` / `<ItemAvatar>` 會自動用固定寬槽對齊。\n *\n * 槽寬 = `AVATAR_SIZE.inline[size]`(20/24/24 @ sm/md/lg)——以最大的 inline\n * prefix(avatar)為基準,icon 在槽內 `justify-center`。\n *\n * ```tsx\n * <ul style={getUniformPrefixSlotStyle(size)}>\n * <ItemIcon icon={Folder} /> ← 16px icon 在 24px 槽內置中\n * <ItemAvatar alt=\"GitHub\" /> ← 24px logo 填滿槽\n * </ul>\n * ```\n */\nexport function getUniformPrefixSlotStyle(size: RowSize): React.CSSProperties {\n return { \"--item-prefix-slot\": `${AVATAR_SIZE.inline[size]}px` } as React.CSSProperties\n}\n\n/**\n * Inline action(suffix 裡的 hover-action icon button)hover 背景尺寸(px)。\n * 規則:icon size + 2px(每邊 +1px,用 absolute positioning 溢出不影響排版)。\n * 對齊 `inline-action.spec.md`(canonical SSOT,2026-04-24 自 item-anatomy.spec 抽出)。\n */\n// code-quality-allow: dead-export — public constant — DS API surface,consumer 可引(即使當前 internal-only)\nexport const INLINE_ACTION_HOVER_BG_SIZE: Record<RowSize, number> = {\n sm: 18,\n md: 18,\n lg: 22,\n}\n\n/**\n * `ROW_PADDING_BY_SIZE` — Family 1+2 row primitive 的 size → padding + typography 公式 SSOT\n *\n * Formula(canonical):`py = (field-height - 1lh) / 2`,對齊 1em text 到 field-height center。\n *\n * **消費者**:menuItemVariants / sidebarMenuButtonVariants / treeItemVariants / 未來 row primitive\n * **Rationale**:前先 3 cva 各自寫同一公式 → drift risk(item-anatomy.spec 有明文「SidebarMenuButton\n * 獨立實作風險」)。抽本 export 後改**一處全同步**,risk 消失(對齊 Meta-Pattern M17「SSOT 必可傳播」)。\n *\n * 2026-04-24 D Phase 1+2 consolidation 建立。\n */\nexport const ROW_PADDING_BY_SIZE: Record<RowSize, string> = {\n sm: 'text-body leading-compact py-[calc((var(--field-height-sm)-1lh)/2)]',\n md: 'text-body leading-compact py-[calc((var(--field-height-md)-1lh)/2)]',\n lg: 'text-body-lg leading-compact py-[calc((var(--field-height-lg)-1lh)/2)]',\n}\n\n// ── Helper components ──────────────────────────────────────────────────────\n\n/**\n * `<ItemPrefix>` — Row primitive 的 prefix slot wrapper。\n *\n * 把 prefix(icon / avatar / checkbox / indicator)包進 `h-[1lh]` 容器,\n * 強制對齊第一行文字中線。這是 item-layout 的底層規則(詳見 spec「Prefix 垂直對齊」)。\n *\n * **為什麼**:\n * - Row primitive 的 outer flex 是 `items-start`(多行 label 時 prefix 不飄移)\n * - 單行時 prefix 透過這個 wrapper 強制對齊第一行中線,視覺效果 = `items-center`\n * - 不同尺寸的 prefix(icon 16、Avatar 24、Checkbox 20)都能穩定置中\n *\n * 用法:\n * ```tsx\n * <div className=\"flex items-start gap-2\">\n * <ItemPrefix>\n * <Avatar size={24} />\n * </ItemPrefix>\n * <span className=\"min-w-0 flex-1 truncate\">{label}</span>\n * </div>\n * ```\n *\n * asChild 的 consumer(例如 `<SidebarMenuButton asChild>`)**必須自己**用這個 wrapper\n * 包 prefix,不能省略。\n */\nexport const ItemPrefix = React.forwardRef<\n HTMLSpanElement,\n React.HTMLAttributes<HTMLSpanElement>\n>(({ className, ...props }, ref) => (\n <span\n ref={ref}\n className={cn(\n \"h-[1lh] shrink-0 flex items-center\",\n // Uniform prefix slot:讀取 `--item-prefix-slot` CSS variable,\n // fallback `auto`(= 預設行為,不佔額外寬度、不 center)。\n // Row-primitive 頂層容器(例如 `<SidebarProvider uniformPrefix>` 的 wrapper)\n // 若設定這個 var,小尺寸 prefix(icon 16px)會在固定寬槽(24px @ md)內置中,\n // 與大尺寸 prefix(app logo 24px)對齊,達成 label 齊左。\n // 詳見 item-anatomy.spec.md「Uniform prefix slot」節。\n \"min-w-[var(--item-prefix-slot,auto)] justify-center\",\n className\n )}\n {...props}\n />\n))\nItemPrefix.displayName = \"ItemPrefix\"\n\n/**\n * `itemPrefixAlignVariants` — Prefix 對齊高度的 cva SSOT(for 複雜 prefix 結構)。\n *\n * ── 存在的理由 ──\n * `<ItemPrefix>` primitive 預設單一 child(icon / avatar 任一)+ inline 對齊。\n * 但**複雜 prefix 結構**(e.g. MenuItem 可同時含 checkbox + icon/avatar 多 child)\n * 或**block mode**(大 prefix + description 對齊塊中心)需要自建 wrapper cva。\n * 本 cva 把 block formula SSOT 化,讓 MenuItem 等 consumer 共用,\n * 改值一處(token or cva)→ 所有 block-prefix consumer 同步。\n *\n * ── Align 變體(對齊 item-anatomy.spec.md「24px 閾值對齊規則」) ──\n * - `\"inline\"`:`h-[1lh]`,小 prefix(≤ 24px)對齊 label 第一行\n * - `\"block-sm\"` / `\"block-md\"`:大 prefix + scanning desc(sm/md menu),\n * 高度 = label(1lh) + gap(token) + desc(caption × 1.3)\n * - `\"block-lg\"`:大 prefix + scanning desc(lg menu),desc 改為 body × 1.3\n *\n * ── Consumer ──\n * MenuItem(inline / block-sm / block-md / block-lg)。其他 row primitive 簡單情境\n * 直接用 `<ItemPrefix>` 就好,不需此 cva。\n *\n * ── SSOT 傳播 ──\n * gap 用 `var(--item-gap-label-desc-scanning[-lg])` token(bare 變體已不存在,4 個 mode×size 變體);font-size 用 token-awareness\n * (`var(--font-caption-size)` / `var(--font-body-size)`)。改 token → 公式同步。\n */\n// code-quality-allow: long-function — cva variant/styles table — 拆 fn 會失去 type inference + 跨 fn 傳 config 反而難讀\nexport const itemPrefixAlignVariants = cva(\n \"flex items-center gap-2 shrink-0\",\n {\n variants: {\n align: {\n inline: \"h-[1lh]\",\n // sm/md desc = text-caption (12px × 1.3 = 15.6px)\n // block-sm / block-md:scanning mode(body + caption 組合)→ 用 scanning mode 專屬 gap token\n \"block-sm\":\n \"h-[calc(1lh+var(--item-gap-label-desc-scanning)+var(--font-caption-size)*1.3)]\",\n \"block-md\":\n \"h-[calc(1lh+var(--item-gap-label-desc-scanning)+var(--font-caption-size)*1.3)]\",\n // block-lg:scanning-lg mode(body-lg + body compact 組合)→ 用 scanning-lg mode 專屬 gap token\n \"block-lg\":\n \"h-[calc(1lh+var(--item-gap-label-desc-scanning-lg)+var(--font-body-size)*1.3)]\",\n },\n },\n defaultVariants: {\n align: \"inline\",\n },\n }\n)\n\n// ── 單行截斷偵測 measure(ItemLabel / ItemContent 共用)──────────────────────\n// 截斷 → tooltip 顯完整內容、僅實際截斷時顯示(owner:components/Tooltip/tooltip.spec.md:32;\n// 引擎 SSOT:patterns/element-anatomy/truncated-text.spec.md)。不消費 `<TruncatedText>`——\n// trigger 需自控(ItemLabel 要 forwardRef + data-sidebar attr + props spread;ItemContent 要保留\n// labelTruncate / clamp className 原樣)→ 走 spec 指定解:public `useTruncated` 自組\n// (同 components/InlineEdit/inline-edit.tsx「單行純值 view 截斷 tooltip」段與 components/Tag 的用法)。\n// 兩種「單行截斷」機制都要偵測(MenuItem 的 clamp 走 className escape,量 DOM 才能全蓋):\n// - `truncate`(nowrap)→ scrollWidth 溢出(useTruncated 預設量法)\n// - `line-clamp-1`(MenuItem labelMaxLines/descMaxLines=1、descriptionClamp=1;文字先 wrap\n// 再垂直裁)→ scrollWidth 不溢出,只能量 scrollHeight;以「可視高度 ≈ 1 行」gate 限定單行\n// clamp。多行 clamp(≥2 行)刻意不顯 tooltip:truncated-text.spec.md「何時不用」(a) 明文\n// 排除多行,DS 尚無多行截斷↔補全 canonical(owner 決策前維持排除)—— gate 機械保證。\nconst measureSingleLineClipped = (el: HTMLElement): boolean => {\n if (el.scrollWidth > el.clientWidth) return true\n const lineHeight = parseFloat(getComputedStyle(el).lineHeight)\n if (Number.isNaN(lineHeight)) return false // line-height:normal 等不可量 → 只認橫向溢出\n const isSingleVisualLine = el.clientHeight < lineHeight * 1.5\n return isSingleVisualLine && el.scrollHeight > el.clientHeight + 1\n}\n\n/**\n * `<ItemLabel>` — Row primitive 的 label span。\n *\n * 預設 `min-w-0 flex-1 truncate`——單行截斷,佔滿剩餘 flex 空間。\n * 截斷時 hover 顯 tooltip 補全(截斷才顯,tooltip.spec.md:32;內建,consumer 免自接)。\n * 加 `data-sidebar=\"menu-label\"` attribute 讓 sidebar icon 模式的 CSS selector 能命中。\n *\n * 用法:\n * ```tsx\n * <ItemLabel>{label}</ItemLabel>\n * ```\n */\nexport const ItemLabel = React.forwardRef<\n HTMLSpanElement,\n React.HTMLAttributes<HTMLSpanElement>\n>(({ className, children, ...props }, forwardedRef) => {\n const { ref: truncationRef, isTruncated } = useTruncated<HTMLSpanElement>({\n measure: measureSingleLineClipped,\n deps: [children],\n })\n return (\n // 永遠 wrap + `open` 控制(truncated-text.spec.md canonical:條件 wrap 會 remount span、\n // ref 對不上真 DOM)。hover-only:trigger = 不可聚焦 span,不加 tabIndex —— menu row 的\n // roving focus 在宿主 row 上,label 加 tab stop 會破壞 APG menu 鍵盤導航;先例 = Tag 帶\n // 內建截斷 tooltip 渲染於 SelectMenu option 內(select.tsx renderLabel → tag.tsx),\n // menu 內無抑制。鍵盤/SR 無損失:row accessible name 本就含完整文字。\n <Tooltip open={isTruncated ? undefined : false}>\n <TooltipTrigger asChild>\n <span\n ref={(el) => {\n truncationRef.current = el\n if (typeof forwardedRef === \"function\") forwardedRef(el)\n else if (forwardedRef) forwardedRef.current = el\n }}\n data-sidebar=\"menu-label\"\n className={cn(\"min-w-0 flex-1 truncate\", className)}\n {...props}\n >\n {children}\n </span>\n </TooltipTrigger>\n <TooltipContent>{children}</TooltipContent>\n </Tooltip>\n )\n})\nItemLabel.displayName = \"ItemLabel\"\n\n/**\n * `<ItemContent>` — Row primitive 的 label + optional description 內容區(SSOT)。\n *\n * ── 存在的唯一理由 ──\n * 封裝「flex-col + label + description + canonical label/description gap」結構,\n * 避免 13+ 消費者各自 hard-code `mt-0.5`。改 token 一處,全 DS 同步。\n *\n * ── 截斷 → tooltip(內建,consumer 免自接)──\n * label(`truncate` 或 `line-clamp-1` escape)與單行截斷 description 在實際截斷時 hover 顯\n * tooltip 補全(截斷才顯,tooltip.spec.md:32);多行 clamp(≥2 行)排除 —— 見\n * `measureSingleLineClipped` 註解(truncated-text.spec.md「何時不用」(a))。\n *\n * ── Consumer 偏離 canonical ──\n * 消費端若有**確切合理理由**不用 `<ItemContent>` / 需自訂 label/desc 行為,必在\n * 該元件 `spec.md` 明文寫下 rationale(對齊 item-anatomy canonical「偏離必明文」)。\n * 合法偏離範例:\n * - MenuItem 的 `leading-compact + text-caption` scanning-mode typography\n * → MenuItem spec 明文 rationale + 消費 `--item-gap-label-desc-scanning[-lg]` token 直接用\n * - SelectionItem 的 control slot 跟 block formula 綁定\n * → SelectionItem spec 明文 rationale\n *\n * ── Props ──\n * - `label`:label 內容(ReactNode,必填)\n * - `description`:description 內容(ReactNode,optional)\n * - `mode`:typography tier(scanning vs reading,世界級對齊 Material `dense` / Carbon `size` / Ant `size`)\n * - `\"reading\"`(預設):繼承 parent `text-body`(14px / 1.5 leading),舒適閱讀(Family 2 / Material body-large 派)\n * - `\"scanning\"`:desc 縮為 `text-caption`(12px)+ `leading-compact`(1.3),緊湊掃視\n * (Family 1 Menu item / Material body-medium dense 派)\n * - `descriptionTone`:desc 顏色語意\n * - `\"secondary\"`(預設):`text-fg-secondary`\n * - `\"error\"`:`text-error-text`\n * - `\"muted\"`:`text-fg-muted`\n * - `\"disabled\"`:`text-fg-disabled`(對齊 M24 state precedence:disabled > muted > emphasis)\n * - `descriptionWrap`:desc 多行 wrap(預設 true)/ false = truncate\n * - `labelClassName` / `descriptionClassName`:escape hatches(明文 rationale 才用)\n *\n * ── World-class benchmark ──\n * 6 家 DS(Material / Polaris / Atlassian / Apple HIG / Carbon / Ant)一致用\n * 14/20 vs 16/24 兩擋 body 表達 scanning vs reading。API 兩派:(A)密度 prop\n * Material `dense` / Carbon `size` / Ant `size`(本 DS 採 A 派);(B)typography\n * token 手選 Polaris / Atlassian / Apple。\n */\n\nexport interface ItemContentProps extends Omit<React.HTMLAttributes<HTMLDivElement>, \"title\"> {\n label: React.ReactNode\n description?: React.ReactNode\n /**\n * Typography mode — **leading 行為**(跟 size 正交的另一維):\n * - `\"reading\"`(預設):default leading(1.5)— Family 2 List item 舒適閱讀\n * - `\"scanning\"`:`leading-compact`(1.3)+ desc 降一 tier —— Menu item / Step 掃視\n */\n mode?: \"reading\" | \"scanning\"\n /**\n * Size tier — **label 字級**(跟 mode 正交的另一維):\n * - `\"md\"`(預設):label `text-body`(14)\n * - `\"lg\"`:label `text-body-lg`(16)— desc 永遠比 label 小一 tier(body)\n *\n * 2 × 2 = 4 typography 組合,對應 4 tokens:\n * - md + reading → body(14/1.5) + body(14/1.5)\n * - md + scanning → body(14/1.3) + caption(12/1.3)\n * - lg + reading → body-lg(16/1.5) + body(14/1.5)\n * - lg + scanning → body-lg(16/1.3) + body(14/1.3)\n */\n size?: \"md\" | \"lg\"\n descriptionTone?: \"secondary\" | \"error\" | \"muted\" | \"disabled\"\n descriptionWrap?: boolean\n /**\n * Description 多行截斷(line-clamp-N)。undefined = 不 clamp(自由 wrap)。\n * Tailwind line-clamp utilities 支援 1-6。\n * `1`(單行 clamp)截斷時 hover 顯 tooltip 補全(截斷才顯,tooltip.spec.md:32);\n * `>= 2`(多行)不配 tooltip —— truncated-text.spec.md「何時不用」(a) 排除多行,\n * DS 尚無多行截斷↔補全 canonical(owner 決策前維持排除)。\n */\n descriptionClamp?: number\n /**\n * Description `break-words`(default `false`,僅在 consumer 需要時 opt-in)。\n * 使用情境:SelectionItem / Steps 這類 description 可能塞長英文 token 需 break。\n */\n descriptionBreakWords?: boolean\n /**\n * Label truncate (default `true`, 對齊 row-item 大宗 idiom)。\n * Opt-out(`false`)for card-like consumers(e.g. ProfileCard label 允許 wrap)。\n * World-class 對照:Material `ListItemText primaryTypographyProps.noWrap` default true。\n */\n labelTruncate?: boolean\n labelClassName?: string\n descriptionClassName?: string\n}\n\n// code-quality-allow: long-function — foundational composite main body — 拆 sub-fn 會複雜化 local state / ref / context binding\nexport const ItemContent = React.forwardRef<HTMLDivElement, ItemContentProps>(\n (\n {\n label,\n description,\n mode = \"reading\",\n size = \"md\",\n descriptionTone = \"secondary\",\n descriptionWrap = true,\n descriptionClamp,\n descriptionBreakWords = false,\n labelTruncate = true,\n labelClassName,\n descriptionClassName,\n className,\n ...props\n },\n ref,\n ) => {\n const toneClass = {\n secondary: \"text-fg-secondary\",\n error: \"text-error-text\",\n muted: \"text-fg-muted\",\n disabled: \"text-fg-disabled\",\n }[descriptionTone]\n\n // Typography desc class — 2 維 cross-product (mode × size):\n // ┌───────────┬────────────────────────────┬────────────────────────────┐\n // │ │ size=\"md\"(body label 14) │ size=\"lg\"(body-lg label 16)│\n // ├───────────┼────────────────────────────┼────────────────────────────┤\n // │ reading │ 繼承 body(14/1.5) │ body(14)default leading │\n // │ scanning │ caption(12)+ compact │ body(14)+ compact │\n // └───────────┴────────────────────────────┴────────────────────────────┘\n const isLg = size === \"lg\"\n const modeClass =\n mode === \"scanning\"\n ? isLg\n ? \"text-[length:var(--font-body-size)] leading-compact\"\n : \"text-[length:var(--font-caption-size)] leading-compact\"\n : isLg\n ? \"text-[length:var(--font-body-size)]\"\n : \"\"\n\n const clampClass = lineClampClass(descriptionClamp) ?? \"\"\n\n // 截斷 → tooltip(截斷才顯,tooltip.spec.md:32)。量測走 measureSingleLineClipped(檔內\n // SSOT 註解):label 蓋 labelTruncate `truncate` + MenuItem labelClassName `line-clamp-1`\n // escape 兩路;description 只在單行截斷(`descriptionClamp === 1` 或 !descriptionWrap 的\n // truncate 分支,含 className escape)顯示——多行 clamp(≥2)由量測 gate 機械排除\n // (truncated-text.spec.md「何時不用」(a):本 primitive 不處理多行)。\n const { ref: labelRef, isTruncated: labelClipped } = useTruncated<HTMLSpanElement>({\n measure: measureSingleLineClipped,\n deps: [label],\n })\n const { ref: descriptionRef, isTruncated: descriptionClipped } = useTruncated<HTMLSpanElement>({\n measure: measureSingleLineClipped,\n deps: [description],\n })\n\n return (\n <div\n ref={ref}\n className={cn(\"flex flex-col min-w-0 flex-1\", className)}\n {...props}\n >\n {/* 永遠 wrap + open 控制(truncated-text.spec.md canonical);hover-only 不加 tabIndex\n (menu row roving focus 見 ItemLabel 註解)。 */}\n <Tooltip open={labelClipped ? undefined : false}>\n <TooltipTrigger asChild>\n <span ref={labelRef} className={cn(labelTruncate && \"truncate\", labelClassName)}>\n {label}\n </span>\n </TooltipTrigger>\n <TooltipContent>{label}</TooltipContent>\n </Tooltip>\n {description && (\n <Tooltip open={descriptionClipped ? undefined : false}>\n <TooltipTrigger asChild>\n <span\n ref={descriptionRef}\n className={cn(\n // Typography-mode-aware gap token(2026-04-23):\n // (mode, size)2 維正交 → 4 token 對應 4 typography 組合\n mode === \"scanning\"\n ? isLg\n ? \"mt-[var(--item-gap-label-desc-scanning-lg)]\"\n : \"mt-[var(--item-gap-label-desc-scanning)]\"\n : isLg\n ? \"mt-[var(--item-gap-label-desc-reading-lg)]\"\n : \"mt-[var(--item-gap-label-desc-reading)]\",\n modeClass,\n toneClass,\n clampClass,\n descriptionBreakWords && \"break-words\",\n !descriptionWrap && \"truncate\",\n descriptionClassName,\n )}\n >\n {description}\n </span>\n </TooltipTrigger>\n <TooltipContent>{description}</TooltipContent>\n </Tooltip>\n )}\n </div>\n )\n },\n)\nItemContent.displayName = \"ItemContent\"\n\n// ── Row size context ──────────────────────────────────────────────────────\n//\n// 作用:讓 row primitive(Sidebar / SelectMenu / Tree / DropdownMenu)只 propagate\n// 一次 size,整個子樹內的 <ItemIcon> / <ItemAvatar> 都自動查到正確尺寸。\n//\n// **存在的唯一理由**:消除 asChild pattern 下 consumer 硬寫 `size={24}` 的可能性。\n// 過去 consumer 必須 `useSidebar().size` 再手動傳 `AVATAR_SIZE.inline[size]`,\n// 每個 consumer 都是一次漂移機會(曾發生:sm 欄 avatar 顯示 24 而非 20,破壞規格)。\n// 現在 consumer 只寫 `<ItemAvatar alt=\"...\" />`,尺寸由 context 自動決定。\n//\n// Row primitive 實作者的責任:在元件內部用 `<RowSizeProvider value={size}>` 包裹\n// children(不論 asChild 或非 asChild 路徑),這樣任何 descendant 的 ItemIcon /\n// ItemAvatar 都會拿到正確尺寸。\n\nconst RowSizeContext = React.createContext<RowSize | null>(null)\n\n/** @internal — L3 row-size primitive;app code 不直接 import,由 MenuItem/Row/Field 等內部消費(inline-action.spec.md)。 */\nexport const RowSizeProvider = RowSizeContext.Provider\n\n/**\n * 讀取當前 row size。在 row primitive 子樹外呼叫會回 fallback(預設 \"md\")。\n * Row primitive 實作者不該呼叫這個——你們已經知道自己的 size。\n * 這個 hook 是給 `<ItemIcon>` / `<ItemAvatar>` 和 asChild consumer 的逃生艙用。\n */\nexport function useRowSize(fallback: RowSize = \"md\"): RowSize {\n return React.useContext(RowSizeContext) ?? fallback\n}\n\n// ── Prefab prefix elements ────────────────────────────────────────────────\n//\n// `<ItemIcon>` 和 `<ItemAvatar>` 是「prefix slot 的完成品元件」——自帶 ItemPrefix\n// wrapper + 自動查 ICON_SIZE / AVATAR_SIZE。\n//\n// **asChild consumer 必須用這兩個元件,禁止手動寫 `<Avatar size={N} />`**。\n// 禁令理由見 item-anatomy.spec.md「Avatar 尺寸選擇」節。\n\nexport interface ItemIconProps\n extends Omit<React.SVGAttributes<SVGSVGElement>, \"children\"> {\n /** Lucide icon 元件(不是 JSX element) */\n icon: LucideIcon\n}\n\n/**\n * `<ItemIcon icon={Folder} />` — 自動查 `ICON_SIZE[rowSize]`,包在 `ItemPrefix` 內。\n * 永遠對齊第一行 label(item-layout 的 icon 永遠 inline)。\n */\nexport const ItemIcon = React.forwardRef<SVGSVGElement, ItemIconProps>(\n ({ icon: Icon, className, ...props }, ref) => {\n const size = useRowSize()\n return (\n // data-prefix-type=\"icon\" 讓 row-primitive 容器的 `:has()` selector 能偵測類型,\n // 用於 `<SidebarProvider uniformPrefix>` opt-in 後的混用 auto-detection\n <ItemPrefix data-prefix-type=\"icon\">\n <Icon\n ref={ref}\n size={ICON_SIZE[size]}\n className={cn(\"shrink-0\", className)}\n aria-hidden\n {...props}\n />\n </ItemPrefix>\n )\n }\n)\nItemIcon.displayName = \"ItemIcon\"\n\nexport interface ItemAvatarProps extends Omit<AvatarProps, \"size\"> {\n /**\n * 對齊模式(選填,預設 `\"inline\"`):\n * - `\"inline\"`:視覺重量輕的 avatar,對齊第一行 label(20/24/24 @ sm/md/lg)\n * - `\"block\"`:視覺重量重的 avatar,對齊 label + description 文字塊(32/32/40 @ sm/md/lg)\n *\n * 預設 inline——大宗情境(扁平 row、footer user、主清單)都用這個。\n * 只有 avatar 是 item 主體(人物卡、顯著身份辨識)才用 block。\n *\n * 詳見 `item-anatomy.spec.md`「Avatar 尺寸選擇」節,含 24px 對齊模式閾值規則。\n */\n mode?: \"inline\" | \"block\"\n}\n\n/**\n * `<ItemAvatar alt=\"Alan\" color=\"blue\" />` — 自動查 `AVATAR_SIZE[mode][rowSize]`,\n * 包在 `ItemPrefix` 內。\n *\n * asChild consumer 的標準用法——不需知道 size,不需 import AVATAR_SIZE,\n * 不可能硬寫錯誤尺寸。\n */\n// code-quality-allow: long-function — forwardRef body 含 size×color×hoverCard 三正交 prop 渲染分流,拆出 sub-fn 反增 prop drilling\nexport const ItemAvatar = React.forwardRef<HTMLDivElement, ItemAvatarProps>(\n ({ mode = \"inline\", ...props }, ref) => {\n const size = useRowSize()\n return (\n // data-prefix-type=\"avatar\" 讓 row-primitive 容器的 `:has()` selector 能偵測類型,\n // 用於 `<SidebarProvider uniformPrefix>` opt-in 後的混用 auto-detection\n <ItemPrefix data-prefix-type=\"avatar\">\n <Avatar ref={ref} size={AVATAR_SIZE[mode][size]} {...props} />\n </ItemPrefix>\n )\n }\n)\nItemAvatar.displayName = \"ItemAvatar\"\n\n// ── Inline action (suffix slot) ───────────────────────────────────────────\n//\n// 共用 row-primitive 的 suffix inline action 渲染——消除過去 Input / NumberInput /\n// Tag / LinkInput / Combobox 各自複製 18 行 JSX 的狀況(見 `inline-action.spec.md`)。\n//\n// 規格(完全對齊 inline-action.spec.md — canonical SSOT,2026-04-24 自 item-anatomy.spec 抽出):\n// - Icon 尺寸 = ICON_SIZE[rowSize] (16/16/20)\n// - Hover bg 尺寸 = icon + 2 (18/18/22,INLINE_ACTION_HOVER_BG_SIZE)\n// - Hover bg 圓角 = rounded-md (sm/md)/ rounded-md (lg)\n// - Icon 顏色:fg-muted → fg-secondary on hover/active\n// - 必須 <button type=\"button\">,aria-label = label,Tooltip(同 label)\n// - cursor-pointer\n\n// ── Low-level:ItemInlineActionButton ──\n//\n// Root 是 `<button>`(不是 Tooltip wrapper),可直接塞進 Radix `asChild` pattern\n// 例如 `<Collapsible.Trigger asChild>`, `<Popover.Trigger asChild>` 等。\n//\n// 自動從 `RowSizeContext` 查:\n// - 寬高 = ICON_SIZE[size]\n// - Hover bg 尺寸 = INLINE_ACTION_HOVER_BG_SIZE[size]\n// - Hover bg 圓角 = rounded-md (sm/md) / rounded-md (lg)\n// - Icon color = fg-muted → fg-secondary on hover/active\n//\n// 接受任何 button props 並 spread(讓 Radix Slot 可以 merge onClick / data-state 等)\n\n/** @internal — internal ItemInlineActionButton 的 props;隨其元件同走 subpath,不進 root barrel front-door。 */\nexport interface ItemInlineActionButtonProps\n extends Omit<React.ButtonHTMLAttributes<HTMLButtonElement>, \"children\"> {\n /** Lucide icon 元件 */\n icon: LucideIcon\n /** 可選:額外加在 icon 上的 className(例如 rotate transition) */\n iconClassName?: string\n /**\n * Size 明確覆寫(用於 fields / Tag 等**不在 `RowSizeProvider` 樹內**的 host):\n * 傳了就以此為準,否則從 `RowSizeContext` 讀(fallback \"md\")。\n *\n * 使用時機:\n * - Row primitive 子樹(Sidebar / TreeView / SelectMenu / DropdownMenu)→ **不傳**,吃 context\n * - Field / Tag(自有 size 系統,沒包 RowSizeProvider)→ **必傳** `size={hostSize}`\n */\n size?: RowSize\n /**\n * Hover / active bg className override(chromatic host 專用,如 Tag solid variant)。\n *\n * 預設 `group-hover/action:bg-neutral-hover group-active/action:bg-neutral-active`。\n * 提供後**完全取代** hover + active 雙態 className,但**不影響** rest 態(永遠 bg-transparent)\n * 與 overlay-trigger 態(`group-data-[state=open]/action:bg-neutral-hover` — 維持 host hover bg)。\n *\n * Consumer 須**同時**包含 hover + active 兩態 selector,例:\n * `'group-hover/action:bg-[var(--my-hover)] group-active/action:bg-[var(--my-active)]'`\n *\n * Use case:Tag solid(blue/green/red 等)需 hover bg 跟 host 色相一致(非 neutral)。\n */\n hoverBgClassName?: string\n /**\n * 標記本 button 是 **overlay trigger**(DropdownMenu / Popover / Tooltip 透過 `asChild` 包覆)。\n *\n * `true` 時:Radix overlay open 期間(`data-state=\"open\"`)button 維持 host hover 樣式\n * — 視覺鎖,讓 user 追溯 floating panel 來源(對齊 shadcn / Radix Themes / Material)。\n *\n * `false` 時(default):無 data-state=open 樣式 — 適用 in-place 互動如 `Collapsible.Trigger`\n * (展開內容接在 trigger 下方,不需追溯)、drag handle、dismiss X 等。\n *\n * Canonical 詳 `inline-action.spec.md`「Overlay trigger canonical」段。\n *\n * @default false\n */\n overlayTrigger?: boolean\n}\n\n/** @internal — L3 inline-action primitive;app code 禁直接 import,經 MenuItem/Row endActions 消費(inline-action.spec.md)。 */\nexport const ItemInlineActionButton = React.forwardRef<\n HTMLButtonElement,\n ItemInlineActionButtonProps\n>(({ icon: Icon, className, iconClassName, style, type = \"button\", size: sizeProp, hoverBgClassName, overlayTrigger = false, ...rest }, ref) => {\n const contextSize = useRowSize()\n const size = sizeProp ?? contextSize\n const iconPx = ICON_SIZE[size]\n const hoverBgPx = INLINE_ACTION_HOVER_BG_SIZE[size]\n return (\n <button\n ref={ref}\n type={type}\n className={cn(\n // 焦點框往外(= 什麼都不寫,吃 styles/base.css 全域規則)。2026-09-10 逐站點實測翻案:\n // 26 個真實站點 × sm/md/lg,四周最小淨空 5–192px(最小值 4.00 = Breadcrumb 兩側的分隔符,\n // canonical「4.00 算放得下」),DPR2 逐像素數框帶 → 零裁切、零遮蓋。列不會裁到它:截斷是\n // label 自己的 `truncate`(兄弟節點),不是祖先。原註解「往外 +2px 實測上下各被裁 1px」\n // 只在 **Tag 宿主**成立(h-6 + 1px 邊框 + overflow-hidden → 淨空 3px,sm 1px),\n // 依 focus-canonical「同一個底層元件被另一個元件放進貼邊位置,往內由那個外層元件承擔」,\n // 往內的 class 搬到 `tag.tsx` TagDismiss,不由 primitive 整個翻內。\n \"group/action relative grid place-content-center shrink-0 cursor-pointer\",\n // 弱化 icon hover 階梯 SSOT(2026-07-30 user 拍板):fg-muted(neutral-7)→ 一階 fg-secondary\n // (neutral-8),不跳到 foreground(neutral-9)。inline-action.spec.md「Icon 色彩」+\n // tokens/color/semantic.css:53 為 rule owner;世界級對照 Fluent 2\n // (neutralForeground3Hover === neutralForeground2 值)/ MUI Chip deleteIcon(.26 → .4)。\n // hover 底色瞬間切換,不做過渡(user 2026-09-10 拍板「第三題改成全部瞬間」;SSOT = tokens/motion/motion.spec.md「hover 回饋不做過渡」)\n \"text-fg-muted hover:text-fg-secondary active:text-fg-secondary\",\n // Overlay trigger active state — 只在 consumer 顯式宣告 overlayTrigger=true 時生效\n // (canonical 2026-05-05:Collapsible / drag / dismiss 等 in-place 互動 ≠ overlay,\n // 不應 leak 視覺 lock。詳 inline-action.spec.md「Overlay trigger canonical」)\n overlayTrigger && \"data-[state=open]:text-fg-secondary\",\n // 2026-09-07:刪掉本地 focus-visible:outline-2 + outline-ring —— 與全域\n // styles/base.css:44-47 逐字等價(同 2px、同 --ring、offset 也吃全域的 2px),\n // 且本區塊無 outline-none,全域本來就生效。刪除為零視覺變化。\n className\n )}\n style={{ width: iconPx, height: iconPx, ...style }}\n {...rest}\n >\n <span\n aria-hidden\n className={cn(\n // ⚠️ 這裡**刻意沒有** `pointer-events-none`,日後不得以「它只是裝飾層」為由加回去。\n // 這塊底色比按鈕盒每邊大 1px(16→18,lg 20→22)。它是 <button> 的子節點,只要不擋指標,\n // 溢出的那 1px 會自己接住點擊並冒泡到 button —— 命中區於是恰好等於使用者看得到的那塊底色。\n // 依據:user 2026-09-24 裁示「重點是要讓 inline action 的可點擊範圍跟其 hover 底色一樣吧?\n // 都是 18*18」,並於同日對本檔本行確認「對,就這樣改」。跨元件 owner =\n // ds-canonical/references/hit-area-canonical.md(規則:懸停回饋的形狀 ≡ 命中區;圖示是內容,可比命中區小)。\n // 本 DS 以滑鼠精度為前提,不以觸控尺寸建議當依據把命中區撐大到超過懸停回饋(同日 user 裁示)。\n // 歷史:原本那行 `pointer-events-none` 沒有任何理由 —— 2026-04-01 7dd65fbf 隨首版誕生、\n // 2026-04-14 4e53ff99 逐字搬進本檔,兩次 commit message 與註解都沒出現 hit / pointer / 點擊 任何一個字。\n // 實測(改前):命中 16.75×16.75,右緣與下緣各短約 1.25px;側欄點在看得見的底色下緣會穿過去觸發整列導覽。\n \"absolute\",\n \"rounded-md\",\n \"bg-transparent\",\n hoverBgClassName ?? \"group-hover/action:bg-neutral-hover group-active/action:bg-neutral-active\",\n // Overlay 開啟 = 維持 host hover bg(只在 overlayTrigger=true 時生效)\n overlayTrigger && \"group-data-[state=open]/action:bg-neutral-hover\"\n )}\n style={{\n width: hoverBgPx,\n height: hoverBgPx,\n top: \"50%\",\n left: \"50%\",\n transform: \"translate(-50%, -50%)\",\n }}\n />\n <Icon size={iconPx} className={cn(\"relative\", iconClassName)} aria-hidden />\n </button>\n )\n})\nItemInlineActionButton.displayName = \"ItemInlineActionButton\"\n\n// ── High-level:ItemInlineAction ──\n//\n// 宣告式 API:consumer 給 `{ icon, label, onClick }`,元件自動處理 Tooltip +\n// aria-label。適用於 row primitive 的 suffix slot(SidebarMenuButton inlineActions 等)。\n//\n// 內部用 `ItemInlineActionButton`——兩層共用同一套視覺規格、同一個 RowSizeContext 查表。\n// 需要接 Radix asChild(例如 collapsible trigger)時改用 `ItemInlineActionButton`,\n// 那層沒有 Tooltip 包裝,Root 就是 button,可直接塞進 Radix Slot。\n\n/** @internal — internal ItemInlineAction 的 props;隨其元件同走 subpath,不進 root barrel front-door。 */\nexport interface ItemInlineActionProps {\n action: InlineActionConfig\n /** Size 覆寫(見 `ItemInlineActionButtonProps.size`)——Field Controls / Tag 必傳 */\n size?: RowSize\n}\n\n/** @internal — L3 inline-action primitive;app code 禁直接 import,經 row 元件消費(inline-action.spec.md)。 */\nexport const ItemInlineAction = React.forwardRef<\n HTMLButtonElement,\n ItemInlineActionProps\n>(({ action, size }, ref) => (\n <Tooltip>\n <TooltipTrigger asChild>\n <ItemInlineActionButton\n ref={ref}\n icon={action.icon}\n aria-label={action.label}\n onClick={action.onClick}\n size={size}\n />\n </TooltipTrigger>\n <TooltipContent>{action.label}</TooltipContent>\n </Tooltip>\n))\nItemInlineAction.displayName = \"ItemInlineAction\"\n\n// ── Item suffix slot ──────────────────────────────────────────────────────\n//\n// `<ItemSuffix>` — row primitive 的 suffix 容器。對齊 item-layout spec:\n// - `h-[1lh]`:suffix 永遠對齊第一行 label(跟 prefix 解耦)\n// - `ml-auto`:靠右\n// - `gap-2`:多個 inline action 之間的標準間距(inline-action.spec.md canonical SSOT)\n// - `hoverReveal` opt-in:opacity 0→1 on 父層 row hover/focus-visible —— **瞬間出現,不淡入**\n// (tokens/motion/motion.spec.md「hover 回饋不做過渡」;2026-09-26 延伸到滑過才出現的按鈕,待辦總帳 L9 / N4(3))\n//\n// `hoverGroup`(2026-05-05 v8 SSOT 升級):row primitive group selector 參數化。\n// 先前 `/menu-item` hardcode → TreeView(`/tree-item`)/ FileItem(`/row`)/\n// DataTable(`/row`)無法消費,被迫自刻 suffix slot(M1 違反)。\n// 參數化後 4 個 row primitive 都走同一條 SSOT。Tailwind JIT 需 static class,\n// 故用 lookup table 而非動態字串(避免 JIT scan 失敗)。\n\nconst SUFFIX_HOVER_REVEAL_BY_GROUP = {\n \"menu-item\":\n \"opacity-0 group-hover/menu-item:opacity-100 group-has-[:focus-visible]/menu-item:opacity-100\",\n \"tree-item\":\n \"opacity-0 group-hover/tree-item:opacity-100 group-has-[:focus-visible]/tree-item:opacity-100\",\n row: \"opacity-0 group-hover/row:opacity-100 group-has-[:focus-visible]/row:opacity-100\",\n} as const\n\nexport type ItemSuffixHoverGroup = keyof typeof SUFFIX_HOVER_REVEAL_BY_GROUP\n\nexport interface ItemSuffixProps extends React.HTMLAttributes<HTMLSpanElement> {\n /**\n * Hover-reveal:預設隱藏,父層 row hover / keyboard focus-visible 時才出現(瞬間,不淡入;tokens/motion/motion.spec.md)。\n * 對齊 TreeView / SidebarMenuButton inline action 行為。預設 false(永遠顯示,如 Badge)。\n *\n * 用 `group-has-[:focus-visible]` 而非 `group-focus-within`——後者會被 mouse click 觸發,\n * 導致 click 後 suffix 永久顯示直到焦點移走。focus-visible 只在鍵盤 tab 時啟動。\n */\n hoverReveal?: boolean\n /**\n * 父層 row 的 group selector(consumer 必加 `group/<name>` 到 row wrapper)。\n * 預設 `'menu-item'`(對齊 MenuItem / Sidebar)。其他 row primitive:\n * `'tree-item'`(TreeView)/ `'row'`(DataTable / FileItem)。\n *\n * 加新 row primitive(且需 hover-reveal)時:在 `SUFFIX_HOVER_REVEAL_BY_GROUP`\n * 加 entry,Tailwind JIT 才能 scan 到 static class。\n */\n hoverGroup?: ItemSuffixHoverGroup\n}\n\nexport const ItemSuffix = React.forwardRef<HTMLSpanElement, ItemSuffixProps>(\n ({ className, hoverReveal = false, hoverGroup = \"menu-item\", ...props }, ref) => (\n <span\n ref={ref}\n className={cn(\n \"h-[1lh] shrink-0 ml-auto flex items-center gap-2\",\n hoverReveal && SUFFIX_HOVER_REVEAL_BY_GROUP[hoverGroup],\n className\n )}\n {...props}\n />\n )\n)\nItemSuffix.displayName = \"ItemSuffix\"\n"],"names":[],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"overlay-surface.d.ts","sourceRoot":"","sources":["../../../src/patterns/overlay-surface/overlay-surface.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAA;
|
|
1
|
+
{"version":3,"file":"overlay-surface.d.ts","sourceRoot":"","sources":["../../../src/patterns/overlay-surface/overlay-surface.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAA;AAuE9B;;;;;;GAMG;AACH,eAAO,MAAM,mBAAmB,sDAAsD,CAAA;AAEtF,MAAM,WAAW,kBACf,SAAQ,KAAK,CAAC,cAAc,CAAC,cAAc,CAAC;IAC5C;;;;;;OAMG;IACH,QAAQ,CAAC,EAAE,OAAO,CAAA;IAClB;;;;;;;;;;;;;OAaG;IACH,QAAQ,CAAC,EAAE,KAAK,CAAC,SAAS,CAAA;CAC3B;AAED,eAAO,MAAM,aAAa,2FAoExB,CAAA;AAGF,eAAO,MAAM,WAAW,6GAmBtB,CAAA;AAGF,eAAO,MAAM,aAAa,6GAcxB,CAAA"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"overlay-surface.js","sources":["../../../src/patterns/overlay-surface/overlay-surface.tsx"],"sourcesContent":["import * as React from 'react'\nimport { cn } from '@/lib/utils'\n// W2 tabsSlot wrapper 契約 SSOT 在 header-canonical(跨家族 W-rule owner);SurfaceHeader 消費同一 const 不 hardcode。\nimport { HEADER_TABS_SLOT_WRAPPER_CLASS } from '@/design-system/patterns/header-canonical/chrome-header'\n\n/**\n * @internal — DS-internal primitive(2026-05-23 per `packages/design-system/ds-canonical/rules/ui-development.md` Public vs Internal canonical)。\n * end-user app 直接 import 無 functioning UI;由 Dialog / Sheet / Popover / Coachmark 等 DS overlay 元件 wrap 消費(HoverCard 不消費 — 2026-07-04 grep 0 import,spec「何時不用」同步)。\n *\n * Overlay Surface primitives — Dialog / Popover / Sheet 共用結構化 sub-components。\n *\n * 抽象這層避免各自硬寫 padding / border 導致漂移。有特殊行為(Dialog Close 按鈕 /\n * viewport-fill 高度)由 consumer 額外包裝,不污染 primitive。\n *\n * ── Header / Footer 高度 canonical(2026-04-22 v3 校準,user intent 精確實作)──\n * **Padding-based sizing**:`py-[var(--layout-space-tight)]`,高度 = max(child) + 2×tight\n * 不用 fixed `min-h`(先前誤用 min-h-chrome-header-height 會讓 bounded button 被鎖死在 48\n * slot 失去自然高度,違反 user 意圖)。\n *\n * 對齊 `--chrome-header-height`(48/56)的**條件**:header 只放 unbounded 控件(close X /\n * text variant button,**native sm + v5 unbounded trick layout 佔位 24**):\n * header = 24 + 2×tight = 48 md / 56 lg ✓。\n *\n * 若 header 塞 **bounded 控件**(primary / tertiary with bg/border):header 自然長高\n * (sm 28 + 2×12 = 52 md),因 bounded 按鈕有視覺邊界,padding 不會顯得過大 — 這是預期的。\n *\n * ── Unbounded controls 在 header canonical(v5 trick,2026-04-22)──\n * Dismiss X(always unbounded)+ text variant header action → **`size=\"sm\"` native**\n * + SurfaceHeader CSS 自動套負 my trick(對 `[data-unbounded]`),layout 佔位縮回 24\n * (xs 等同)。Rationale:button native size 跟 touch target 保留 sm(a11y 最小 24+ hit\n * target,視覺 render 仍 28/32),layout 佔位精確匹配 chrome-header-height 幾何。詳\n * `overlay-surface.spec.md`「Chrome dismiss size canonical」。\n *\n * **Notification banner family**(Notice / Alert / Toast,fixed `px-4 py-3` variant,\n * 無 padding-based header)→ dismiss 用 `size=\"xs\"` explicit(24 固定,無 trick)。\n *\n * ── Token 規則 ──\n * horizontal padding: `px-[var(--layout-space-loose)]`\n * vertical padding: `py-[var(--layout-space-tight)]`\n * 分隔線: `border-{b|t} border-divider`\n *\n * ── Body ──\n * Body padding-based(content area),`px-[var(--layout-space-loose)] py-[var(--layout-space-tight)]`。\n * Dialog / Sheet body 走 ScrollArea + chrome padding(`px-loose pt-tight pb-bottom`)。\n * List-as-region 場景(menu / Cmd+K)由 consumer 用 className override 撤 chrome padding +\n * 自管 list outer wrapper(詳 overlay-surface.spec.md「List-as-region in overlay body」)。\n */\n\n// Chrome-slot layout trick(2026-04-22 v5,2026-05-04 重思 parameterize):\n// **所有 unbounded control**(Button with `data-unbounded=\"true\"` — Button 自動在 variant=\"text\" 或 dismiss\n// 時標記)的 **native size 不變**(sm: 28 md / 32 lg),但 **layout 佔位** via 負 `my` 縮成 `--chrome-slot-h`。\n//\n// **Slot height = header title 的 line-height**(讓 button 不 dominate,title 自然撐 header)\n// - Default 衍生自 title typography:`calc(var(--font-body-lg-size)*1.5)` = 16×1.5 = 24(Dialog/Sheet title = text-body-lg)。\n// 2026-06-16:從寫死 `var(--field-height-xs)`(巧合也=24)改為衍生 → title 字級若改,slot 自動跟、不靠巧合(M17 SSOT;\n// 1.5 = typography.spec body 行高常數,非 token 故 inline,若 body 行高改 1.3 須同步此處)。\n// - Popover-tier(title = text-body)走 `COMPACT_HEADER_SLOT`(見下)= calc(--font-body-size×1.5) = 14×1.5 = 21\n// (= PopoverTitle text-body line-box,衍生非寫死;2026-06-16 從各自寫死 1.25rem/20 巧合值收斂進 SSOT)\n//\n// Header 永遠 padding-based(無 min-h),但因 slot = title line-box,header 高度由 title 主導:\n// - Dialog: max(24 title, 24 slot) + py-tight(12*2)= 48 ✓ chrome-header-height\n// - Popover: max(21 title, 21 slot) + py-tight(12*2)= 45 ✓ 自然輕量\n// Q10 穩定性:title-only / title+button / refresh in/out 全 case header 高度 = title + py(slot=title,不超過)\n//\n// 負 my 公式:(slot - native) / 2,density-aware:\n// Dialog md: (24 - 28) / 2 = -2px; lg: (24 - 32) / 2 = -4px\n// Popover md: (21 - 28) / 2 = -3.5px(sub-pixel,可接受)\nconst CHROME_UNBOUNDED_SLOT = '[&_[data-unbounded]]:my-[calc((var(--chrome-slot-h,calc(var(--font-body-lg-size)*1.5))-var(--field-height-sm))/2)]'\n\n/**\n * 輕量浮層 header 的 slot override —— **SSOT,禁各自 inline**(2026-06-16 收斂)。\n * Popover-tier header(title = PopoverTitle `text-body`)的 slot = body line-box = `calc(--font-body-size×1.5)` = 21,\n * 衍生自 title typography → title 字級改,slot 自動跟(取代舊各自寫死的 1.25rem / 20 巧合值)。\n * 消費者(同設計邏輯,皆 raw SurfaceHeader + PopoverTitle):PopoverHeader / DataTable sort·filter panel /\n * FileItem upload-manager。Default slot(Dialog/Sheet,title = text-body-lg)= 24,在 CHROME_UNBOUNDED_SLOT fallback。\n */\nexport const COMPACT_HEADER_SLOT = '[--chrome-slot-h:calc(var(--font-body-size)*1.5)]'\n\nexport interface SurfaceHeaderProps\n extends React.HTMLAttributes<HTMLDivElement> {\n /**\n * 是否內含 Tabs。\n * true(無 tabsSlot)→ 移除自身 border-b。\n * 若提供 tabsSlot,自動 column mode + auto suppress border(不需手動傳 withTabs=true)。\n * 對齊 patterns/header-canonical/header-canonical.spec.md W1\n * 「Header semantic owner / TabsList paint owner in withTabs state」。\n */\n withTabs?: boolean\n /**\n * Tabs row slot(2026-05-18 加 per header-canonical.spec.md W2/W4 真實能用 + user-mandated fix)。\n * 提供時 SurfaceHeader 自動 column 結構:\n * row 1 = children(title + actions/dismiss,px-loose py-tight)\n * row 2 = tabsSlot 包在 `HEADER_TABS_SLOT_WRAPPER_CLASS` wrapper(v4:wrapper 自身\n * 無 px / 無 border;`[&_[data-slot=tabs-list]]` 注入 px-loose,寬度 TabsList\n * 各 overflow 模式自帶,border-b 延展全寬 — W1 視覺一條線。\n * 詳 chrome-header.tsx HEADER_TABS_SLOT_WRAPPER_CLASS 註解)\n *\n * Consumer 傳:`tabsSlot={<TabsList>...</TabsList>}`,TabsContent 仍放 DialogBody 內。\n * `<Tabs>` root 必須 wrap 整 DialogContent(Radix TabsList ↔ TabsContent 同 root 連動)。\n *\n * 提供 tabsSlot 自動 withTabs=true,不需另傳 prop。\n */\n tabsSlot?: React.ReactNode\n}\n\nexport const SurfaceHeader = React.forwardRef<\n HTMLDivElement,\n SurfaceHeaderProps\n>(({ className, withTabs, tabsSlot, children, ...props }, ref) => {\n // tabsSlot 提供 → 自動 withTabs(consumer 不需手動兩個 prop)\n const hasTabs = tabsSlot != null || withTabs === true\n\n // Column mode(tabsSlot 提供時)— 對應 header-canonical.spec.md W2 + W4\n if (tabsSlot != null) {\n return (\n <div\n ref={ref}\n data-slot=\"surface-header\"\n className={cn('flex flex-col shrink-0', className)}\n {...props}\n >\n {/* Row 1:header content row(原 single-row behavior 不變,但 border-b 撤掉,row 2 wrapper 接管 paint)*/}\n <div\n className={cn(\n 'flex items-center gap-2',\n 'px-[var(--layout-space-loose)] py-[var(--layout-space-tight)]',\n CHROME_UNBOUNDED_SLOT,\n )}\n >\n {children}\n </div>\n {/* Row 2:tabsSlot wrapper — TabsList 全 dialog 寬 + 內 px-loose padding inset triggers\n 2026-05-18 v3 fix(user verbatim approval「你確定這樣是世界級的設計且符合我們一致的設計\n 語言和 SSOT...就這樣做」):\n - v1: wrapper border + TabsList border = 雙線(W1 違反)\n - v2: wrapper px-loose + TabsList w-full inside = border 只跨 dialog - 2×px-loose\n (不到 dialog 邊 user 抓「分隔線寬度應該要填滿整個 dialog」)\n - v3:wrapper 不 inset,TabsList 自己 px-loose 內 padding inset triggers\n → TabsList border-b 延展全 dialog 寬,triggers 對齊 header content\n - v4(2026-07-30):選取機制改 `data-slot=\"tabs-list\"` attribute contract\n (beta.86 overlay 包裹層破 direct-child;詳 chrome-header.tsx 註解)\n 對齊既有 `tabs.spec.md`「Underline 與 TabsList border 的視覺關係」段「selected 底線從\n TabsList 的 gray border 位置長出來」(token = border-divider,tabs.tsx TABS_LIST_BASE)\n + GitHub Primer UnderlineNav / Ant Design line type / Mantine default 共識\n (全派 TabsList 自畫 full-width underline)。*/}\n <div className={HEADER_TABS_SLOT_WRAPPER_CLASS}>\n {tabsSlot}\n </div>\n </div>\n )\n }\n\n // Padding-based(預設) — Dialog/Sheet 用 body-lg title (16/24),自然撐 max(24 title, 24 button slot) = 24\n // → header = 24 + py-tight 12×2 = 48 chrome-header-height ✓ 穩定無需 min-h\n // Popover 等輕量 chrome 走 COMPACT_HEADER_SLOT(slot = calc(--font-body-size×1.5)=21 = title line-box,無 min-h / 無 py override):max(21 title, 21 slot) + py-tight 12×2 = 45(見上方 L57)\n //\n // withTabs=true(無 tabsSlot,backward compat):移除 border-b,consumer 自畫\n return (\n <div\n ref={ref}\n data-slot=\"surface-header\"\n className={cn(\n 'flex items-center gap-2 shrink-0',\n !hasTabs && 'border-b border-divider',\n 'px-[var(--layout-space-loose)] py-[var(--layout-space-tight)]',\n CHROME_UNBOUNDED_SLOT,\n className,\n )}\n {...props}\n >\n {children}\n </div>\n )\n})\nSurfaceHeader.displayName = 'SurfaceHeader'\n\nexport const SurfaceBody = React.forwardRef<\n HTMLDivElement,\n React.HTMLAttributes<HTMLDivElement>\n>(({ className, ...props }, ref) => (\n // 2026-05-04 viewport-aware scroll canonical:\n // parent(PopoverContent / HoverCardContent / Dialog / Sheet)是 flex flex-col + max-h + overflow-hidden\n // header / footer shrink-0;Body flex-1 min-h-0 overflow-y-auto → 視窗太小時 body 內 scroll\n // 非 flex-col parent 內 flex-1/min-h-0 no-op,backward compat\n <div\n ref={ref}\n data-slot=\"surface-body\"\n className={cn(\n 'flex-1 min-h-0 overflow-y-auto',\n 'focus-visible:focus-ring-inset',\n 'px-[var(--layout-space-loose)] py-[var(--layout-space-tight)]',\n className,\n )}\n {...props}\n />\n))\nSurfaceBody.displayName = 'SurfaceBody'\n\nexport const SurfaceFooter = React.forwardRef<\n HTMLDivElement,\n React.HTMLAttributes<HTMLDivElement>\n>(({ className, ...props }, ref) => (\n <div\n ref={ref}\n data-slot=\"surface-footer\"\n className={cn(\n 'flex items-center justify-end gap-2 shrink-0 border-t border-divider',\n 'px-[var(--layout-space-loose)] py-[var(--layout-space-tight)]',\n className,\n )}\n {...props}\n />\n))\nSurfaceFooter.displayName = 'SurfaceFooter'\n"],"names":[],"mappings":";;;;AAmEA,MAAM,wBAAwB;AASvB,MAAM,sBAAsB;AA6B5B,MAAM,gBAAgB,MAAM,WAGjC,CAAC,EAAE,WAAW,UAAU,UAAU,UAAU,GAAG,MAAA,GAAS,QAAQ;AAEhE,QAAM,UAAU,YAAY,QAAQ,aAAa;AAGjD,MAAI,YAAY,MAAM;AACpB,WACE;AAAA,MAAC;AAAA,MAAA;AAAA,QACC;AAAA,QACA,aAAU;AAAA,QACV,WAAW,GAAG,0BAA0B,SAAS;AAAA,QAChD,GAAG;AAAA,QAGJ,UAAA;AAAA,UAAA;AAAA,YAAC;AAAA,YAAA;AAAA,cACC,WAAW;AAAA,gBACT;AAAA,gBACA;AAAA,gBACA;AAAA,cAAA;AAAA,cAGD;AAAA,YAAA;AAAA,UAAA;AAAA,UAgBH,oBAAC,OAAA,EAAI,WAAW,gCACb,UAAA,SAAA,CACH;AAAA,QAAA;AAAA,MAAA;AAAA,IAAA;AAAA,EAGN;AAOA,SACE;AAAA,IAAC;AAAA,IAAA;AAAA,MACC;AAAA,MACA,aAAU;AAAA,MACV,WAAW;AAAA,QACT;AAAA,QACA,CAAC,WAAW;AAAA,QACZ;AAAA,QACA;AAAA,QACA;AAAA,MAAA;AAAA,MAED,GAAG;AAAA,MAEH;AAAA,IAAA;AAAA,EAAA;AAGP,CAAC;AACD,cAAc,cAAc;AAErB,MAAM,cAAc,MAAM,WAG/B,CAAC,EAAE,WAAW,GAAG,SAAS;AAAA;AAAA;AAAA;AAAA;AAAA,EAK1B;AAAA,IAAC;AAAA,IAAA;AAAA,MACC;AAAA,MACA,aAAU;AAAA,MACV,WAAW;AAAA,QACT;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,MAAA;AAAA,MAED,GAAG;AAAA,IAAA;AAAA,EAAA;AAAA,CAEP;AACD,YAAY,cAAc;AAEnB,MAAM,gBAAgB,MAAM,WAGjC,CAAC,EAAE,WAAW,GAAG,MAAA,GAAS,QAC1B;AAAA,EAAC;AAAA,EAAA;AAAA,IACC;AAAA,IACA,aAAU;AAAA,IACV,WAAW;AAAA,MACT;AAAA,MACA;AAAA,MACA;AAAA,IAAA;AAAA,IAED,GAAG;AAAA,EAAA;AACN,CACD;AACD,cAAc,cAAc;"}
|
|
1
|
+
{"version":3,"file":"overlay-surface.js","sources":["../../../src/patterns/overlay-surface/overlay-surface.tsx"],"sourcesContent":["import * as React from 'react'\nimport { cn } from '@/lib/utils'\n// W2 tabsSlot wrapper 契約 SSOT 在 header-canonical(跨家族 W-rule owner);SurfaceHeader 消費同一 const 不 hardcode。\nimport { HEADER_TABS_SLOT_WRAPPER_CLASS } from '@/design-system/patterns/header-canonical/chrome-header'\n\n/**\n * @internal — DS-internal primitive(2026-05-23 per `packages/design-system/ds-canonical/rules/ui-development.md` Public vs Internal canonical)。\n * end-user app 直接 import 無 functioning UI;由 Dialog / Sheet / Popover / Coachmark 等 DS overlay 元件 wrap 消費(HoverCard 不消費 — 2026-07-04 grep 0 import,spec「何時不用」同步)。\n *\n * Overlay Surface primitives — Dialog / Popover / Sheet 共用結構化 sub-components。\n *\n * 抽象這層避免各自硬寫 padding / border 導致漂移。有特殊行為(Dialog Close 按鈕 /\n * viewport-fill 高度)由 consumer 額外包裝,不污染 primitive。\n *\n * ── Header / Footer 高度 canonical(2026-04-22 v3 校準,user intent 精確實作)──\n * **Padding-based sizing**:`py-[var(--layout-space-tight)]`,高度 = max(child) + 2×tight\n * 不用 fixed `min-h`(先前誤用 min-h-chrome-header-height 會讓 bounded button 被鎖死在 48\n * slot 失去自然高度,違反 user 意圖)。\n *\n * 對齊 `--chrome-header-height`(48/56)的**條件**:header 只放 unbounded 控件(close X /\n * text variant button,**native sm + v5 unbounded trick layout 佔位 24**):\n * header = 24 + 2×tight = 48 md / 56 lg ✓。\n *\n * 若 header 塞 **bounded 控件**(primary / tertiary with bg/border):header 自然長高\n * (sm 28 + 2×12 = 52 md),因 bounded 按鈕有視覺邊界,padding 不會顯得過大 — 這是預期的。\n *\n * ── Unbounded controls 在 header canonical(v5 trick,2026-04-22)──\n * Dismiss X(always unbounded)+ text variant header action → **`size=\"sm\"` native**\n * + SurfaceHeader CSS 自動套負 my trick(對 `[data-unbounded]`),layout 佔位縮回 24\n * (xs 等同)。Rationale:button native size 跟命中區保留 sm(視覺 render 仍 28/32,命中 ≡ 可視\n * → `ds-canonical/references/hit-area-canonical.md`),layout 佔位精確匹配 chrome-header-height 幾何。\n * 縮到 xs 的問題是**與 chrome 的比例**(chrome dismiss 是 sm = 28),不是 a11y —— 24 正好是\n * `tokens/uiSize/uiSize.spec.md`「元件高度地板」訂的最小值。詳\n * `overlay-surface.spec.md`「Chrome dismiss size canonical」。\n *\n * **Notification banner family**(Notice / Alert / Toast,fixed `px-4 py-3` variant,\n * 無 padding-based header)→ dismiss 用 `size=\"xs\"` explicit(24 固定,無 trick)。\n *\n * ── Token 規則 ──\n * horizontal padding: `px-[var(--layout-space-loose)]`\n * vertical padding: `py-[var(--layout-space-tight)]`\n * 分隔線: `border-{b|t} border-divider`\n *\n * ── Body ──\n * Body padding-based(content area),`px-[var(--layout-space-loose)] py-[var(--layout-space-tight)]`。\n * Dialog / Sheet body 走 ScrollArea + chrome padding(`px-loose pt-tight pb-bottom`)。\n * List-as-region 場景(menu / Cmd+K)由 consumer 用 className override 撤 chrome padding +\n * 自管 list outer wrapper(詳 overlay-surface.spec.md「List-as-region in overlay body」)。\n */\n\n// Chrome-slot layout trick(2026-04-22 v5,2026-05-04 重思 parameterize):\n// **所有 unbounded control**(Button with `data-unbounded=\"true\"` — Button 自動在 variant=\"text\" 或 dismiss\n// 時標記)的 **native size 不變**(sm: 28 md / 32 lg),但 **layout 佔位** via 負 `my` 縮成 `--chrome-slot-h`。\n//\n// **Slot height = header title 的 line-height**(讓 button 不 dominate,title 自然撐 header)\n// - Default 衍生自 title typography:`calc(var(--font-body-lg-size)*1.5)` = 16×1.5 = 24(Dialog/Sheet title = text-body-lg)。\n// 2026-06-16:從寫死 `var(--field-height-xs)`(巧合也=24)改為衍生 → title 字級若改,slot 自動跟、不靠巧合(M17 SSOT;\n// 1.5 = typography.spec body 行高常數,非 token 故 inline,若 body 行高改 1.3 須同步此處)。\n// - Popover-tier(title = text-body)走 `COMPACT_HEADER_SLOT`(見下)= calc(--font-body-size×1.5) = 14×1.5 = 21\n// (= PopoverTitle text-body line-box,衍生非寫死;2026-06-16 從各自寫死 1.25rem/20 巧合值收斂進 SSOT)\n//\n// Header 永遠 padding-based(無 min-h),但因 slot = title line-box,header 高度由 title 主導:\n// - Dialog: max(24 title, 24 slot) + py-tight(12*2)= 48 ✓ chrome-header-height\n// - Popover: max(21 title, 21 slot) + py-tight(12*2)= 45 ✓ 自然輕量\n// Q10 穩定性:title-only / title+button / refresh in/out 全 case header 高度 = title + py(slot=title,不超過)\n//\n// 負 my 公式:(slot - native) / 2,density-aware:\n// Dialog md: (24 - 28) / 2 = -2px; lg: (24 - 32) / 2 = -4px\n// Popover md: (21 - 28) / 2 = -3.5px(sub-pixel,可接受)\nconst CHROME_UNBOUNDED_SLOT = '[&_[data-unbounded]]:my-[calc((var(--chrome-slot-h,calc(var(--font-body-lg-size)*1.5))-var(--field-height-sm))/2)]'\n\n/**\n * 輕量浮層 header 的 slot override —— **SSOT,禁各自 inline**(2026-06-16 收斂)。\n * Popover-tier header(title = PopoverTitle `text-body`)的 slot = body line-box = `calc(--font-body-size×1.5)` = 21,\n * 衍生自 title typography → title 字級改,slot 自動跟(取代舊各自寫死的 1.25rem / 20 巧合值)。\n * 消費者(同設計邏輯,皆 raw SurfaceHeader + PopoverTitle):PopoverHeader / DataTable sort·filter panel /\n * FileItem upload-manager。Default slot(Dialog/Sheet,title = text-body-lg)= 24,在 CHROME_UNBOUNDED_SLOT fallback。\n */\nexport const COMPACT_HEADER_SLOT = '[--chrome-slot-h:calc(var(--font-body-size)*1.5)]'\n\nexport interface SurfaceHeaderProps\n extends React.HTMLAttributes<HTMLDivElement> {\n /**\n * 是否內含 Tabs。\n * true(無 tabsSlot)→ 移除自身 border-b。\n * 若提供 tabsSlot,自動 column mode + auto suppress border(不需手動傳 withTabs=true)。\n * 對齊 patterns/header-canonical/header-canonical.spec.md W1\n * 「Header semantic owner / TabsList paint owner in withTabs state」。\n */\n withTabs?: boolean\n /**\n * Tabs row slot(2026-05-18 加 per header-canonical.spec.md W2/W4 真實能用 + user-mandated fix)。\n * 提供時 SurfaceHeader 自動 column 結構:\n * row 1 = children(title + actions/dismiss,px-loose py-tight)\n * row 2 = tabsSlot 包在 `HEADER_TABS_SLOT_WRAPPER_CLASS` wrapper(v4:wrapper 自身\n * 無 px / 無 border;`[&_[data-slot=tabs-list]]` 注入 px-loose,寬度 TabsList\n * 各 overflow 模式自帶,border-b 延展全寬 — W1 視覺一條線。\n * 詳 chrome-header.tsx HEADER_TABS_SLOT_WRAPPER_CLASS 註解)\n *\n * Consumer 傳:`tabsSlot={<TabsList>...</TabsList>}`,TabsContent 仍放 DialogBody 內。\n * `<Tabs>` root 必須 wrap 整 DialogContent(Radix TabsList ↔ TabsContent 同 root 連動)。\n *\n * 提供 tabsSlot 自動 withTabs=true,不需另傳 prop。\n */\n tabsSlot?: React.ReactNode\n}\n\nexport const SurfaceHeader = React.forwardRef<\n HTMLDivElement,\n SurfaceHeaderProps\n>(({ className, withTabs, tabsSlot, children, ...props }, ref) => {\n // tabsSlot 提供 → 自動 withTabs(consumer 不需手動兩個 prop)\n const hasTabs = tabsSlot != null || withTabs === true\n\n // Column mode(tabsSlot 提供時)— 對應 header-canonical.spec.md W2 + W4\n if (tabsSlot != null) {\n return (\n <div\n ref={ref}\n data-slot=\"surface-header\"\n className={cn('flex flex-col shrink-0', className)}\n {...props}\n >\n {/* Row 1:header content row(原 single-row behavior 不變,但 border-b 撤掉,row 2 wrapper 接管 paint)*/}\n <div\n className={cn(\n 'flex items-center gap-2',\n 'px-[var(--layout-space-loose)] py-[var(--layout-space-tight)]',\n CHROME_UNBOUNDED_SLOT,\n )}\n >\n {children}\n </div>\n {/* Row 2:tabsSlot wrapper — TabsList 全 dialog 寬 + 內 px-loose padding inset triggers\n 2026-05-18 v3 fix(user verbatim approval「你確定這樣是世界級的設計且符合我們一致的設計\n 語言和 SSOT...就這樣做」):\n - v1: wrapper border + TabsList border = 雙線(W1 違反)\n - v2: wrapper px-loose + TabsList w-full inside = border 只跨 dialog - 2×px-loose\n (不到 dialog 邊 user 抓「分隔線寬度應該要填滿整個 dialog」)\n - v3:wrapper 不 inset,TabsList 自己 px-loose 內 padding inset triggers\n → TabsList border-b 延展全 dialog 寬,triggers 對齊 header content\n - v4(2026-07-30):選取機制改 `data-slot=\"tabs-list\"` attribute contract\n (beta.86 overlay 包裹層破 direct-child;詳 chrome-header.tsx 註解)\n 對齊既有 `tabs.spec.md`「Underline 與 TabsList border 的視覺關係」段「selected 底線從\n TabsList 的 gray border 位置長出來」(token = border-divider,tabs.tsx TABS_LIST_BASE)\n + GitHub Primer UnderlineNav / Ant Design line type / Mantine default 共識\n (全派 TabsList 自畫 full-width underline)。*/}\n <div className={HEADER_TABS_SLOT_WRAPPER_CLASS}>\n {tabsSlot}\n </div>\n </div>\n )\n }\n\n // Padding-based(預設) — Dialog/Sheet 用 body-lg title (16/24),自然撐 max(24 title, 24 button slot) = 24\n // → header = 24 + py-tight 12×2 = 48 chrome-header-height ✓ 穩定無需 min-h\n // Popover 等輕量 chrome 走 COMPACT_HEADER_SLOT(slot = calc(--font-body-size×1.5)=21 = title line-box,無 min-h / 無 py override):max(21 title, 21 slot) + py-tight 12×2 = 45(見上方 L57)\n //\n // withTabs=true(無 tabsSlot,backward compat):移除 border-b,consumer 自畫\n return (\n <div\n ref={ref}\n data-slot=\"surface-header\"\n className={cn(\n 'flex items-center gap-2 shrink-0',\n !hasTabs && 'border-b border-divider',\n 'px-[var(--layout-space-loose)] py-[var(--layout-space-tight)]',\n CHROME_UNBOUNDED_SLOT,\n className,\n )}\n {...props}\n >\n {children}\n </div>\n )\n})\nSurfaceHeader.displayName = 'SurfaceHeader'\n\nexport const SurfaceBody = React.forwardRef<\n HTMLDivElement,\n React.HTMLAttributes<HTMLDivElement>\n>(({ className, ...props }, ref) => (\n // 2026-05-04 viewport-aware scroll canonical:\n // parent(PopoverContent / HoverCardContent / Dialog / Sheet)是 flex flex-col + max-h + overflow-hidden\n // header / footer shrink-0;Body flex-1 min-h-0 overflow-y-auto → 視窗太小時 body 內 scroll\n // 非 flex-col parent 內 flex-1/min-h-0 no-op,backward compat\n <div\n ref={ref}\n data-slot=\"surface-body\"\n className={cn(\n 'flex-1 min-h-0 overflow-y-auto',\n 'focus-visible:focus-ring-inset',\n 'px-[var(--layout-space-loose)] py-[var(--layout-space-tight)]',\n className,\n )}\n {...props}\n />\n))\nSurfaceBody.displayName = 'SurfaceBody'\n\nexport const SurfaceFooter = React.forwardRef<\n HTMLDivElement,\n React.HTMLAttributes<HTMLDivElement>\n>(({ className, ...props }, ref) => (\n <div\n ref={ref}\n data-slot=\"surface-footer\"\n className={cn(\n 'flex items-center justify-end gap-2 shrink-0 border-t border-divider',\n 'px-[var(--layout-space-loose)] py-[var(--layout-space-tight)]',\n className,\n )}\n {...props}\n />\n))\nSurfaceFooter.displayName = 'SurfaceFooter'\n"],"names":[],"mappings":";;;;AAqEA,MAAM,wBAAwB;AASvB,MAAM,sBAAsB;AA6B5B,MAAM,gBAAgB,MAAM,WAGjC,CAAC,EAAE,WAAW,UAAU,UAAU,UAAU,GAAG,MAAA,GAAS,QAAQ;AAEhE,QAAM,UAAU,YAAY,QAAQ,aAAa;AAGjD,MAAI,YAAY,MAAM;AACpB,WACE;AAAA,MAAC;AAAA,MAAA;AAAA,QACC;AAAA,QACA,aAAU;AAAA,QACV,WAAW,GAAG,0BAA0B,SAAS;AAAA,QAChD,GAAG;AAAA,QAGJ,UAAA;AAAA,UAAA;AAAA,YAAC;AAAA,YAAA;AAAA,cACC,WAAW;AAAA,gBACT;AAAA,gBACA;AAAA,gBACA;AAAA,cAAA;AAAA,cAGD;AAAA,YAAA;AAAA,UAAA;AAAA,UAgBH,oBAAC,OAAA,EAAI,WAAW,gCACb,UAAA,SAAA,CACH;AAAA,QAAA;AAAA,MAAA;AAAA,IAAA;AAAA,EAGN;AAOA,SACE;AAAA,IAAC;AAAA,IAAA;AAAA,MACC;AAAA,MACA,aAAU;AAAA,MACV,WAAW;AAAA,QACT;AAAA,QACA,CAAC,WAAW;AAAA,QACZ;AAAA,QACA;AAAA,QACA;AAAA,MAAA;AAAA,MAED,GAAG;AAAA,MAEH;AAAA,IAAA;AAAA,EAAA;AAGP,CAAC;AACD,cAAc,cAAc;AAErB,MAAM,cAAc,MAAM,WAG/B,CAAC,EAAE,WAAW,GAAG,SAAS;AAAA;AAAA;AAAA;AAAA;AAAA,EAK1B;AAAA,IAAC;AAAA,IAAA;AAAA,MACC;AAAA,MACA,aAAU;AAAA,MACV,WAAW;AAAA,QACT;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,MAAA;AAAA,MAED,GAAG;AAAA,IAAA;AAAA,EAAA;AAAA,CAEP;AACD,YAAY,cAAc;AAEnB,MAAM,gBAAgB,MAAM,WAGjC,CAAC,EAAE,WAAW,GAAG,MAAA,GAAS,QAC1B;AAAA,EAAC;AAAA,EAAA;AAAA,IACC;AAAA,IACA,aAAU;AAAA,IACV,WAAW;AAAA,MACT;AAAA,MACA;AAAA,MACA;AAAA,IAAA;AAAA,IAED,GAAG;AAAA,EAAA;AACN,CACD;AACD,cAAc,cAAc;"}
|
|
@@ -53,7 +53,12 @@ export declare const CAT_SOLID_TOKENS: Record<CategoricalHue, {
|
|
|
53
53
|
bg: string;
|
|
54
54
|
text: string;
|
|
55
55
|
}>;
|
|
56
|
-
/**
|
|
56
|
+
/**
|
|
57
|
+
* Calendar event tile:subtle 底 + hover step-2(1:1)。
|
|
58
|
+
* 淡底的滑過 = 同色離底更遠一格:淺色變深、深色變亮(色階號碼 = 離所在底色多遠;primitives.css 檔頭、
|
|
59
|
+
* color.spec.md「Dark mode subtle」)。與上方實心色互動 state 不同 —— 那是 semantic `--{hue}-hover`(深色要反轉),
|
|
60
|
+
* 這裡是淡底區 step-1 → step-2,兩主題同向,直接用 primitive(2026-09-26 深色 step-2..4 改半透明淡底後成立)。
|
|
61
|
+
*/
|
|
57
62
|
export declare const CAT_EVENT: Record<CategoricalHue, string>;
|
|
58
63
|
/** Calendar all-day accent:左側 step-6 實心條。 */
|
|
59
64
|
export declare const CAT_ACCENT: Record<CategoricalHue, string>;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"categorical-color.d.ts","sourceRoot":"","sources":["../../src/tokens/categorical-color.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AAEH,oMAAoM;AACpM,eAAO,MAAM,gBAAgB,mIAanB,CAAA;AAEV,MAAM,MAAM,cAAc,GAAG,CAAC,OAAO,gBAAgB,CAAC,CAAC,MAAM,CAAC,CAAA;AAE9D,6FAA6F;AAC7F,MAAM,MAAM,gBAAgB,GAAG,cAAc,GAAG,SAAS,CAAA;AAEzD,wEAAwE;AACxE,eAAO,MAAM,UAAU,EAAE,MAAM,CAAC,cAAc,EAAE,MAAM,CAarD,CAAA;AAED;;;;;;;GAOG;AACH,eAAO,MAAM,SAAS,EAAE,MAAM,CAAC,cAAc,EAAE,MAAM,CAapD,CAAA;AAED,sEAAsE;AACtE,eAAO,MAAM,YAAY,EAAE,MAAM,CAAC,cAAc,EAAE;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAalF,CAAA;AAED,yEAAyE;AACzE,eAAO,MAAM,iBAAiB,EAAE,MAAM,CAAC,cAAc,EAAE;IAAE,EAAE,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,CAalF,CAAA;AAED,eAAO,MAAM,gBAAgB,EAAE,MAAM,CAAC,cAAc,EAAE;IAAE,EAAE,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,CAajF,CAAA;AAED
|
|
1
|
+
{"version":3,"file":"categorical-color.d.ts","sourceRoot":"","sources":["../../src/tokens/categorical-color.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AAEH,oMAAoM;AACpM,eAAO,MAAM,gBAAgB,mIAanB,CAAA;AAEV,MAAM,MAAM,cAAc,GAAG,CAAC,OAAO,gBAAgB,CAAC,CAAC,MAAM,CAAC,CAAA;AAE9D,6FAA6F;AAC7F,MAAM,MAAM,gBAAgB,GAAG,cAAc,GAAG,SAAS,CAAA;AAEzD,wEAAwE;AACxE,eAAO,MAAM,UAAU,EAAE,MAAM,CAAC,cAAc,EAAE,MAAM,CAarD,CAAA;AAED;;;;;;;GAOG;AACH,eAAO,MAAM,SAAS,EAAE,MAAM,CAAC,cAAc,EAAE,MAAM,CAapD,CAAA;AAED,sEAAsE;AACtE,eAAO,MAAM,YAAY,EAAE,MAAM,CAAC,cAAc,EAAE;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAalF,CAAA;AAED,yEAAyE;AACzE,eAAO,MAAM,iBAAiB,EAAE,MAAM,CAAC,cAAc,EAAE;IAAE,EAAE,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,CAalF,CAAA;AAED,eAAO,MAAM,gBAAgB,EAAE,MAAM,CAAC,cAAc,EAAE;IAAE,EAAE,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,CAajF,CAAA;AAED;;;;;GAKG;AACH,eAAO,MAAM,SAAS,EAAE,MAAM,CAAC,cAAc,EAAE,MAAM,CAapD,CAAA;AAED,6CAA6C;AAC7C,eAAO,MAAM,UAAU,EAAE,MAAM,CAAC,cAAc,EAAE,MAAM,CAarD,CAAA"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"categorical-color.js","sources":["../../src/tokens/categorical-color.ts"],"sourcesContent":["/**\n * categorical-color.ts — Categorical 色相 SSOT(2026-06-04 codify)\n *\n * Tag / Avatar / Calendar 等「以色相做分類標籤」的元件,色彩 variant **一律消費此 SSOT**。\n * 目的:**variant 名 == primitive 色相名,1:1,零 offset** —— 根治「red variant 接到 deep-orange」\n * 這類 categorical-vs-semantic 混淆(2026-06-04 user 抓:red = 品牌紅 hue-25〔--brand #DF3232 同族〕\n * ≠ deep-orange hue-38 ≠ semantic --error〔= deep-orange〕。三者完全無關)。\n *\n * 世界級對照:\n * - Ant Design Tag preset colors —— 用原始色相名(red/orange/green/blue…)做 categorical labeling\n * (https://ant.design/components/tag/)。本 DS Tag/Avatar 採此模型。\n * - Atlassian Color 原則 —— **categorical(裝飾)色 vs semantic(狀態)色必須分離**\n * (https://atlassian.design/foundations/color):categorical `red` 用 `--color-red-*`,跟 semantic\n * `--error`(= deep-orange)無關;把 error 色拿來當 categorical 標籤 = 反 pattern。\n *\n * ⚠️ Tailwind v4 掃描限制:class 字串**必須以字面形式出現在被掃描的 source**(本檔在 src/,會被掃),\n * 不可用 runtime function 拼字串(否則 `bg-[var(--color-orange-1)]` 不被生成 → silent 失效)。\n * 故本 SSOT 以**字面 const map** 提供;consumer `import` 後索引(cva 列 key 取值,保 VariantProps 型別)。\n *\n * 互動 state(solid dismiss hover/active)用 semantic `--{hue}-hover` / `--{hue}-active`\n * (dark-mode 方向 pre-computed,**不可**用 primitive step-5,否則 dark mode 反向)。12 色相皆已定義。\n *\n * **加色相**:加進 `CATEGORICAL_HUES` + 下列 5 個 map 各補一行字面值(key X 用 `--color-X-*`,1:1)。\n * **機械防線**:`scripts/categorical-color-invariants.mjs` I1(current required CI / deterministic audit chain)強制 map 的 key X 的值含 `--color-X-`(名實一致,禁 offset;原引 hook 名不存在,2026-07-04 修)。\n */\n\n/** 12 個 categorical 色相。⚠️ 順序 = **categorical 指派序**(圖表系列鄰色最大區辨:blue→green→deep-orange→yellow→red…),**非** primitives.css 檔內宣告序 — 兩者本就不同,map 以名為 key 故功能無涉;**禁**為「對齊」重排(重排 = 圖表系列換色 = 視覺 breaking)。 */\nexport const CATEGORICAL_HUES = [\n 'blue',\n 'green',\n 'deep-orange',\n 'yellow',\n 'red',\n 'orange',\n 'amber',\n 'lime',\n 'turquoise',\n 'indigo',\n 'purple',\n 'magenta',\n] as const\n\nexport type CategoricalHue = (typeof CATEGORICAL_HUES)[number]\n\n/** neutral 不是色相(無 hue),用 secondary/foreground + neutral-9 solid,各 consumer 自處理(不在下列 map)。 */\nexport type CategoricalColor = CategoricalHue | 'neutral'\n\n/** subtle 模式 class:step-1 底 + step-7 字。key X 一律對 `--color-X-*`(1:1)。 */\nexport const CAT_SUBTLE: Record<CategoricalHue, string> = {\n blue: 'bg-[var(--color-blue-1)] text-[var(--color-blue-7)]',\n green: 'bg-[var(--color-green-1)] text-[var(--color-green-7)]',\n 'deep-orange': 'bg-[var(--color-deep-orange-1)] text-[var(--color-deep-orange-7)]',\n yellow: 'bg-[var(--color-yellow-1)] text-[var(--color-yellow-7)]',\n red: 'bg-[var(--color-red-1)] text-[var(--color-red-7)]',\n orange: 'bg-[var(--color-orange-1)] text-[var(--color-orange-7)]',\n amber: 'bg-[var(--color-amber-1)] text-[var(--color-amber-7)]',\n lime: 'bg-[var(--color-lime-1)] text-[var(--color-lime-7)]',\n turquoise: 'bg-[var(--color-turquoise-1)] text-[var(--color-turquoise-7)]',\n indigo: 'bg-[var(--color-indigo-1)] text-[var(--color-indigo-7)]',\n purple: 'bg-[var(--color-purple-1)] text-[var(--color-purple-7)]',\n magenta: 'bg-[var(--color-magenta-1)] text-[var(--color-magenta-7)]',\n}\n\n/**\n * solid 模式 class:step-6 底 + on-emphasis 配對文字。\n * 文字色依「step-6 底亮度」分桶(WCAG ≥3:1 大粗字門檻,2026-06-04 user「以最低為原則」):\n * - 夠深的色底 → `text-on-emphasis`(白):blue / deep-orange / red / indigo / purple / magenta\n * - 太亮的色底 → `text-on-emphasis-dark`(深):yellow / amber / orange / lime / turquoise(白字連 3:1 都不過;turquoise 2026-07-16 dark 色相修正後 L=0.68 白字 2.77 → 切深字,兩 mode 5.65/6.47 全過)\n * - ★ green 例外:白字 2.47 連 3:1 都不過,但 user 拍板維持白字(綠底白字慣見觀感)= documented exception。\n * 對比為 oklch→相對亮度實測(scripts/categorical-color-invariants.mjs 機械驗,green 列 exempt)。\n */\nexport const CAT_SOLID: Record<CategoricalHue, string> = {\n blue: 'bg-[var(--color-blue-6)] text-on-emphasis',\n green: 'bg-[var(--color-green-6)] text-on-emphasis', // ★白字 documented exception(實測 2.47 < 3:1)\n 'deep-orange': 'bg-[var(--color-deep-orange-6)] text-on-emphasis',\n yellow: 'bg-[var(--color-yellow-6)] text-on-emphasis-dark',\n red: 'bg-[var(--color-red-6)] text-on-emphasis',\n orange: 'bg-[var(--color-orange-6)] text-on-emphasis-dark',\n amber: 'bg-[var(--color-amber-6)] text-on-emphasis-dark',\n lime: 'bg-[var(--color-lime-6)] text-on-emphasis-dark',\n turquoise: 'bg-[var(--color-turquoise-6)] text-on-emphasis-dark',\n indigo: 'bg-[var(--color-indigo-6)] text-on-emphasis',\n purple: 'bg-[var(--color-purple-6)] text-on-emphasis',\n magenta: 'bg-[var(--color-magenta-6)] text-on-emphasis',\n}\n\n/** solid 互動(dismiss hover/active)— semantic dark-mode-aware token。 */\nexport const CAT_INTERACT: Record<CategoricalHue, { hover: string; active: string }> = {\n blue: { hover: 'var(--blue-hover)', active: 'var(--blue-active)' },\n green: { hover: 'var(--green-hover)', active: 'var(--green-active)' },\n 'deep-orange': { hover: 'var(--deep-orange-hover)', active: 'var(--deep-orange-active)' },\n yellow: { hover: 'var(--yellow-hover)', active: 'var(--yellow-active)' },\n red: { hover: 'var(--red-hover)', active: 'var(--red-active)' },\n orange: { hover: 'var(--orange-hover)', active: 'var(--orange-active)' },\n amber: { hover: 'var(--amber-hover)', active: 'var(--amber-active)' },\n lime: { hover: 'var(--lime-hover)', active: 'var(--lime-active)' },\n turquoise: { hover: 'var(--turquoise-hover)', active: 'var(--turquoise-active)' },\n indigo: { hover: 'var(--indigo-hover)', active: 'var(--indigo-active)' },\n purple: { hover: 'var(--purple-hover)', active: 'var(--purple-active)' },\n magenta: { hover: 'var(--magenta-hover)', active: 'var(--magenta-active)' },\n}\n\n/** Avatar style 物件用:subtle / solid 的 raw token { bg, text }(非 class)。 */\nexport const CAT_SUBTLE_TOKENS: Record<CategoricalHue, { bg: string; text: string }> = {\n blue: { bg: 'var(--color-blue-1)', text: 'var(--color-blue-7)' },\n green: { bg: 'var(--color-green-1)', text: 'var(--color-green-7)' },\n 'deep-orange': { bg: 'var(--color-deep-orange-1)', text: 'var(--color-deep-orange-7)' },\n yellow: { bg: 'var(--color-yellow-1)', text: 'var(--color-yellow-7)' },\n red: { bg: 'var(--color-red-1)', text: 'var(--color-red-7)' },\n orange: { bg: 'var(--color-orange-1)', text: 'var(--color-orange-7)' },\n amber: { bg: 'var(--color-amber-1)', text: 'var(--color-amber-7)' },\n lime: { bg: 'var(--color-lime-1)', text: 'var(--color-lime-7)' },\n turquoise: { bg: 'var(--color-turquoise-1)', text: 'var(--color-turquoise-7)' },\n indigo: { bg: 'var(--color-indigo-1)', text: 'var(--color-indigo-7)' },\n purple: { bg: 'var(--color-purple-1)', text: 'var(--color-purple-7)' },\n magenta: { bg: 'var(--color-magenta-1)', text: 'var(--color-magenta-7)' },\n}\n// 同 CAT_SOLID 的文字分桶(白 / 深),raw token 版供 Avatar style 物件用。green = ★白字 exception。\nexport const CAT_SOLID_TOKENS: Record<CategoricalHue, { bg: string; text: string }> = {\n blue: { bg: 'var(--color-blue-6)', text: 'var(--on-emphasis)' },\n green: { bg: 'var(--color-green-6)', text: 'var(--on-emphasis)' }, // ★白字 documented exception\n 'deep-orange': { bg: 'var(--color-deep-orange-6)', text: 'var(--on-emphasis)' },\n yellow: { bg: 'var(--color-yellow-6)', text: 'var(--on-emphasis-dark)' },\n red: { bg: 'var(--color-red-6)', text: 'var(--on-emphasis)' },\n orange: { bg: 'var(--color-orange-6)', text: 'var(--on-emphasis-dark)' },\n amber: { bg: 'var(--color-amber-6)', text: 'var(--on-emphasis-dark)' },\n lime: { bg: 'var(--color-lime-6)', text: 'var(--on-emphasis-dark)' },\n turquoise: { bg: 'var(--color-turquoise-6)', text: 'var(--on-emphasis-dark)' },\n indigo: { bg: 'var(--color-indigo-6)', text: 'var(--on-emphasis)' },\n purple: { bg: 'var(--color-purple-6)', text: 'var(--on-emphasis)' },\n magenta: { bg: 'var(--color-magenta-6)', text: 'var(--on-emphasis)' },\n}\n\n/** Calendar event tile:subtle 底 + hover step-2(1:1)。 */\nexport const CAT_EVENT: Record<CategoricalHue, string> = {\n blue: 'bg-[var(--color-blue-1)] text-[var(--color-blue-7)] hover:bg-[var(--color-blue-2)]',\n green: 'bg-[var(--color-green-1)] text-[var(--color-green-7)] hover:bg-[var(--color-green-2)]',\n 'deep-orange': 'bg-[var(--color-deep-orange-1)] text-[var(--color-deep-orange-7)] hover:bg-[var(--color-deep-orange-2)]',\n yellow: 'bg-[var(--color-yellow-1)] text-[var(--color-yellow-7)] hover:bg-[var(--color-yellow-2)]',\n red: 'bg-[var(--color-red-1)] text-[var(--color-red-7)] hover:bg-[var(--color-red-2)]',\n orange: 'bg-[var(--color-orange-1)] text-[var(--color-orange-7)] hover:bg-[var(--color-orange-2)]',\n amber: 'bg-[var(--color-amber-1)] text-[var(--color-amber-7)] hover:bg-[var(--color-amber-2)]',\n lime: 'bg-[var(--color-lime-1)] text-[var(--color-lime-7)] hover:bg-[var(--color-lime-2)]',\n turquoise: 'bg-[var(--color-turquoise-1)] text-[var(--color-turquoise-7)] hover:bg-[var(--color-turquoise-2)]',\n indigo: 'bg-[var(--color-indigo-1)] text-[var(--color-indigo-7)] hover:bg-[var(--color-indigo-2)]',\n purple: 'bg-[var(--color-purple-1)] text-[var(--color-purple-7)] hover:bg-[var(--color-purple-2)]',\n magenta: 'bg-[var(--color-magenta-1)] text-[var(--color-magenta-7)] hover:bg-[var(--color-magenta-2)]',\n}\n\n/** Calendar all-day accent:左側 step-6 實心條。 */\nexport const CAT_ACCENT: Record<CategoricalHue, string> = {\n blue: 'border-l-[3px] border-[var(--color-blue-6)]',\n green: 'border-l-[3px] border-[var(--color-green-6)]',\n 'deep-orange': 'border-l-[3px] border-[var(--color-deep-orange-6)]',\n yellow: 'border-l-[3px] border-[var(--color-yellow-6)]',\n red: 'border-l-[3px] border-[var(--color-red-6)]',\n orange: 'border-l-[3px] border-[var(--color-orange-6)]',\n amber: 'border-l-[3px] border-[var(--color-amber-6)]',\n lime: 'border-l-[3px] border-[var(--color-lime-6)]',\n turquoise: 'border-l-[3px] border-[var(--color-turquoise-6)]',\n indigo: 'border-l-[3px] border-[var(--color-indigo-6)]',\n purple: 'border-l-[3px] border-[var(--color-purple-6)]',\n magenta: 'border-l-[3px] border-[var(--color-magenta-6)]',\n}\n"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"categorical-color.js","sources":["../../src/tokens/categorical-color.ts"],"sourcesContent":["/**\n * categorical-color.ts — Categorical 色相 SSOT(2026-06-04 codify)\n *\n * Tag / Avatar / Calendar 等「以色相做分類標籤」的元件,色彩 variant **一律消費此 SSOT**。\n * 目的:**variant 名 == primitive 色相名,1:1,零 offset** —— 根治「red variant 接到 deep-orange」\n * 這類 categorical-vs-semantic 混淆(2026-06-04 user 抓:red = 品牌紅 hue-25〔--brand #DF3232 同族〕\n * ≠ deep-orange hue-38 ≠ semantic --error〔= deep-orange〕。三者完全無關)。\n *\n * 世界級對照:\n * - Ant Design Tag preset colors —— 用原始色相名(red/orange/green/blue…)做 categorical labeling\n * (https://ant.design/components/tag/)。本 DS Tag/Avatar 採此模型。\n * - Atlassian Color 原則 —— **categorical(裝飾)色 vs semantic(狀態)色必須分離**\n * (https://atlassian.design/foundations/color):categorical `red` 用 `--color-red-*`,跟 semantic\n * `--error`(= deep-orange)無關;把 error 色拿來當 categorical 標籤 = 反 pattern。\n *\n * ⚠️ Tailwind v4 掃描限制:class 字串**必須以字面形式出現在被掃描的 source**(本檔在 src/,會被掃),\n * 不可用 runtime function 拼字串(否則 `bg-[var(--color-orange-1)]` 不被生成 → silent 失效)。\n * 故本 SSOT 以**字面 const map** 提供;consumer `import` 後索引(cva 列 key 取值,保 VariantProps 型別)。\n *\n * 互動 state(solid dismiss hover/active)用 semantic `--{hue}-hover` / `--{hue}-active`\n * (dark-mode 方向 pre-computed,**不可**用 primitive step-5,否則 dark mode 反向)。12 色相皆已定義。\n *\n * **加色相**:加進 `CATEGORICAL_HUES` + 下列 5 個 map 各補一行字面值(key X 用 `--color-X-*`,1:1)。\n * **機械防線**:`scripts/categorical-color-invariants.mjs` I1(current required CI / deterministic audit chain)強制 map 的 key X 的值含 `--color-X-`(名實一致,禁 offset;原引 hook 名不存在,2026-07-04 修)。\n */\n\n/** 12 個 categorical 色相。⚠️ 順序 = **categorical 指派序**(圖表系列鄰色最大區辨:blue→green→deep-orange→yellow→red…),**非** primitives.css 檔內宣告序 — 兩者本就不同,map 以名為 key 故功能無涉;**禁**為「對齊」重排(重排 = 圖表系列換色 = 視覺 breaking)。 */\nexport const CATEGORICAL_HUES = [\n 'blue',\n 'green',\n 'deep-orange',\n 'yellow',\n 'red',\n 'orange',\n 'amber',\n 'lime',\n 'turquoise',\n 'indigo',\n 'purple',\n 'magenta',\n] as const\n\nexport type CategoricalHue = (typeof CATEGORICAL_HUES)[number]\n\n/** neutral 不是色相(無 hue),用 secondary/foreground + neutral-9 solid,各 consumer 自處理(不在下列 map)。 */\nexport type CategoricalColor = CategoricalHue | 'neutral'\n\n/** subtle 模式 class:step-1 底 + step-7 字。key X 一律對 `--color-X-*`(1:1)。 */\nexport const CAT_SUBTLE: Record<CategoricalHue, string> = {\n blue: 'bg-[var(--color-blue-1)] text-[var(--color-blue-7)]',\n green: 'bg-[var(--color-green-1)] text-[var(--color-green-7)]',\n 'deep-orange': 'bg-[var(--color-deep-orange-1)] text-[var(--color-deep-orange-7)]',\n yellow: 'bg-[var(--color-yellow-1)] text-[var(--color-yellow-7)]',\n red: 'bg-[var(--color-red-1)] text-[var(--color-red-7)]',\n orange: 'bg-[var(--color-orange-1)] text-[var(--color-orange-7)]',\n amber: 'bg-[var(--color-amber-1)] text-[var(--color-amber-7)]',\n lime: 'bg-[var(--color-lime-1)] text-[var(--color-lime-7)]',\n turquoise: 'bg-[var(--color-turquoise-1)] text-[var(--color-turquoise-7)]',\n indigo: 'bg-[var(--color-indigo-1)] text-[var(--color-indigo-7)]',\n purple: 'bg-[var(--color-purple-1)] text-[var(--color-purple-7)]',\n magenta: 'bg-[var(--color-magenta-1)] text-[var(--color-magenta-7)]',\n}\n\n/**\n * solid 模式 class:step-6 底 + on-emphasis 配對文字。\n * 文字色依「step-6 底亮度」分桶(WCAG ≥3:1 大粗字門檻,2026-06-04 user「以最低為原則」):\n * - 夠深的色底 → `text-on-emphasis`(白):blue / deep-orange / red / indigo / purple / magenta\n * - 太亮的色底 → `text-on-emphasis-dark`(深):yellow / amber / orange / lime / turquoise(白字連 3:1 都不過;turquoise 2026-07-16 dark 色相修正後 L=0.68 白字 2.77 → 切深字,兩 mode 5.65/6.47 全過)\n * - ★ green 例外:白字 2.47 連 3:1 都不過,但 user 拍板維持白字(綠底白字慣見觀感)= documented exception。\n * 對比為 oklch→相對亮度實測(scripts/categorical-color-invariants.mjs 機械驗,green 列 exempt)。\n */\nexport const CAT_SOLID: Record<CategoricalHue, string> = {\n blue: 'bg-[var(--color-blue-6)] text-on-emphasis',\n green: 'bg-[var(--color-green-6)] text-on-emphasis', // ★白字 documented exception(實測 2.47 < 3:1)\n 'deep-orange': 'bg-[var(--color-deep-orange-6)] text-on-emphasis',\n yellow: 'bg-[var(--color-yellow-6)] text-on-emphasis-dark',\n red: 'bg-[var(--color-red-6)] text-on-emphasis',\n orange: 'bg-[var(--color-orange-6)] text-on-emphasis-dark',\n amber: 'bg-[var(--color-amber-6)] text-on-emphasis-dark',\n lime: 'bg-[var(--color-lime-6)] text-on-emphasis-dark',\n turquoise: 'bg-[var(--color-turquoise-6)] text-on-emphasis-dark',\n indigo: 'bg-[var(--color-indigo-6)] text-on-emphasis',\n purple: 'bg-[var(--color-purple-6)] text-on-emphasis',\n magenta: 'bg-[var(--color-magenta-6)] text-on-emphasis',\n}\n\n/** solid 互動(dismiss hover/active)— semantic dark-mode-aware token。 */\nexport const CAT_INTERACT: Record<CategoricalHue, { hover: string; active: string }> = {\n blue: { hover: 'var(--blue-hover)', active: 'var(--blue-active)' },\n green: { hover: 'var(--green-hover)', active: 'var(--green-active)' },\n 'deep-orange': { hover: 'var(--deep-orange-hover)', active: 'var(--deep-orange-active)' },\n yellow: { hover: 'var(--yellow-hover)', active: 'var(--yellow-active)' },\n red: { hover: 'var(--red-hover)', active: 'var(--red-active)' },\n orange: { hover: 'var(--orange-hover)', active: 'var(--orange-active)' },\n amber: { hover: 'var(--amber-hover)', active: 'var(--amber-active)' },\n lime: { hover: 'var(--lime-hover)', active: 'var(--lime-active)' },\n turquoise: { hover: 'var(--turquoise-hover)', active: 'var(--turquoise-active)' },\n indigo: { hover: 'var(--indigo-hover)', active: 'var(--indigo-active)' },\n purple: { hover: 'var(--purple-hover)', active: 'var(--purple-active)' },\n magenta: { hover: 'var(--magenta-hover)', active: 'var(--magenta-active)' },\n}\n\n/** Avatar style 物件用:subtle / solid 的 raw token { bg, text }(非 class)。 */\nexport const CAT_SUBTLE_TOKENS: Record<CategoricalHue, { bg: string; text: string }> = {\n blue: { bg: 'var(--color-blue-1)', text: 'var(--color-blue-7)' },\n green: { bg: 'var(--color-green-1)', text: 'var(--color-green-7)' },\n 'deep-orange': { bg: 'var(--color-deep-orange-1)', text: 'var(--color-deep-orange-7)' },\n yellow: { bg: 'var(--color-yellow-1)', text: 'var(--color-yellow-7)' },\n red: { bg: 'var(--color-red-1)', text: 'var(--color-red-7)' },\n orange: { bg: 'var(--color-orange-1)', text: 'var(--color-orange-7)' },\n amber: { bg: 'var(--color-amber-1)', text: 'var(--color-amber-7)' },\n lime: { bg: 'var(--color-lime-1)', text: 'var(--color-lime-7)' },\n turquoise: { bg: 'var(--color-turquoise-1)', text: 'var(--color-turquoise-7)' },\n indigo: { bg: 'var(--color-indigo-1)', text: 'var(--color-indigo-7)' },\n purple: { bg: 'var(--color-purple-1)', text: 'var(--color-purple-7)' },\n magenta: { bg: 'var(--color-magenta-1)', text: 'var(--color-magenta-7)' },\n}\n// 同 CAT_SOLID 的文字分桶(白 / 深),raw token 版供 Avatar style 物件用。green = ★白字 exception。\nexport const CAT_SOLID_TOKENS: Record<CategoricalHue, { bg: string; text: string }> = {\n blue: { bg: 'var(--color-blue-6)', text: 'var(--on-emphasis)' },\n green: { bg: 'var(--color-green-6)', text: 'var(--on-emphasis)' }, // ★白字 documented exception\n 'deep-orange': { bg: 'var(--color-deep-orange-6)', text: 'var(--on-emphasis)' },\n yellow: { bg: 'var(--color-yellow-6)', text: 'var(--on-emphasis-dark)' },\n red: { bg: 'var(--color-red-6)', text: 'var(--on-emphasis)' },\n orange: { bg: 'var(--color-orange-6)', text: 'var(--on-emphasis-dark)' },\n amber: { bg: 'var(--color-amber-6)', text: 'var(--on-emphasis-dark)' },\n lime: { bg: 'var(--color-lime-6)', text: 'var(--on-emphasis-dark)' },\n turquoise: { bg: 'var(--color-turquoise-6)', text: 'var(--on-emphasis-dark)' },\n indigo: { bg: 'var(--color-indigo-6)', text: 'var(--on-emphasis)' },\n purple: { bg: 'var(--color-purple-6)', text: 'var(--on-emphasis)' },\n magenta: { bg: 'var(--color-magenta-6)', text: 'var(--on-emphasis)' },\n}\n\n/**\n * Calendar event tile:subtle 底 + hover step-2(1:1)。\n * 淡底的滑過 = 同色離底更遠一格:淺色變深、深色變亮(色階號碼 = 離所在底色多遠;primitives.css 檔頭、\n * color.spec.md「Dark mode subtle」)。與上方實心色互動 state 不同 —— 那是 semantic `--{hue}-hover`(深色要反轉),\n * 這裡是淡底區 step-1 → step-2,兩主題同向,直接用 primitive(2026-09-26 深色 step-2..4 改半透明淡底後成立)。\n */\nexport const CAT_EVENT: Record<CategoricalHue, string> = {\n blue: 'bg-[var(--color-blue-1)] text-[var(--color-blue-7)] hover:bg-[var(--color-blue-2)]',\n green: 'bg-[var(--color-green-1)] text-[var(--color-green-7)] hover:bg-[var(--color-green-2)]',\n 'deep-orange': 'bg-[var(--color-deep-orange-1)] text-[var(--color-deep-orange-7)] hover:bg-[var(--color-deep-orange-2)]',\n yellow: 'bg-[var(--color-yellow-1)] text-[var(--color-yellow-7)] hover:bg-[var(--color-yellow-2)]',\n red: 'bg-[var(--color-red-1)] text-[var(--color-red-7)] hover:bg-[var(--color-red-2)]',\n orange: 'bg-[var(--color-orange-1)] text-[var(--color-orange-7)] hover:bg-[var(--color-orange-2)]',\n amber: 'bg-[var(--color-amber-1)] text-[var(--color-amber-7)] hover:bg-[var(--color-amber-2)]',\n lime: 'bg-[var(--color-lime-1)] text-[var(--color-lime-7)] hover:bg-[var(--color-lime-2)]',\n turquoise: 'bg-[var(--color-turquoise-1)] text-[var(--color-turquoise-7)] hover:bg-[var(--color-turquoise-2)]',\n indigo: 'bg-[var(--color-indigo-1)] text-[var(--color-indigo-7)] hover:bg-[var(--color-indigo-2)]',\n purple: 'bg-[var(--color-purple-1)] text-[var(--color-purple-7)] hover:bg-[var(--color-purple-2)]',\n magenta: 'bg-[var(--color-magenta-1)] text-[var(--color-magenta-7)] hover:bg-[var(--color-magenta-2)]',\n}\n\n/** Calendar all-day accent:左側 step-6 實心條。 */\nexport const CAT_ACCENT: Record<CategoricalHue, string> = {\n blue: 'border-l-[3px] border-[var(--color-blue-6)]',\n green: 'border-l-[3px] border-[var(--color-green-6)]',\n 'deep-orange': 'border-l-[3px] border-[var(--color-deep-orange-6)]',\n yellow: 'border-l-[3px] border-[var(--color-yellow-6)]',\n red: 'border-l-[3px] border-[var(--color-red-6)]',\n orange: 'border-l-[3px] border-[var(--color-orange-6)]',\n amber: 'border-l-[3px] border-[var(--color-amber-6)]',\n lime: 'border-l-[3px] border-[var(--color-lime-6)]',\n turquoise: 'border-l-[3px] border-[var(--color-turquoise-6)]',\n indigo: 'border-l-[3px] border-[var(--color-indigo-6)]',\n purple: 'border-l-[3px] border-[var(--color-purple-6)]',\n magenta: 'border-l-[3px] border-[var(--color-magenta-6)]',\n}\n"],"names":[],"mappings":"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"}
|
|
@@ -36,7 +36,7 @@
|
|
|
36
36
|
* 3. Audit Dim「Icon size literal compliance」DS-wide audit-time scan(write-time hook 尚未實作 — 改靠 audit-time + mindset)
|
|
37
37
|
*
|
|
38
38
|
* ── Carve-out(不適用本 SSOT)──
|
|
39
|
-
* - `rating.spec.md` Rating
|
|
39
|
+
* - `rating.spec.md` Rating 可以點的星 20/24/24(identity scale;出處 Ant 20 / MUI 24 在該 spec「為什麼不完全對齊 icon tier」;唯讀精簡版「★ 數值」的星走本 tier)
|
|
40
40
|
* - `avatar.spec.md` Avatar 內 icon `round_even(size × 0.6)`(Material/Apple HIG)
|
|
41
41
|
* - `empty.tsx` Empty 用 Avatar 48 wrap → icon 28(Avatar formula derived)
|
|
42
42
|
* - `file-viewer.tsx` thumb 64 → icon 20(thumbnail file-type indicator,hardcode 無公式)
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Stack gap — 「疊在一起的兩樣東西」之間的縫,全 DS 同一個值。
|
|
3
|
+
*
|
|
4
|
+
* CSS 雙生:`tokens/uiSize/uiSize.css` `--stack-gap`(2px)。兩邊必須相等,由
|
|
5
|
+
* `scripts/token-twin-invariant.mjs` 機械鎖住(改值時兩邊一起改;只改一邊 CI 會紅)。
|
|
6
|
+
*
|
|
7
|
+
* 為什麼要有 JS 版:遮罩幾何(avatar.tsx 挖空的半徑、file-item.tsx 焦點框挖空的縫)與
|
|
8
|
+
* `outline-offset`(steps.tsx 外圈)要拿數字去算,CSS 變數在那些地方進不去 calc 之外的運算。
|
|
9
|
+
*
|
|
10
|
+
* 消費者(2026-09-26,M17 抽 token 前這個 2 散在 6 處字面值):
|
|
11
|
+
* - styles/base.css `:focus-visible` outline-offset(直接用 CSS 變數)
|
|
12
|
+
* - avatar.tsx `AVATAR_STACK_GAP_PX`、狀態圓點 / 計數徽章的挖空縫
|
|
13
|
+
* - file-item.tsx 進度條與焦點框之間的縫
|
|
14
|
+
* - steps.tsx 外圈與圓之間的縫
|
|
15
|
+
* 設計理由住 avatar.spec.md「頭像堆疊(疊在一起時)」段(「縫寬 2px 是 AI 推導」那段),本檔不重述。
|
|
16
|
+
*/
|
|
17
|
+
export declare const STACK_GAP_PX = 2;
|
|
18
|
+
//# sourceMappingURL=stack-gap.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"stack-gap.d.ts","sourceRoot":"","sources":["../../../src/tokens/uiSize/stack-gap.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AACH,eAAO,MAAM,YAAY,IAAI,CAAA"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"stack-gap.js","sources":["../../../src/tokens/uiSize/stack-gap.ts"],"sourcesContent":["/**\n * Stack gap — 「疊在一起的兩樣東西」之間的縫,全 DS 同一個值。\n *\n * CSS 雙生:`tokens/uiSize/uiSize.css` `--stack-gap`(2px)。兩邊必須相等,由\n * `scripts/token-twin-invariant.mjs` 機械鎖住(改值時兩邊一起改;只改一邊 CI 會紅)。\n *\n * 為什麼要有 JS 版:遮罩幾何(avatar.tsx 挖空的半徑、file-item.tsx 焦點框挖空的縫)與\n * `outline-offset`(steps.tsx 外圈)要拿數字去算,CSS 變數在那些地方進不去 calc 之外的運算。\n *\n * 消費者(2026-09-26,M17 抽 token 前這個 2 散在 6 處字面值):\n * - styles/base.css `:focus-visible` outline-offset(直接用 CSS 變數)\n * - avatar.tsx `AVATAR_STACK_GAP_PX`、狀態圓點 / 計數徽章的挖空縫\n * - file-item.tsx 進度條與焦點框之間的縫\n * - steps.tsx 外圈與圓之間的縫\n * 設計理由住 avatar.spec.md「頭像堆疊(疊在一起時)」段(「縫寬 2px 是 AI 推導」那段),本檔不重述。\n */\nexport const STACK_GAP_PX = 2\n"],"names":[],"mappings":"AAgBO,MAAM,eAAe;"}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"description": "_generated: build-fork-governance.mjs; provider=codex; sourceDigest=
|
|
2
|
+
"description": "_generated: build-fork-governance.mjs; provider=codex; sourceDigest=a22cf76b2776fa4b8c71cc08f3b470b07997e0b4f31faf80ec7fd735eae7d8a4; exact npm corpus; native hooks are feedback only, provider-neutral check + protected CI are authoritative.",
|
|
3
3
|
"hooks": {
|
|
4
4
|
"SessionStart": [
|
|
5
5
|
{
|
|
@@ -3,8 +3,8 @@
|
|
|
3
3
|
"schemaVersion": 1,
|
|
4
4
|
"role": "template-consumer",
|
|
5
5
|
"release": {
|
|
6
|
-
"designSystem": "0.1.0-beta.
|
|
7
|
-
"storybookConfig": "0.1.0-beta.
|
|
6
|
+
"designSystem": "0.1.0-beta.146",
|
|
7
|
+
"storybookConfig": "0.1.0-beta.146"
|
|
8
8
|
},
|
|
9
9
|
"upgradeTrust": {
|
|
10
10
|
"schemaVersion": 1,
|
|
@@ -42,10 +42,10 @@
|
|
|
42
42
|
"implementationSha256": "98b74118fb5b410b77ad2994aa9105fa7609c7353822f8c5213cce54d94f0af4"
|
|
43
43
|
},
|
|
44
44
|
"payload": {
|
|
45
|
-
"forkCorpusLockSha256": "
|
|
45
|
+
"forkCorpusLockSha256": "cfd12e8858e90c88de1b50d2fc4853c4d76691e4e0b20979b4749cced70f1c30",
|
|
46
46
|
"discoveryPolicySha256": "fba289aeeb5a3f11139b9589f7ab08b5e8c8799c7bdf8f0ea57ebfeb8e7c9da6",
|
|
47
47
|
"providerMaterializersSha256": "732e85b01a12e7dd19469e26e4427f0e0cfe5117a78fc9f69c2f0f89702f463a",
|
|
48
|
-
"providerLifecycleSha256": "
|
|
48
|
+
"providerLifecycleSha256": "b8167162d63414c71a4cdcdfa8461113a3c195bd5ce5f077eea3ae4528d97e07",
|
|
49
49
|
"providerInventorySha256": "00adf1c4f6b3367055a9cc3fdfe658660771b4c9931f696e12761a95076ad92c",
|
|
50
50
|
"executionRuntimeSha256": "d9377cc45308731f287d7b1711b7c38fc4e02ff8f84729bcbbf01bb52d136904",
|
|
51
51
|
"commonInstructionSha256": "e730127bff1820a035b11c3b3119e7354975d7e9c8c69d7e8b1adfe307c53ecc",
|
|
@@ -58,7 +58,7 @@
|
|
|
58
58
|
},
|
|
59
59
|
"codex": {
|
|
60
60
|
"instructionSha256": "e730127bff1820a035b11c3b3119e7354975d7e9c8c69d7e8b1adfe307c53ecc",
|
|
61
|
-
"hookSha256": "
|
|
61
|
+
"hookSha256": "94bc0e9e2f6cdbcb211495ee776943b71ef3fc44ece562f6ee152a810cb8a97d",
|
|
62
62
|
"skillsSha256": "5f8c2acd654e1fd22ed596c84032074f1bb844522fa1bc29db1c1462e77764d7",
|
|
63
63
|
"treeDigests": {}
|
|
64
64
|
},
|
|
@@ -101,16 +101,19 @@
|
|
|
101
101
|
"governance/trust/issuers.json": "b4da935a9d39239f99fdb15f9ee0a05b538ac0c427b32b77fce8010560fdef2b",
|
|
102
102
|
"governance/visual-baseline-review-policy.json": "339179f3bb7805f067c40cb22509734fa3f1cf2834129e64f6e0bf08aa70b334",
|
|
103
103
|
"schemas/provider-cli-toolchain.schema.json": "7e5c6c14c8a9295373f56df968afdb8cb8888764ce1b060993a655ccec8ac61d",
|
|
104
|
-
"scripts/audit-consumer-a11y.mjs": "
|
|
104
|
+
"scripts/audit-consumer-a11y.mjs": "648b65cedabb5a21538487da857c0398621f2a8ce5da53da7bce534499def467",
|
|
105
105
|
"scripts/check-plugin-installed.mjs": "f2a673cc011743fe51f87746b9aebbd3332378cd2732088eda621e16a3c74386",
|
|
106
|
-
"scripts/consumer-source-harness.mjs": "
|
|
107
|
-
"scripts/lib/a11y-static-server.mjs": "
|
|
106
|
+
"scripts/consumer-source-harness.mjs": "aad4e8ba31f3efbead67700bcff05855dc89cab6f562943d61aae3842cc2276f",
|
|
107
|
+
"scripts/lib/a11y-static-server.mjs": "c22322959d2898bc8ebb6c95077bd4c97499f49a690e1fe00f2220e8f809f4ef",
|
|
108
108
|
"scripts/lib/canonical-path-containment.mjs": "0244e31dc3be5015bed3efd65a5a77086a2e4676e9ac79cd0801c8cde6a74fcd",
|
|
109
109
|
"scripts/lib/closed-tool-execution.mjs": "ed3af261d18a119e13089ce6eea0f571d8cfc0f7b438746c3c85a24de0439b22",
|
|
110
110
|
"scripts/lib/consumer-control-plane-policy.mjs": "51aba211bb2f755eff7fa48bb060f9835356a62d2643ab5049147a9896c7bb72",
|
|
111
111
|
"scripts/lib/exact-workspace-dependencies.mjs": "b4921ad903a3ed3d901187d406a87270190945c5ca1874b089e80df898d47a97",
|
|
112
112
|
"scripts/lib/governance-dependency-bootstrap.mjs": "82c17173dfe06c5f28e961973d7320fee35ab3b43921a23807c5c724f99f7724",
|
|
113
|
+
"scripts/lib/launch-browser.mjs": "95fdb808ae067e5a9b1167c5260c9defb165077f695b1324be8f2f356a6db4c0",
|
|
113
114
|
"scripts/lib/provider-lifecycle.mjs": "9aabb2c98efc454b8555308a34830e6590b034b10a63f76f788fbd4cf37ce7e7",
|
|
115
|
+
"scripts/lib/storybook-render-health.mjs": "583beac441f2f2bc88630da73a00d38bfb4da0b1c1e11152b1e9a87030837418",
|
|
116
|
+
"scripts/lib/storybook-static-snapshot.mjs": "a6ed23c4285d57ded8b4943d276cb2b71bc1a545ab867b80c06eda7d59ea48b9",
|
|
114
117
|
"scripts/lib/verified-exact-npm-runtime.mjs": "811ed3390a4ff6b4e818a5ce8b5c0ce34e10f48fb443b6177d6e4c8e14fa9670",
|
|
115
118
|
"scripts/lib/workspace-post-create.mjs": "f3ddc6db740d5c4d48b1c6e16c55222325e4d1c16fe6644a455e7b7e5bea0a0d",
|
|
116
119
|
"scripts/lib/worktree-fingerprint.mjs": "a2fe922b3be6ff1b782ffc7e76b0ebb61b3691ba9b46768c0d0349913228ce4a",
|
|
@@ -30,10 +30,19 @@ export const CANONICAL_WORKSPACE_BUILD_SCRIPTS = Object.freeze({
|
|
|
30
30
|
build: 'tsc -b && vite build',
|
|
31
31
|
typecheck: 'tsc --noEmit',
|
|
32
32
|
})
|
|
33
|
+
// 必須是**相對 import 閉包**:staged 的檢查器只看得到這裡列的檔。a11y-static-server 的依賴
|
|
34
|
+
//(canonical-path-containment 自 2026-07-28、storybook-static-snapshot 自 2026-09-24)與 audit-consumer-a11y 的
|
|
35
|
+
// openStory(launch-browser → storybook-render-health,2026-09-25)漏列時,staged 的 a11y 檢查器在 import 階段就
|
|
36
|
+
// ERR_MODULE_NOT_FOUND(2026-09-25 以 --stage-trusted-product-checks 實跑重現)。閉包由 test-consumer-source-harness 斷言。
|
|
33
37
|
export const TRUSTED_PRODUCT_CHECK_FILES = Object.freeze([
|
|
34
38
|
'scripts/audit-consumer-a11y.mjs',
|
|
35
39
|
'scripts/consumer-source-harness.mjs',
|
|
36
40
|
'scripts/lib/a11y-static-server.mjs',
|
|
41
|
+
'scripts/lib/canonical-path-containment.mjs',
|
|
42
|
+
'scripts/lib/launch-browser.mjs',
|
|
43
|
+
'scripts/lib/provider-lifecycle.mjs',
|
|
44
|
+
'scripts/lib/storybook-render-health.mjs',
|
|
45
|
+
'scripts/lib/storybook-static-snapshot.mjs',
|
|
37
46
|
'scripts/lint-ds-internal-imports.mjs',
|
|
38
47
|
])
|
|
39
48
|
export const TRUSTED_PRODUCT_RUNTIME_FILES = Object.freeze([
|