@qijenchen/design-system 0.1.0-beta.145 → 0.1.0-beta.147
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/components/Accordion/accordion.d.ts +1 -1
- package/dist/components/Accordion/accordion.d.ts.map +1 -1
- package/dist/components/Accordion/accordion.js +8 -3
- package/dist/components/Accordion/accordion.js.map +1 -1
- package/dist/components/AccountMenu/account-menu.d.ts.map +1 -1
- package/dist/components/AccountMenu/account-menu.js.map +1 -1
- package/dist/components/AgentPanel/agent-panel-fab.d.ts +7 -1
- package/dist/components/AgentPanel/agent-panel-fab.d.ts.map +1 -1
- package/dist/components/AgentPanel/agent-panel-fab.js +36 -14
- package/dist/components/AgentPanel/agent-panel-fab.js.map +1 -1
- package/dist/components/AgentPanel/agent-panel-logo.d.ts +3 -3
- package/dist/components/AgentPanel/agent-panel-logo.js.map +1 -1
- package/dist/components/AgentPanel/agent-panel.d.ts +1 -1
- package/dist/components/AgentPanel/agent-panel.d.ts.map +1 -1
- package/dist/components/AgentPanel/agent-panel.js +28 -22
- package/dist/components/AgentPanel/agent-panel.js.map +1 -1
- package/dist/components/AppShell/app-shell.d.ts +1 -1
- package/dist/components/AppShell/app-shell.js +1 -1
- package/dist/components/AppShell/app-shell.js.map +1 -1
- package/dist/components/Avatar/avatar.d.ts +95 -1
- package/dist/components/Avatar/avatar.d.ts.map +1 -1
- package/dist/components/Avatar/avatar.js +106 -15
- package/dist/components/Avatar/avatar.js.map +1 -1
- package/dist/components/Avatar/index.js +14 -2
- package/dist/components/Breadcrumb/breadcrumb.d.ts +4 -2
- package/dist/components/Breadcrumb/breadcrumb.d.ts.map +1 -1
- package/dist/components/Breadcrumb/breadcrumb.js +2 -2
- package/dist/components/Breadcrumb/breadcrumb.js.map +1 -1
- package/dist/components/BulkActionBar/bulk-action-bar.d.ts.map +1 -1
- package/dist/components/BulkActionBar/bulk-action-bar.js +27 -2
- package/dist/components/BulkActionBar/bulk-action-bar.js.map +1 -1
- package/dist/components/Button/button.d.ts +2 -2
- package/dist/components/Button/button.d.ts.map +1 -1
- package/dist/components/Button/button.js +118 -22
- package/dist/components/Button/button.js.map +1 -1
- package/dist/components/Calendar/calendar.d.ts +34 -10
- package/dist/components/Calendar/calendar.d.ts.map +1 -1
- package/dist/components/Calendar/calendar.js +210 -36
- package/dist/components/Calendar/calendar.js.map +1 -1
- package/dist/components/Carousel/carousel.d.ts +1 -1
- package/dist/components/Carousel/carousel.d.ts.map +1 -1
- package/dist/components/Carousel/carousel.js +3 -2
- package/dist/components/Carousel/carousel.js.map +1 -1
- package/dist/components/Chart/chart.d.ts +1 -0
- package/dist/components/Chart/chart.d.ts.map +1 -1
- package/dist/components/Chart/chart.js +8 -0
- package/dist/components/Chart/chart.js.map +1 -1
- package/dist/components/Checkbox/checkbox.d.ts +1 -1
- package/dist/components/Checkbox/checkbox.d.ts.map +1 -1
- package/dist/components/Checkbox/checkbox.js +10 -2
- package/dist/components/Checkbox/checkbox.js.map +1 -1
- package/dist/components/Chip/chip.d.ts +1 -1
- package/dist/components/Chip/chip.d.ts.map +1 -1
- package/dist/components/Chip/chip.js +4 -2
- package/dist/components/Chip/chip.js.map +1 -1
- package/dist/components/Combobox/combobox.d.ts +1 -1
- package/dist/components/Combobox/combobox.d.ts.map +1 -1
- package/dist/components/Combobox/combobox.js +37 -11
- package/dist/components/Combobox/combobox.js.map +1 -1
- package/dist/components/Command/command.d.ts +1 -1
- package/dist/components/Command/command.d.ts.map +1 -1
- package/dist/components/Command/command.js +106 -8
- package/dist/components/Command/command.js.map +1 -1
- package/dist/components/DataTable/cell-registry.d.ts.map +1 -1
- package/dist/components/DataTable/cell-registry.js +3 -2
- package/dist/components/DataTable/cell-registry.js.map +1 -1
- package/dist/components/DataTable/data-table-column-visibility-panel.d.ts.map +1 -1
- package/dist/components/DataTable/data-table-column-visibility-panel.js +24 -24
- package/dist/components/DataTable/data-table-column-visibility-panel.js.map +1 -1
- package/dist/components/DataTable/data-table-interaction-layer.d.ts +2 -1
- package/dist/components/DataTable/data-table-interaction-layer.d.ts.map +1 -1
- package/dist/components/DataTable/data-table-interaction-layer.js +3 -4
- package/dist/components/DataTable/data-table-interaction-layer.js.map +1 -1
- package/dist/components/DataTable/data-table-sort-manager.d.ts.map +1 -1
- package/dist/components/DataTable/data-table-sort-manager.js +20 -20
- package/dist/components/DataTable/data-table-sort-manager.js.map +1 -1
- package/dist/components/DataTable/data-table.d.ts +8 -5
- package/dist/components/DataTable/data-table.d.ts.map +1 -1
- package/dist/components/DataTable/data-table.js +197 -71
- package/dist/components/DataTable/data-table.js.map +1 -1
- package/dist/components/DateGrid/date-grid.d.ts +1 -1
- package/dist/components/DateGrid/date-grid.d.ts.map +1 -1
- package/dist/components/DateGrid/date-grid.js +358 -225
- package/dist/components/DateGrid/date-grid.js.map +1 -1
- package/dist/components/DatePicker/date-picker.d.ts.map +1 -1
- package/dist/components/DatePicker/date-picker.js +16 -1
- package/dist/components/DatePicker/date-picker.js.map +1 -1
- package/dist/components/Dialog/dialog.d.ts.map +1 -1
- package/dist/components/Dialog/dialog.js +55 -43
- package/dist/components/Dialog/dialog.js.map +1 -1
- package/dist/components/DropdownMenu/dropdown-menu-keyboard.d.ts +86 -0
- package/dist/components/DropdownMenu/dropdown-menu-keyboard.d.ts.map +1 -0
- package/dist/components/DropdownMenu/dropdown-menu-keyboard.js +172 -0
- package/dist/components/DropdownMenu/dropdown-menu-keyboard.js.map +1 -0
- package/dist/components/DropdownMenu/dropdown-menu.d.ts +6 -1
- package/dist/components/DropdownMenu/dropdown-menu.d.ts.map +1 -1
- package/dist/components/DropdownMenu/dropdown-menu.js +50 -39
- package/dist/components/DropdownMenu/dropdown-menu.js.map +1 -1
- package/dist/components/Field/field-context.d.ts +4 -0
- package/dist/components/Field/field-context.d.ts.map +1 -1
- package/dist/components/Field/field-context.js +1 -2
- package/dist/components/Field/field-context.js.map +1 -1
- package/dist/components/Field/field-edit-keys.d.ts +8 -0
- package/dist/components/Field/field-edit-keys.d.ts.map +1 -1
- package/dist/components/Field/field-edit-keys.js +3 -0
- package/dist/components/Field/field-edit-keys.js.map +1 -1
- package/dist/components/Field/field-wrapper.d.ts +18 -1
- package/dist/components/Field/field-wrapper.d.ts.map +1 -1
- package/dist/components/Field/field-wrapper.js +39 -10
- package/dist/components/Field/field-wrapper.js.map +1 -1
- package/dist/components/Field/field.d.ts +3 -2
- package/dist/components/Field/field.d.ts.map +1 -1
- package/dist/components/Field/field.js +2 -1
- package/dist/components/Field/field.js.map +1 -1
- package/dist/components/FileItem/file-item.d.ts +26 -2
- package/dist/components/FileItem/file-item.d.ts.map +1 -1
- package/dist/components/FileItem/file-item.js +82 -25
- package/dist/components/FileItem/file-item.js.map +1 -1
- package/dist/components/FileItem/index.js +4 -2
- package/dist/components/FileUpload/file-upload.d.ts +1 -1
- package/dist/components/FileUpload/file-upload.d.ts.map +1 -1
- package/dist/components/FileUpload/file-upload.js +132 -55
- package/dist/components/FileUpload/file-upload.js.map +1 -1
- package/dist/components/FileViewer/file-viewer.d.ts +1 -1
- package/dist/components/FileViewer/file-viewer.d.ts.map +1 -1
- package/dist/components/FileViewer/file-viewer.js +9 -4
- package/dist/components/FileViewer/file-viewer.js.map +1 -1
- package/dist/components/InlineEdit/inline-edit.d.ts.map +1 -1
- package/dist/components/InlineEdit/inline-edit.js +1 -1
- package/dist/components/InlineEdit/inline-edit.js.map +1 -1
- package/dist/components/Input/input.d.ts.map +1 -1
- package/dist/components/Input/input.js +6 -1
- package/dist/components/Input/input.js.map +1 -1
- package/dist/components/LinkInput/link-input.d.ts.map +1 -1
- package/dist/components/LinkInput/link-input.js +20 -4
- package/dist/components/LinkInput/link-input.js.map +1 -1
- package/dist/components/Notice/notice.js +1 -1
- package/dist/components/Notice/notice.js.map +1 -1
- package/dist/components/NumberInput/number-input.d.ts.map +1 -1
- package/dist/components/NumberInput/number-input.js +6 -1
- package/dist/components/NumberInput/number-input.js.map +1 -1
- package/dist/components/OverflowIndicator/index.js +2 -1
- package/dist/components/OverflowIndicator/overflow-indicator.d.ts +19 -1
- package/dist/components/OverflowIndicator/overflow-indicator.d.ts.map +1 -1
- package/dist/components/OverflowIndicator/overflow-indicator.js +47 -27
- package/dist/components/OverflowIndicator/overflow-indicator.js.map +1 -1
- package/dist/components/Pagination/pagination.d.ts +1 -1
- package/dist/components/Pagination/pagination.js +1 -1
- package/dist/components/Pagination/pagination.js.map +1 -1
- package/dist/components/PeoplePicker/avatar-stack-overflow.d.ts +7 -6
- package/dist/components/PeoplePicker/avatar-stack-overflow.d.ts.map +1 -1
- package/dist/components/PeoplePicker/avatar-stack-overflow.js +6 -7
- package/dist/components/PeoplePicker/avatar-stack-overflow.js.map +1 -1
- package/dist/components/PeoplePicker/people-picker-helpers.d.ts.map +1 -1
- package/dist/components/PeoplePicker/people-picker-helpers.js +1 -1
- package/dist/components/PeoplePicker/people-picker-helpers.js.map +1 -1
- package/dist/components/PeoplePicker/people-picker.d.ts.map +1 -1
- package/dist/components/PeoplePicker/people-picker.js +2 -2
- package/dist/components/PeoplePicker/people-picker.js.map +1 -1
- package/dist/components/PeoplePicker/person-display.d.ts.map +1 -1
- package/dist/components/PeoplePicker/person-display.js +26 -28
- package/dist/components/PeoplePicker/person-display.js.map +1 -1
- package/dist/components/ProfileCard/profile-card.d.ts.map +1 -1
- package/dist/components/ProfileCard/profile-card.js.map +1 -1
- package/dist/components/RadioGroup/radio-group.d.ts +1 -1
- package/dist/components/RadioGroup/radio-group.d.ts.map +1 -1
- package/dist/components/RadioGroup/radio-group.js +10 -4
- package/dist/components/RadioGroup/radio-group.js.map +1 -1
- package/dist/components/Rating/rating.d.ts +28 -6
- package/dist/components/Rating/rating.d.ts.map +1 -1
- package/dist/components/Rating/rating.js +97 -80
- package/dist/components/Rating/rating.js.map +1 -1
- package/dist/components/ScrollArea/scroll-area.d.ts +1 -1
- package/dist/components/ScrollArea/scroll-area.js +1 -1
- package/dist/components/ScrollArea/scroll-area.js.map +1 -1
- package/dist/components/SegmentedControl/segmented-control.d.ts +1 -1
- package/dist/components/SegmentedControl/segmented-control.d.ts.map +1 -1
- package/dist/components/SegmentedControl/segmented-control.js +3 -2
- package/dist/components/SegmentedControl/segmented-control.js.map +1 -1
- package/dist/components/Select/select.d.ts.map +1 -1
- package/dist/components/Select/select.js +15 -8
- package/dist/components/Select/select.js.map +1 -1
- package/dist/components/SelectMenu/index.js +2 -1
- package/dist/components/SelectMenu/index.js.map +1 -1
- package/dist/components/SelectMenu/select-menu-keyboard.d.ts +59 -0
- package/dist/components/SelectMenu/select-menu-keyboard.d.ts.map +1 -0
- package/dist/components/SelectMenu/select-menu-keyboard.js +107 -0
- package/dist/components/SelectMenu/select-menu-keyboard.js.map +1 -0
- package/dist/components/SelectMenu/select-menu.d.ts +1 -22
- package/dist/components/SelectMenu/select-menu.d.ts.map +1 -1
- package/dist/components/SelectMenu/select-menu.js +18 -34
- package/dist/components/SelectMenu/select-menu.js.map +1 -1
- package/dist/components/SelectionControl/selection-item.d.ts.map +1 -1
- package/dist/components/SelectionControl/selection-item.js +1 -0
- package/dist/components/SelectionControl/selection-item.js.map +1 -1
- package/dist/components/Sidebar/sidebar.d.ts +7 -9
- package/dist/components/Sidebar/sidebar.d.ts.map +1 -1
- package/dist/components/Sidebar/sidebar.js +171 -50
- package/dist/components/Sidebar/sidebar.js.map +1 -1
- package/dist/components/Slider/slider.d.ts +1 -1
- package/dist/components/Slider/slider.d.ts.map +1 -1
- package/dist/components/Slider/slider.js +9 -4
- package/dist/components/Slider/slider.js.map +1 -1
- package/dist/components/Steps/steps.d.ts.map +1 -1
- package/dist/components/Steps/steps.js +54 -26
- package/dist/components/Steps/steps.js.map +1 -1
- package/dist/components/Switch/switch.d.ts +1 -1
- package/dist/components/Switch/switch.d.ts.map +1 -1
- package/dist/components/Switch/switch.js +7 -2
- package/dist/components/Switch/switch.js.map +1 -1
- package/dist/components/Tabs/tabs.d.ts +1 -1
- package/dist/components/Tabs/tabs.d.ts.map +1 -1
- package/dist/components/Tabs/tabs.js +39 -4
- package/dist/components/Tabs/tabs.js.map +1 -1
- package/dist/components/Tag/tag.d.ts +1 -1
- package/dist/components/Tag/tag.d.ts.map +1 -1
- package/dist/components/Tag/tag.js +3 -2
- package/dist/components/Tag/tag.js.map +1 -1
- package/dist/components/Textarea/textarea.d.ts.map +1 -1
- package/dist/components/Textarea/textarea.js +8 -2
- package/dist/components/Textarea/textarea.js.map +1 -1
- package/dist/components/TimePicker/time-columns.d.ts.map +1 -1
- package/dist/components/TimePicker/time-columns.js +8 -4
- package/dist/components/TimePicker/time-columns.js.map +1 -1
- package/dist/components/TimePicker/time-picker.d.ts +1 -1
- package/dist/components/TimePicker/time-picker.js +1 -1
- package/dist/components/TimePicker/time-picker.js.map +1 -1
- package/dist/components/TreeView/tree-view.d.ts +9 -5
- package/dist/components/TreeView/tree-view.d.ts.map +1 -1
- package/dist/components/TreeView/tree-view.js +266 -214
- package/dist/components/TreeView/tree-view.js.map +1 -1
- package/dist/hooks/use-input-modality.d.ts +12 -17
- package/dist/hooks/use-input-modality.d.ts.map +1 -1
- package/dist/hooks/use-input-modality.js +4 -34
- package/dist/hooks/use-input-modality.js.map +1 -1
- package/dist/hooks/use-overflow-items.d.ts.map +1 -1
- package/dist/hooks/use-overflow-items.js +2 -0
- package/dist/hooks/use-overflow-items.js.map +1 -1
- package/dist/hooks/use-truncated.d.ts.map +1 -1
- package/dist/hooks/use-truncated.js.map +1 -1
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +23 -3
- package/dist/index.js.map +1 -1
- package/dist/lib/drag-announcements.d.ts +32 -0
- package/dist/lib/drag-announcements.d.ts.map +1 -1
- package/dist/lib/drag-announcements.js +39 -1
- package/dist/lib/drag-announcements.js.map +1 -1
- package/dist/lib/focus-after-trigger.d.ts +45 -0
- package/dist/lib/focus-after-trigger.d.ts.map +1 -0
- package/dist/lib/focus-after-trigger.js +66 -0
- package/dist/lib/focus-after-trigger.js.map +1 -0
- package/dist/lib/overlay-coexistence.js +1 -1
- package/dist/lib/roving-list-keyboard.d.ts +208 -0
- package/dist/lib/roving-list-keyboard.d.ts.map +1 -0
- package/dist/lib/roving-list-keyboard.js +174 -0
- package/dist/lib/roving-list-keyboard.js.map +1 -0
- package/dist/lib/utils.d.ts.map +1 -1
- package/dist/lib/utils.js +6 -1
- package/dist/lib/utils.js.map +1 -1
- package/dist/patterns/element-anatomy/item-anatomy.d.ts +4 -4
- package/dist/patterns/element-anatomy/item-anatomy.d.ts.map +1 -1
- package/dist/patterns/element-anatomy/item-anatomy.js +14 -4
- package/dist/patterns/element-anatomy/item-anatomy.js.map +1 -1
- package/dist/patterns/horizontal-overflow/horizontal-overflow.d.ts.map +1 -1
- package/dist/patterns/horizontal-overflow/horizontal-overflow.js +25 -0
- package/dist/patterns/horizontal-overflow/horizontal-overflow.js.map +1 -1
- package/dist/patterns/overlay-surface/overlay-surface.d.ts.map +1 -1
- package/dist/patterns/overlay-surface/overlay-surface.js.map +1 -1
- package/dist/tokens/categorical-color.d.ts +6 -1
- package/dist/tokens/categorical-color.d.ts.map +1 -1
- package/dist/tokens/categorical-color.js.map +1 -1
- package/dist/tokens/uiSize/icon-size.d.ts +1 -1
- package/dist/tokens/uiSize/stack-gap.d.ts +18 -0
- package/dist/tokens/uiSize/stack-gap.d.ts.map +1 -0
- package/dist/tokens/uiSize/stack-gap.js +5 -0
- package/dist/tokens/uiSize/stack-gap.js.map +1 -0
- package/ds-canonical/fork/codex/agents/skills/performance-audit/SKILL.md +1 -1
- package/ds-canonical/fork/codex/hooks.json +1 -1
- package/ds-canonical/fork/consumer/lock.json +17 -14
- package/ds-canonical/fork/consumer/managed-files/423729840acdd43c812f17c245b871716c6b60f76e4d3e94864d0e34a815bf8d.blob +9 -0
- package/ds-canonical/fork/consumer/managed-files/46f10bc55cdfdf834d0749eebff42ea0bb8576f361b36d2b6ba1851354c50150.blob +47 -9
- package/ds-canonical/fork/consumer/managed-files/5a0d134a2645114c8caac20458070b00a7f0fd55b3e8805b7325cb4cf52fa15e.blob +110 -0
- package/ds-canonical/fork/consumer/managed-files/5af0a46760af338d51fd0f7950675f8e97f7d2f09386b8818fcd7a58e6654e75.blob +790 -0
- package/ds-canonical/fork/consumer/managed-files/68bfcddff9c4a16388c248cd34fb66163e9e64b3d809a48ef3984f6ae4ff6d12.blob +211 -0
- package/ds-canonical/fork/consumer/managed-files/7eb6cc60abfdabaf6617c336058fad1454ccde08889295767bfd9b7fd153cc6e.blob +45 -6
- package/ds-canonical/fork/consumer/managed-files/92e4828cdcb96ff0d01daa16f6885f24045a8e72820665650f2a1890dc3a446c.blob +42 -2
- package/ds-canonical/fork/consumer/managed-files/ab072e7f2eca51f0e18a5e18c4b333260ee5d219a12cc6df5bb2d683f0b2734d.blob +27 -1
- package/ds-canonical/fork/consumer/managed-files/e71c65f45c6af7ce12b6b3c0d8028f185da5b719e2050c9b2a6b243d41ae4878.blob +62 -14
- package/ds-canonical/fork/governance.lock +41 -17
- package/ds-canonical/fork/hooks/check_consumer_app_invariants.sh +16 -4
- package/ds-canonical/fork/hooks/check_consumer_code_quality.sh +3 -1
- package/ds-canonical/fork/hooks/check_pattern_invariants.sh +13 -9
- package/ds-canonical/fork/launchers/settings-hooks.json +1 -1
- package/ds-canonical/fork/manifest.json +17 -11
- package/ds-canonical/fork/providers/claude/skills/performance-audit/SKILL.md +1 -1
- package/ds-canonical/fork/providers/codex/hook-config.json +1 -1
- package/ds-canonical/fork/providers/codex/skills/performance-audit/SKILL.md +1 -1
- package/ds-canonical/fork/skills/performance-audit/SKILL.md +1 -1
- package/ds-canonical/hooks/check_consumer_app_invariants.sh +16 -4
- package/ds-canonical/hooks/check_consumer_code_quality.sh +3 -1
- package/ds-canonical/hooks/check_datatable_invariants.sh +4 -1
- package/ds-canonical/hooks/check_pattern_invariants.sh +13 -9
- package/ds-canonical/hooks/check_plugin_fork_health.sh +8 -1
- package/ds-canonical/hooks/check_propose_discipline.sh +8 -1
- package/ds-canonical/hooks/check_storybook_addon_packaging.sh +8 -1
- package/ds-canonical/hooks/check_substantive_edit_approval_preflight.sh +64 -2
- package/ds-canonical/hooks/lib/_overlay_handcraft.sh +5 -2
- package/ds-canonical/hooks/registrations.json +12 -7
- package/ds-canonical/hooks/session_start_governance_check.sh +16 -0
- package/ds-canonical/hooks/tests/test_check_consumer_app_invariants.sh +25 -0
- package/ds-canonical/hooks/tests/test_check_consumer_code_quality.sh +4 -0
- package/ds-canonical/hooks/tests/test_check_consumer_ds_primitive_misuse.sh +14 -0
- package/ds-canonical/hooks/tests/test_check_datatable_invariants.sh +24 -0
- package/ds-canonical/hooks/tests/test_check_overlay_handcraft.sh +32 -0
- package/ds-canonical/hooks/tests/test_check_pattern_invariants.sh +59 -0
- package/ds-canonical/hooks/tests/test_check_plugin_fork_health.sh +32 -1
- package/ds-canonical/hooks/tests/test_check_propose_discipline.sh +21 -0
- package/ds-canonical/hooks/tests/test_check_storybook_addon_packaging.sh +21 -0
- package/ds-canonical/hooks/tests/test_check_substantive_edit_approval_preflight.sh +97 -0
- package/ds-canonical/hooks/tests/test_session_start_governance_check.sh +49 -0
- package/ds-canonical/references/README.md +12 -6
- package/ds-canonical/references/drag-canonical.md +3 -1
- package/ds-canonical/references/focus-canonical.md +26 -13
- package/ds-canonical/references/governance-audit-coverage.md +1 -1
- package/ds-canonical/references/hit-area-canonical.md +361 -0
- package/ds-canonical/references/item-anatomy-recipe.md +4 -2
- package/ds-canonical/references/keyboard-model-canonical.md +559 -0
- package/ds-canonical/rules/meta-patterns.md +4 -4
- package/ds-canonical/rules/self-verify.md +4 -4
- package/ds-canonical/skills/design-system-audit/references/audit-prompts.md +1 -1
- package/ds-canonical/skills/design-system-audit/references/historical-bugs.md +96 -0
- package/ds-canonical/skills/knowledge-prune/SKILL.md +4 -1
- package/ds-canonical/skills/performance-audit/SKILL.md +1 -1
- package/ds-story-manifest.json +19 -7
- package/llms-full.txt +2 -2
- package/llms.txt +2 -2
- package/package.json +3 -1
- package/src/components/Accordion/accordion.anatomy.stories.tsx +3 -3
- package/src/components/Accordion/accordion.spec.md +1 -1
- package/src/components/Accordion/accordion.tsx +8 -3
- package/src/components/AccountMenu/account-menu.spec.md +2 -3
- package/src/components/AccountMenu/account-menu.tsx +0 -1
- package/src/components/AgentPanel/agent-decision-card.spec.md +88 -0
- package/src/components/AgentPanel/agent-panel-fab.spec.md +151 -0
- package/src/components/AgentPanel/agent-panel-fab.tsx +67 -24
- package/src/components/AgentPanel/agent-panel-logo.spec.md +87 -0
- package/src/components/AgentPanel/agent-panel-logo.tsx +3 -3
- package/src/components/AgentPanel/agent-panel.anatomy.stories.tsx +1 -1
- package/src/components/AgentPanel/agent-panel.principles.stories.tsx +1 -1
- package/src/components/AgentPanel/agent-panel.spec.md +30 -275
- package/src/components/AgentPanel/agent-panel.stories.tsx +113 -8
- package/src/components/AgentPanel/agent-panel.tsx +59 -22
- package/src/components/Alert/alert.anatomy.stories.tsx +1 -1
- package/src/components/AppShell/app-shell.tsx +1 -1
- package/src/components/AspectRatio/aspect-ratio.anatomy.stories.tsx +10 -15
- package/src/components/Avatar/avatar.anatomy.stories.tsx +23 -26
- package/src/components/Avatar/avatar.principles.stories.tsx +10 -10
- package/src/components/Avatar/avatar.spec.md +45 -6
- package/src/components/Avatar/avatar.stories.tsx +8 -8
- package/src/components/Avatar/avatar.tsx +227 -21
- package/src/components/Badge/badge.anatomy.stories.tsx +16 -21
- package/src/components/Breadcrumb/breadcrumb.anatomy.stories.tsx +3 -3
- package/src/components/Breadcrumb/breadcrumb.spec.md +3 -3
- package/src/components/Breadcrumb/breadcrumb.tsx +5 -3
- package/src/components/BulkActionBar/bulk-action-bar.anatomy.stories.tsx +1 -1
- package/src/components/BulkActionBar/bulk-action-bar.principles.stories.tsx +1 -1
- package/src/components/BulkActionBar/bulk-action-bar.spec.md +1 -0
- package/src/components/BulkActionBar/bulk-action-bar.stories.tsx +57 -2
- package/src/components/BulkActionBar/bulk-action-bar.tsx +32 -2
- package/src/components/Button/button.anatomy.stories.tsx +174 -33
- package/src/components/Button/button.spec.md +23 -8
- package/src/components/Button/button.tsx +116 -21
- package/src/components/Calendar/calendar.anatomy.stories.tsx +34 -11
- package/src/components/Calendar/calendar.principles.stories.tsx +44 -1
- package/src/components/Calendar/calendar.spec.md +97 -20
- package/src/components/Calendar/calendar.stories.tsx +101 -4
- package/src/components/Calendar/calendar.tsx +379 -74
- package/src/components/Carousel/carousel.anatomy.stories.tsx +16 -17
- package/src/components/Carousel/carousel.spec.md +48 -2
- package/src/components/Carousel/carousel.tsx +9 -3
- package/src/components/Chart/chart.anatomy.stories.tsx +7 -5
- package/src/components/Chart/chart.spec.md +32 -7
- package/src/components/Chart/chart.tsx +9 -0
- package/src/components/Checkbox/checkbox.anatomy.stories.tsx +21 -28
- package/src/components/Checkbox/checkbox.spec.md +4 -2
- package/src/components/Checkbox/checkbox.tsx +10 -2
- package/src/components/Chip/chip.anatomy.stories.tsx +1 -1
- package/src/components/Chip/chip.spec.md +2 -1
- package/src/components/Chip/chip.tsx +4 -2
- package/src/components/Combobox/combobox.anatomy.stories.tsx +30 -37
- package/src/components/Combobox/combobox.spec.md +11 -3
- package/src/components/Combobox/combobox.tsx +43 -12
- package/src/components/Command/command.spec.md +11 -0
- package/src/components/Command/command.tsx +143 -10
- package/src/components/DataTable/cell-registry.tsx +5 -1
- package/src/components/DataTable/data-table-column-visibility-panel.tsx +5 -6
- package/src/components/DataTable/data-table-interaction-layer.tsx +27 -14
- package/src/components/DataTable/data-table-known-defects.spec.md +72 -0
- package/src/components/DataTable/data-table-scroll-performance.spec.md +295 -0
- package/src/components/DataTable/data-table-sort-manager.tsx +5 -6
- package/src/components/DataTable/data-table.anatomy.stories.tsx +1 -1
- package/src/components/DataTable/data-table.css +64 -0
- package/src/components/DataTable/data-table.spec.md +70 -346
- package/src/components/DataTable/data-table.stories.tsx +56 -9
- package/src/components/DataTable/data-table.tsx +392 -103
- package/src/components/DateGrid/date-grid.anatomy.stories.tsx +2 -2
- package/src/components/DateGrid/date-grid.spec.md +163 -1
- package/src/components/DateGrid/date-grid.tsx +270 -7
- package/src/components/DatePicker/date-picker.anatomy.stories.tsx +18 -27
- package/src/components/DatePicker/date-picker.spec.md +6 -2
- package/src/components/DatePicker/date-picker.tsx +20 -1
- package/src/components/Dialog/dialog.spec.md +6 -2
- package/src/components/Dialog/dialog.stories.tsx +18 -9
- package/src/components/Dialog/dialog.tsx +83 -48
- package/src/components/DropdownMenu/dropdown-menu-keyboard.ts +255 -0
- package/src/components/DropdownMenu/dropdown-menu.anatomy.stories.tsx +32 -33
- package/src/components/DropdownMenu/dropdown-menu.spec.md +18 -5
- package/src/components/DropdownMenu/dropdown-menu.stories.tsx +58 -1
- package/src/components/DropdownMenu/dropdown-menu.tsx +70 -33
- package/src/components/Empty/empty.spec.md +1 -2
- package/src/components/Field/field-context.ts +9 -4
- package/src/components/Field/field-controls.spec.md +5 -2
- package/src/components/Field/field-edit-keys.ts +12 -0
- package/src/components/Field/field-wrapper.tsx +76 -10
- package/src/components/Field/field.anatomy.stories.tsx +1 -1
- package/src/components/Field/field.spec.md +11 -6
- package/src/components/Field/field.stories.tsx +24 -1
- package/src/components/Field/field.tsx +10 -4
- package/src/components/FileItem/file-item.anatomy.stories.tsx +20 -10
- package/src/components/FileItem/file-item.principles.stories.tsx +14 -7
- package/src/components/FileItem/file-item.spec.md +50 -48
- package/src/components/FileItem/file-item.stories.tsx +41 -28
- package/src/components/FileItem/file-item.tsx +184 -50
- package/src/components/FileUpload/file-upload.anatomy.stories.tsx +11 -15
- package/src/components/FileUpload/file-upload.spec.md +27 -1
- package/src/components/FileUpload/file-upload.stories.tsx +33 -0
- package/src/components/FileUpload/file-upload.tsx +173 -43
- package/src/components/FileViewer/file-viewer.tsx +9 -4
- package/src/components/HoverCard/hover-card.stories.tsx +2 -2
- package/src/components/InlineEdit/inline-edit.spec.md +2 -0
- package/src/components/InlineEdit/inline-edit.tsx +8 -3
- package/src/components/Input/input.anatomy.stories.tsx +23 -31
- package/src/components/Input/input.spec.md +11 -0
- package/src/components/Input/input.tsx +6 -1
- package/src/components/LinkInput/link-input.anatomy.stories.tsx +18 -21
- package/src/components/LinkInput/link-input.principles.stories.tsx +4 -4
- package/src/components/LinkInput/link-input.spec.md +12 -1
- package/src/components/LinkInput/link-input.tsx +37 -4
- package/src/components/Menu/menu-item.anatomy.stories.tsx +50 -56
- package/src/components/Menu/menu-item.spec.md +1 -1
- package/src/components/Menu/menu-item.stories.tsx +48 -42
- package/src/components/Notice/notice.anatomy.stories.tsx +8 -16
- package/src/components/Notice/notice.spec.md +1 -1
- package/src/components/Notice/notice.tsx +1 -1
- package/src/components/NumberInput/number-input.anatomy.stories.tsx +21 -28
- package/src/components/NumberInput/number-input.spec.md +2 -0
- package/src/components/NumberInput/number-input.tsx +8 -1
- package/src/components/OverflowIndicator/overflow-indicator.anatomy.stories.tsx +28 -48
- package/src/components/OverflowIndicator/overflow-indicator.spec.md +13 -1
- package/src/components/OverflowIndicator/overflow-indicator.stories.tsx +12 -9
- package/src/components/OverflowIndicator/overflow-indicator.tsx +32 -12
- package/src/components/Pagination/pagination.spec.md +1 -0
- package/src/components/Pagination/pagination.tsx +1 -1
- package/src/components/PeoplePicker/avatar-stack-overflow.ts +14 -13
- package/src/components/PeoplePicker/people-picker-helpers.ts +5 -2
- package/src/components/PeoplePicker/people-picker.anatomy.stories.tsx +1 -1
- package/src/components/PeoplePicker/people-picker.spec.md +4 -2
- package/src/components/PeoplePicker/people-picker.tsx +1 -4
- package/src/components/PeoplePicker/person-display.tsx +45 -28
- package/src/components/Popover/popover.anatomy.stories.tsx +1 -1
- package/src/components/Popover/popover.principles.stories.tsx +3 -12
- package/src/components/Popover/popover.spec.md +7 -3
- package/src/components/ProfileCard/profile-card.anatomy.stories.tsx +1 -1
- package/src/components/ProfileCard/profile-card.spec.md +1 -1
- package/src/components/ProfileCard/profile-card.tsx +0 -3
- package/src/components/ProgressBar/progress-bar.anatomy.stories.tsx +11 -21
- package/src/components/ProgressBar/progress-bar.spec.md +1 -1
- package/src/components/RadioGroup/radio-group.anatomy.stories.tsx +17 -24
- package/src/components/RadioGroup/radio-group.spec.md +1 -1
- package/src/components/RadioGroup/radio-group.tsx +22 -4
- package/src/components/Rating/rating.anatomy.stories.tsx +131 -124
- package/src/components/Rating/rating.principles.stories.tsx +69 -43
- package/src/components/Rating/rating.spec.md +142 -63
- package/src/components/Rating/rating.stories.tsx +11 -15
- package/src/components/Rating/rating.tsx +131 -117
- package/src/components/ScrollArea/scroll-area.anatomy.stories.tsx +1 -1
- package/src/components/ScrollArea/scroll-area.principles.stories.tsx +6 -3
- package/src/components/ScrollArea/scroll-area.spec.md +3 -1
- package/src/components/ScrollArea/scroll-area.stories.tsx +5 -2
- package/src/components/ScrollArea/scroll-area.tsx +1 -1
- package/src/components/SegmentedControl/segmented-control.anatomy.stories.tsx +1 -1
- package/src/components/SegmentedControl/segmented-control.spec.md +2 -2
- package/src/components/SegmentedControl/segmented-control.tsx +3 -2
- package/src/components/Select/select.anatomy.stories.tsx +26 -35
- package/src/components/Select/select.spec.md +4 -3
- package/src/components/Select/select.tsx +11 -4
- package/src/components/SelectMenu/select-menu-keyboard.ts +218 -0
- package/src/components/SelectMenu/select-menu.anatomy.stories.tsx +6 -17
- package/src/components/SelectMenu/select-menu.spec.md +12 -4
- package/src/components/SelectMenu/select-menu.tsx +21 -56
- package/src/components/SelectionControl/selection-item.anatomy.stories.tsx +9 -33
- package/src/components/SelectionControl/selection-item.spec.md +2 -2
- package/src/components/SelectionControl/selection-item.tsx +3 -1
- package/src/components/Separator/separator.stories.tsx +4 -4
- package/src/components/Sheet/sheet.principles.stories.tsx +31 -13
- package/src/components/Sidebar/sidebar.anatomy.stories.tsx +1 -1
- package/src/components/Sidebar/sidebar.spec.md +73 -6
- package/src/components/Sidebar/sidebar.stories.tsx +82 -4
- package/src/components/Sidebar/sidebar.tsx +281 -64
- package/src/components/Skeleton/skeleton.spec.md +1 -1
- package/src/components/Slider/slider.anatomy.stories.tsx +1 -1
- package/src/components/Slider/slider.spec.md +6 -5
- package/src/components/Slider/slider.tsx +13 -8
- package/src/components/Steps/steps-state-visuals.spec.md +157 -0
- package/src/components/Steps/steps.anatomy.stories.tsx +1 -0
- package/src/components/Steps/steps.spec.md +59 -142
- package/src/components/Steps/steps.stories.tsx +1 -1
- package/src/components/Steps/steps.tsx +125 -33
- package/src/components/Switch/switch.spec.md +3 -3
- package/src/components/Switch/switch.tsx +6 -1
- package/src/components/Tabs/tabs.spec.md +5 -1
- package/src/components/Tabs/tabs.tsx +42 -3
- package/src/components/Tag/tag.anatomy.stories.tsx +24 -28
- package/src/components/Tag/tag.spec.md +10 -11
- package/src/components/Tag/tag.tsx +3 -3
- package/src/components/Textarea/textarea.tsx +7 -1
- package/src/components/TimePicker/time-columns.tsx +9 -4
- package/src/components/TimePicker/time-picker.spec.md +4 -1
- package/src/components/TimePicker/time-picker.tsx +1 -1
- package/src/components/Tooltip/tooltip.anatomy.stories.tsx +6 -15
- package/src/components/Tooltip/tooltip.principles.stories.tsx +4 -16
- package/src/components/Tooltip/tooltip.spec.md +2 -1
- package/src/components/TreeView/tree-view.anatomy.stories.tsx +35 -13
- package/src/components/TreeView/tree-view.spec.md +67 -38
- package/src/components/TreeView/tree-view.stories.tsx +221 -12
- package/src/components/TreeView/tree-view.tsx +463 -356
- package/src/hooks/use-input-modality.ts +17 -46
- package/src/hooks/use-overflow-items.ts +4 -0
- package/src/hooks/use-truncated.ts +3 -2
- package/src/index.ts +17 -0
- package/src/lib/README.md +2 -0
- package/src/lib/drag-announcements.ts +78 -0
- package/src/lib/focus-after-trigger.ts +102 -0
- package/src/lib/roving-list-keyboard.ts +400 -0
- package/src/lib/utils.ts +5 -0
- package/src/patterns/action-bar/action-bar.spec.md +2 -0
- package/src/patterns/element-anatomy/inline-action.spec.md +16 -12
- package/src/patterns/element-anatomy/item-anatomy.spec.md +98 -6
- package/src/patterns/element-anatomy/item-anatomy.stories.tsx +75 -70
- package/src/patterns/element-anatomy/item-anatomy.tsx +17 -6
- package/src/patterns/element-anatomy/truncated-text.spec.md +1 -0
- package/src/patterns/header-canonical/header-canonical.spec.md +3 -2
- package/src/patterns/horizontal-overflow/horizontal-overflow.spec.md +2 -0
- package/src/patterns/horizontal-overflow/horizontal-overflow.stories.tsx +36 -0
- package/src/patterns/horizontal-overflow/horizontal-overflow.tsx +28 -0
- package/src/patterns/overlay-surface/overlay-chrome-sizing.spec.md +180 -0
- package/src/patterns/overlay-surface/overlay-surface.spec.md +8 -162
- package/src/patterns/overlay-surface/overlay-surface.tsx +5 -3
- package/src/patterns/resize-handle/resize-handle.spec.md +3 -3
- package/src/styles/base.css +1 -1
- package/src/tokens/categorical-color.ts +6 -1
- package/src/tokens/color/color.spec.md +91 -24
- package/src/tokens/color/color.stories.tsx +50 -2
- package/src/tokens/color/primitives.css +41 -38
- package/src/tokens/color/semantic.css +105 -36
- package/src/tokens/layoutSpace/layoutSpace.spec.md +2 -0
- package/src/tokens/motion/motion.spec.md +31 -8
- package/src/tokens/orphan-tokens.spec.md +1 -1
- package/src/tokens/uiSize/icon-size.ts +1 -1
- package/src/tokens/uiSize/stack-gap.ts +17 -0
- package/src/tokens/uiSize/uiSize.css +12 -0
- package/src/tokens/uiSize/uiSize.spec.md +17 -16
- package/src/tokens/uiSize/uiSize.stories.tsx +1 -1
- package/dist/node_modules/aria-hidden/dist/es2015/index.js +0 -147
- package/dist/node_modules/aria-hidden/dist/es2015/index.js.map +0 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"app-shell.js","sources":["../../../src/components/AppShell/app-shell.tsx"],"sourcesContent":["// @benchmark-cited: 2026-05-19 — Mantine AppShell / Ant Layout / Material 3 Drawer / Atlassian Navigation System cite in app-shell.spec.md frontmatter.\n/**\n * AppShell — web service page-level layout primitive。\n *\n * 組合 Sidebar + ChromeHeader + Aside + main 成完整 page shell。SSOT 邊界:本 composite component only\n * own slot composition + layout mode + Aside responsive mode;不 own sidebar / header /\n * sheet 視覺(各自 spec own)。\n *\n * 對齊 Mantine AppShell compound API + Ant Layout slot 模式 + Material 3 standard/modal\n * drawer canonical(per spec.md frontmatter cite)。\n *\n * Spec SSOT:`components/AppShell/app-shell.spec.md`\n */\n\nimport * as React from 'react'\nimport { X as XIcon } from 'lucide-react'\nimport {\n Sheet,\n SheetContent,\n SheetTitle,\n handleSheetOpenAutoFocus,\n} from '@/design-system/components/Sheet/sheet'\nimport { Button } from '@/design-system/components/Button/button'\nimport { ScrollArea } from '@/design-system/components/ScrollArea/scroll-area'\nimport { ChromeHeader } from '@/design-system/patterns/header-canonical/chrome-header'\nimport { useIsNarrowViewport } from '@/design-system/hooks/use-is-narrow-viewport'\nimport { cn } from '@/lib/utils'\n\n// ── Types ────────────────────────────────────────────────────────────────────\n\ntype AppShellLayout = 'primary-sidebar' | 'primary-header'\n\nexport interface AppShellProps extends React.HTMLAttributes<HTMLDivElement> {\n /** primary-sidebar (Linear/Notion 派) | primary-header (GitHub/Slack 派);預設 primary-sidebar */\n layout?: AppShellLayout\n /** Sidebar 元素(應傳 Sidebar primitive,per Consumer 紀律;型別 optional 不機械強制,對齊 spec「應」措辭)*/\n sidebar?: React.ReactNode\n /**\n * Local page header(永遠 render 在 main column 頂部,當前頁 actions / breadcrumb / page-level filter)。\n * 兩 layout mode 都會 render 此 slot — `primary-header` mode 多了 globalHeader 在上方,\n * **不取代** local header(per 2026-05-20 user clarification「primary-header = primary-sidebar + 一條 global header」)。\n */\n header?: React.ReactNode\n /**\n * Global header(僅 `primary-header` mode render)。橫跨整 viewport 的頂部 bar:\n * account avatar / workspace switcher / global search / notifications。\n * 對齊 GitHub top nav / Slack workspace bar / Gmail logo bar 慣例。\n * `primary-sidebar` mode 傳此 prop 會被忽略。\n */\n globalHeader?: React.ReactNode\n /** Aside 元素(`<AppShellAside>` sub-component);可選 */\n aside?: React.ReactNode\n /** Aside open state(modal mode 必須)*/\n asideOpen?: boolean\n onAsideOpenChange?: (open: boolean) => void\n /** Main content;`<main>` landmark + padding=0 */\n children: React.ReactNode\n}\n\nexport interface AppShellAsideProps {\n /** Required:modal mode 走 Sheet → aria-labelledby 強制,per sheet.spec.md:98 */\n title: string\n /** Width(number 或 breakpoint-keyed object);clamp min:240 max:640 */\n width?: number | { md?: number; xl?: number }\n /** Children content */\n children: React.ReactNode\n className?: string\n}\n\n// ── Context ──────────────────────────────────────────────────────────────────\n\ninterface AppShellContextValue {\n layout: AppShellLayout\n asideOpen: boolean\n setAsideOpen: (open: boolean) => void\n isMobile: boolean\n}\n\nconst AppShellContext = React.createContext<AppShellContextValue | null>(null)\n\nfunction useAppShell(): AppShellContextValue {\n const ctx = React.useContext(AppShellContext)\n if (!ctx) throw new Error('AppShellAside must be used within <AppShell>')\n return ctx\n}\n\n// Mobile breakpoint:**消費既有 `useIsNarrowViewport`**(`hooks/use-is-narrow-viewport.ts` SSOT,\n// 768px,跟 Sidebar SSOT 同源)— 不發明 local hook,per codex Layer B D2/D4 verdict 避 drift。\n\n// xl breakpoint(對齊 Tailwind v4 xl = 1280px,DS-wide consensus)\nconst XL_BREAKPOINT_PX = 1280\n\nfunction useIsXl(): boolean {\n const [isXl, setIsXl] = React.useState<boolean>(() => {\n if (typeof window === 'undefined') return false\n return window.matchMedia(`(min-width: ${XL_BREAKPOINT_PX}px)`).matches\n })\n\n React.useEffect(() => {\n const mq = window.matchMedia(`(min-width: ${XL_BREAKPOINT_PX}px)`)\n const handler = (e: MediaQueryListEvent) => setIsXl(e.matches)\n mq.addEventListener('change', handler)\n return () => mq.removeEventListener('change', handler)\n }, [])\n\n return isXl\n}\n\n// ── Width resolve(consumer 自傳 + clamp 240-640)──────────────────────────────\n\nconst ASIDE_WIDTH_MIN = 240\nconst ASIDE_WIDTH_MAX = 640\nconst ASIDE_WIDTH_DEFAULT = 320\n\nfunction resolveAsideWidth(width: AppShellAsideProps['width'], isXl: boolean): number {\n if (typeof width === 'number') {\n return Math.max(ASIDE_WIDTH_MIN, Math.min(ASIDE_WIDTH_MAX, width))\n }\n if (width && typeof width === 'object') {\n // breakpoint-keyed:xl viewport(≥1280px)用 xl,否則 md\n const v = (isXl ? width.xl ?? width.md : width.md) ?? ASIDE_WIDTH_DEFAULT\n return Math.max(ASIDE_WIDTH_MIN, Math.min(ASIDE_WIDTH_MAX, v))\n }\n return ASIDE_WIDTH_DEFAULT\n}\n\n// ── Skip-to-main link(a11y WCAG 2.4.1)───────────────────────────────────────\n\nfunction SkipToMain() {\n return (\n <nav aria-label=\"Skip links\">\n <a\n href=\"#app-shell-main\"\n className={cn(\n 'sr-only focus:not-sr-only',\n 'focus:fixed focus:top-2 focus:left-2 focus:z-50',\n 'focus:px-3 focus:py-2 focus:rounded-md',\n 'focus:bg-surface focus:text-foreground focus:shadow-[var(--elevation-200)]',\n // 焦點框走全域外描邊(base.css)—— skip link 被鍵盤聚焦時 :focus-visible 成立。\n // 原本的 ring-primary 與 --ring 同值(semantic.css:337 `--ring: var(--primary)`),遷移零視覺差。\n )}\n >\n Skip to main content\n </a>\n </nav>\n )\n}\n\n// ── AppShell root ────────────────────────────────────────────────────────────\n\nconst AppShell = React.forwardRef<HTMLDivElement, AppShellProps>(\n (\n {\n layout = 'primary-sidebar',\n sidebar,\n header,\n globalHeader,\n aside,\n asideOpen: asideOpenProp,\n onAsideOpenChange,\n children,\n className,\n ...props\n },\n ref\n ) => {\n const [asideOpenInternal, setAsideOpenInternal] = React.useState(false)\n const isControlled = asideOpenProp !== undefined\n const asideOpen = isControlled ? asideOpenProp : asideOpenInternal\n\n const setAsideOpen = React.useCallback(\n (open: boolean) => {\n if (!isControlled) setAsideOpenInternal(open)\n onAsideOpenChange?.(open)\n },\n [isControlled, onAsideOpenChange]\n )\n\n const isMobile = useIsNarrowViewport()\n\n // ── Keyboard: cmd+. toggle aside ──\n // ⌘B sidebar toggle by Sidebar SSOT(本 component 不重覆 register)\n React.useEffect(() => {\n const onKey = (e: KeyboardEvent) => {\n if (e.key === '.' && (e.metaKey || e.ctrlKey)) {\n e.preventDefault()\n setAsideOpen(!asideOpen)\n }\n }\n window.addEventListener('keydown', onKey)\n return () => window.removeEventListener('keydown', onKey)\n }, [asideOpen, setAsideOpen])\n\n const ctxValue = React.useMemo<AppShellContextValue>(\n () => ({ layout, asideOpen, setAsideOpen, isMobile }),\n [layout, asideOpen, setAsideOpen, isMobile]\n )\n\n // ── Layout grid(2 mode)──\n // primary-sidebar:\n // row1: [sidebar (頂天)][main col (header + main)][aside (頂天)]\n // primary-header:\n // row1: [header (橫跨整 viewport, banner role)]\n // row2: [sidebar][main][aside]\n\n // AppShellAside 自決 inline vs modal mode(via AppShellContext.isMobile)。\n // AppShell 一律只 render `{aside}` 一次,AppShellAside 內部根據 isMobile 決定 render 形式。\n\n if (layout === 'primary-header') {\n // primary-header layout(2026-05-21 v2 — user clarification「primary-header = primary-sidebar + 一條 global header」):\n // 結構:row1 globalHeader(全寬 global bar)/ row2 [sidebar][main col: localHeader + main][aside]\n // - globalHeader = 跨頁 account / workspace switcher / notifications(對齊 GitHub top nav / Slack workspace bar)\n // - header = local page header,**仍存在**(per page actions / breadcrumb,對齊 GitHub repo header / Slack channel header / Gmail email-list toolbar 2-layer 慣例)\n // Consumer 必傳 `<Sidebar viewportInsetTop=\"var(--chrome-header-height)\">` 讓 sidebar 從 globalHeader 下方起算。\n return (\n <AppShellContext.Provider value={ctxValue}>\n <div\n ref={ref}\n className={cn('flex h-svh w-full flex-col overflow-hidden bg-canvas', className)}\n {...props}\n >\n <SkipToMain />\n {/* Row 1:Global header(account / workspace switcher / notifications,橫跨整 viewport)*/}\n {globalHeader && <div className=\"flex-shrink-0\">{globalHeader}</div>}\n {/* Row 2:[sidebar][main col][aside]horizontal row */}\n <div className=\"flex flex-1 min-h-0 w-full\">\n {sidebar}\n {/* Main column:local header + main content(per user model「primary-header = primary-sidebar + global header」)*/}\n <div className=\"flex flex-col flex-1 min-w-0 min-h-0 overflow-hidden\">\n {header && <div className=\"flex-shrink-0\">{header}</div>}\n <main\n id=\"app-shell-main\"\n tabIndex={-1}\n className=\"flex-1 min-w-0 min-h-0 overflow-y-auto focus-visible:focus-ring-inset\"\n >\n {children}\n </main>\n </div>\n {aside}\n </div>\n </div>\n </AppShellContext.Provider>\n )\n }\n\n // primary-sidebar layout\n return (\n <AppShellContext.Provider value={ctxValue}>\n <div\n ref={ref}\n className={cn('flex h-svh w-full overflow-hidden bg-canvas', className)}\n {...props}\n >\n <SkipToMain />\n {/* Sidebar — 頂天 */}\n {sidebar}\n {/* Main column(header + main 垂直堆)*/}\n <div className=\"flex flex-col flex-1 min-w-0 min-h-0 overflow-hidden\">\n {header && (\n // Header 在 main column 內(main col sibling,非 main descendant)。ChromeHeader 自\n // 2026-05-20 起 render `<header>`(chrome-header.tsx);依 W3C HTML-AAM,`<div>` wrapper\n // 不 scope out banner(只有 main/article/aside/nav/section 能)→ 仍得 implicit banner ✓\n // (對齊 app-shell.spec.md「W3C ARIA in HTML banner rule」段)。\n <div className=\"flex-shrink-0\">{header}</div>\n )}\n <main\n id=\"app-shell-main\"\n tabIndex={-1}\n className=\"flex-1 min-h-0 overflow-y-auto focus-visible:focus-ring-inset\"\n >\n {children}\n </main>\n </div>\n {/* Aside slot — desktop inline OR mobile Sheet,內部自決 */}\n {aside}\n </div>\n </AppShellContext.Provider>\n )\n }\n)\nAppShell.displayName = 'AppShell'\n\n// ── AppShellAside sub-component ──────────────────────────────────────────────\n\n/**\n * AppShellAside — right panel:standard inline(desktop) vs modal overlay(mobile)。\n *\n * Desktop(viewport ≥ 768px):\n * - Render 直接放 layout grid 右側(AppShell 恆 render {aside};AppShellAside 於 !asideOpen 自行 return null — 內部自決,非 via parent)\n * - 不蓋 mask / background 可操作 / 佔 layout 寬\n * - Vertical extent:primary-sidebar → 頂天立地 / primary-header → header 下方\n *\n * Mobile(viewport < 768px):\n * - Render 走 Sheet primitive(side=\"right\",per sheet.spec.md)\n * - Mask 蓋 / background 不可操作 / 不佔 layout 寬\n * - title 強制(aria-labelledby per sheet.spec.md:98)\n */\nconst AppShellAside = React.forwardRef<HTMLElement, AppShellAsideProps>(\n ({ title, width, children, className }, ref) => {\n const { asideOpen, setAsideOpen, isMobile } = useAppShell()\n const isXl = useIsXl()\n const resolvedWidth = resolveAsideWidth(width, isXl)\n\n // Shared frame:always-on header(title + close X)+ body(ScrollArea + layoutSpace 規則 1B 父層 padding)\n // 對齊 codex Layer B 2026-05-20「container mode 可變,panel role/content 不該變」+ Notion/Figma right\n // panel 共識(modal vs inline 結構相同,host wrapper 不同)。\n // 2026-05-20 migrate 消費 ChromeHeader primitive(撤回自刻 + 撤回 bg-surface 疊加 — 對齊\n // `header-canonical.spec.md`「6. Background ownership」段「Nested chrome header 透明繼承\n // parent」:aside container 自身已 bg-surface,內 header 不該再畫 bg 避免疊加 drift)。\n // titleNode 依 mode 不同:modal(Sheet)用 SheetTitle 綁 Radix Dialog titleId — 否則\n // SheetContent 的 aria-labelledby 指向不存在的 id → SR 念「unnamed dialog」+ Radix\n // TitleWarning console.error(違 sheet.spec.md:98「Sheet content 必有可見 title」)。\n // inline(aside)用純 <h2>:aside 自身已 aria-label={title},且無 Dialog context 不可用 SheetTitle。\n // R1:mobile Sheet 關閉後焦點還原目標(open 時 snapshot)\n const asideOpenerRef = React.useRef<HTMLElement | null>(null)\n\n const renderFrame = (titleNode: React.ReactNode) => (\n <>\n <ChromeHeader>\n {titleNode}\n <Button\n iconOnly\n dismiss\n size=\"sm\"\n startIcon={XIcon}\n aria-label=\"關閉\"\n onClick={() => setAsideOpen(false)}\n />\n </ChromeHeader>\n <ScrollArea className=\"flex-1 min-h-0\">\n {children}\n </ScrollArea>\n </>\n )\n\n // Modal mode(mobile)— Sheet from right\n // 2026-07-14 dim-9 修:同步轉交 forwarded ref + 合併 consumer className —— 原 mobile 分支\n // 兩者皆遺失,同一 API(ref / className)隨 breakpoint 靜默失效(desktop 有效、mobile 無效)。\n if (isMobile) {\n return (\n <Sheet open={asideOpen} onOpenChange={setAsideOpen}>\n <SheetContent\n ref={ref as React.Ref<HTMLDivElement>}\n side=\"right\"\n // R1 WCAG 2.4.3(2026-07-14,Claude+codex A′ 共識):controlled Sheet 無 SheetTrigger →\n // Radix onCloseAutoFocus triggerRef=null + preventDefault 吃掉 fallback → 焦點掉 <body>。\n // 解:開啟時 snapshot opener(事件 dispatch 時 Radix 尚未移焦;MUI/React Aria/Ant snapshot\n // 共識 + DatePicker Range 先例 date-picker.tsx:1061),關閉手動還原;opener 已 unmount →\n // fallback skip-link main(#app-shell-main tabIndex=-1)。\n onOpenAutoFocus={(e) => {\n asideOpenerRef.current = document.activeElement instanceof HTMLElement ? document.activeElement : null\n handleSheetOpenAutoFocus(e) // 接力 DS 預設 body-focus(自訂 handler 覆寫預設,故顯式呼叫)\n }}\n onCloseAutoFocus={(e) => {\n e.preventDefault()\n const opener = asideOpenerRef.current\n asideOpenerRef.current = null\n if (opener?.isConnected) { opener.focus({ preventScroll: true }); return }\n document.getElementById('app-shell-main')?.focus({ preventScroll: true })\n }}\n className={cn(\n 'w-[min(90vw,var(--app-shell-aside-modal-width))] flex flex-col p-0 [&>button]:hidden',\n className\n )}\n style={{ ['--app-shell-aside-modal-width' as string]: `${resolvedWidth}px` }}\n >\n {renderFrame(<SheetTitle className=\"flex-1\">{title}</SheetTitle>)}\n </SheetContent>\n </Sheet>\n )\n }\n\n // Standard inline mode(desktop)\n if (!asideOpen) return null\n\n return (\n <aside\n ref={ref}\n aria-label={title}\n className={cn(\n 'flex flex-col h-full min-h-0 overflow-hidden',\n 'bg-surface border-l border-divider',\n className\n )}\n style={{ width: resolvedWidth }}\n >\n {renderFrame(<h2 className=\"text-body-lg font-medium flex-1 truncate\">{title}</h2>)}\n </aside>\n )\n }\n)\nAppShellAside.displayName = 'AppShellAside'\n\n// ── Exports ──────────────────────────────────────────────────────────────────\n\n// Story auto-compile metadata — canonical migration(2026-08-01)\n// AppShell is a page-level composition primitive. `layout` is a structural mode,\n// not a visual variant axis; slot controls own their interaction states and sizes.\nexport const appShellMeta = {\n component: 'AppShell',\n family: 'composite',\n variants: {},\n sizes: {},\n states: [],\n tokens: {\n bg: ['bg-canvas', 'bg-surface'],\n fg: ['text-foreground'],\n border: ['border-divider'],\n ring: ['ring-primary'],\n },\n defaultVariant: undefined,\n defaultSize: undefined,\n} as const\n\n// code-quality-allow: dead-export useAppShell — public compound API hook(consumer 可自拼 custom aside layout,\n// 對齊 Radix `useDialogContext` / MUI `useFormControl` 慣例)。內部 AppShellAside 已消費(L294),\n// audit script 抓「無 cross-file import」是 false positive(2026-05-21 D2 codify)。\nexport { AppShell, AppShellAside, useAppShell }\n"],"names":["XIcon"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"app-shell.js","sources":["../../../src/components/AppShell/app-shell.tsx"],"sourcesContent":["// @benchmark-cited: 2026-05-19 — Mantine AppShell / Ant Layout / Material 3 Drawer / Atlassian Navigation System cite in app-shell.spec.md frontmatter.\n/**\n * AppShell — web service page-level layout primitive。\n *\n * 組合 Sidebar + ChromeHeader + Aside + main 成完整 page shell。SSOT 邊界:本 composite component only\n * own slot composition + layout mode + Aside responsive mode;不 own sidebar / header /\n * sheet 視覺(各自 spec own)。\n *\n * 對齊 Mantine AppShell compound API + Ant Layout slot 模式 + Material 3 standard/modal\n * drawer canonical(per spec.md frontmatter cite)。\n *\n * Spec SSOT:`components/AppShell/app-shell.spec.md`\n */\n\nimport * as React from 'react'\nimport { X as XIcon } from 'lucide-react'\nimport {\n Sheet,\n SheetContent,\n SheetTitle,\n handleSheetOpenAutoFocus,\n} from '@/design-system/components/Sheet/sheet'\nimport { Button } from '@/design-system/components/Button/button'\nimport { ScrollArea } from '@/design-system/components/ScrollArea/scroll-area'\nimport { ChromeHeader } from '@/design-system/patterns/header-canonical/chrome-header'\nimport { useIsNarrowViewport } from '@/design-system/hooks/use-is-narrow-viewport'\nimport { cn } from '@/lib/utils'\n\n// ── Types ────────────────────────────────────────────────────────────────────\n\ntype AppShellLayout = 'primary-sidebar' | 'primary-header'\n\nexport interface AppShellProps extends React.HTMLAttributes<HTMLDivElement> {\n /** primary-sidebar (Linear/Notion 派) | primary-header (GitHub/Slack 派);預設 primary-sidebar */\n layout?: AppShellLayout\n /** Sidebar 元素(應傳 Sidebar primitive,per Consumer 紀律;型別 optional 不機械強制,對齊 spec「應」措辭)*/\n sidebar?: React.ReactNode\n /**\n * Local page header(永遠 render 在 main column 頂部,當前頁 actions / breadcrumb / page-level filter)。\n * 兩 layout mode 都會 render 此 slot — `primary-header` mode 多了 globalHeader 在上方,\n * **不取代** local header(per 2026-05-20 user clarification「primary-header = primary-sidebar + 一條 global header」)。\n */\n header?: React.ReactNode\n /**\n * Global header(僅 `primary-header` mode render)。橫跨整 viewport 的頂部 bar:\n * account avatar / workspace switcher / global search / notifications。\n * 對齊 GitHub top nav / Slack workspace bar / Gmail logo bar 慣例。\n * `primary-sidebar` mode 傳此 prop 會被忽略。\n */\n globalHeader?: React.ReactNode\n /** Aside 元素(`<AppShellAside>` sub-component);可選 */\n aside?: React.ReactNode\n /** Aside open state(modal mode 必須)*/\n asideOpen?: boolean\n onAsideOpenChange?: (open: boolean) => void\n /** Main content;`<main>` landmark + padding=0 */\n children: React.ReactNode\n}\n\nexport interface AppShellAsideProps {\n /** Required:modal mode 走 Sheet → aria-labelledby 強制,per sheet.spec.md:98 */\n title: string\n /** Width(number 或 breakpoint-keyed object);clamp min:240 max:640 */\n width?: number | { md?: number; xl?: number }\n /** Children content */\n children: React.ReactNode\n className?: string\n}\n\n// ── Context ──────────────────────────────────────────────────────────────────\n\ninterface AppShellContextValue {\n layout: AppShellLayout\n asideOpen: boolean\n setAsideOpen: (open: boolean) => void\n isMobile: boolean\n}\n\nconst AppShellContext = React.createContext<AppShellContextValue | null>(null)\n\nfunction useAppShell(): AppShellContextValue {\n const ctx = React.useContext(AppShellContext)\n if (!ctx) throw new Error('AppShellAside must be used within <AppShell>')\n return ctx\n}\n\n// Mobile breakpoint:**消費既有 `useIsNarrowViewport`**(`hooks/use-is-narrow-viewport.ts` SSOT,\n// 768px,跟 Sidebar SSOT 同源)— 不發明 local hook,per codex Layer B D2/D4 verdict 避 drift。\n\n// xl breakpoint(對齊 Tailwind v4 xl = 1280px,DS-wide consensus)\nconst XL_BREAKPOINT_PX = 1280\n\nfunction useIsXl(): boolean {\n const [isXl, setIsXl] = React.useState<boolean>(() => {\n if (typeof window === 'undefined') return false\n return window.matchMedia(`(min-width: ${XL_BREAKPOINT_PX}px)`).matches\n })\n\n React.useEffect(() => {\n const mq = window.matchMedia(`(min-width: ${XL_BREAKPOINT_PX}px)`)\n const handler = (e: MediaQueryListEvent) => setIsXl(e.matches)\n mq.addEventListener('change', handler)\n return () => mq.removeEventListener('change', handler)\n }, [])\n\n return isXl\n}\n\n// ── Width resolve(consumer 自傳 + clamp 240-640)──────────────────────────────\n\nconst ASIDE_WIDTH_MIN = 240\nconst ASIDE_WIDTH_MAX = 640\nconst ASIDE_WIDTH_DEFAULT = 320\n\nfunction resolveAsideWidth(width: AppShellAsideProps['width'], isXl: boolean): number {\n if (typeof width === 'number') {\n return Math.max(ASIDE_WIDTH_MIN, Math.min(ASIDE_WIDTH_MAX, width))\n }\n if (width && typeof width === 'object') {\n // breakpoint-keyed:xl viewport(≥1280px)用 xl,否則 md\n const v = (isXl ? width.xl ?? width.md : width.md) ?? ASIDE_WIDTH_DEFAULT\n return Math.max(ASIDE_WIDTH_MIN, Math.min(ASIDE_WIDTH_MAX, v))\n }\n return ASIDE_WIDTH_DEFAULT\n}\n\n// ── Skip-to-main link(a11y WCAG 2.4.1)───────────────────────────────────────\n\nfunction SkipToMain() {\n return (\n <nav aria-label=\"Skip links\">\n <a\n href=\"#app-shell-main\"\n className={cn(\n 'sr-only focus:not-sr-only',\n 'focus:fixed focus:top-2 focus:left-2 focus:z-50',\n 'focus:px-3 focus:py-2 focus:rounded-md',\n 'focus:bg-surface focus:text-foreground focus:shadow-[var(--elevation-200)]',\n // 焦點框走全域外描邊(base.css)—— skip link 被鍵盤聚焦時 :focus-visible 成立。\n // 原本的 ring-primary 與 --ring 同值(semantic.css:337 `--ring: var(--primary)`),遷移零視覺差。\n )}\n >\n Skip to main content\n </a>\n </nav>\n )\n}\n\n// ── AppShell root ────────────────────────────────────────────────────────────\n\nconst AppShell = React.forwardRef<HTMLDivElement, AppShellProps>(\n (\n {\n layout = 'primary-sidebar',\n sidebar,\n header,\n globalHeader,\n aside,\n asideOpen: asideOpenProp,\n onAsideOpenChange,\n children,\n className,\n ...props\n },\n ref\n ) => {\n const [asideOpenInternal, setAsideOpenInternal] = React.useState(false)\n const isControlled = asideOpenProp !== undefined\n const asideOpen = isControlled ? asideOpenProp : asideOpenInternal\n\n const setAsideOpen = React.useCallback(\n (open: boolean) => {\n if (!isControlled) setAsideOpenInternal(open)\n onAsideOpenChange?.(open)\n },\n [isControlled, onAsideOpenChange]\n )\n\n const isMobile = useIsNarrowViewport()\n\n // ── Keyboard: cmd+. toggle aside ──\n // ⌘B sidebar toggle by Sidebar SSOT(本 component 不重覆 register)\n React.useEffect(() => {\n const onKey = (e: KeyboardEvent) => {\n if (e.key === '.' && (e.metaKey || e.ctrlKey)) {\n e.preventDefault()\n setAsideOpen(!asideOpen)\n }\n }\n window.addEventListener('keydown', onKey)\n return () => window.removeEventListener('keydown', onKey)\n }, [asideOpen, setAsideOpen])\n\n const ctxValue = React.useMemo<AppShellContextValue>(\n () => ({ layout, asideOpen, setAsideOpen, isMobile }),\n [layout, asideOpen, setAsideOpen, isMobile]\n )\n\n // ── Layout grid(2 mode)──\n // primary-sidebar:\n // row1: [sidebar (頂天)][main col (header + main)][aside (頂天)]\n // primary-header:\n // row1: [header (橫跨整 viewport, banner role)]\n // row2: [sidebar][main][aside]\n\n // AppShellAside 自決 inline vs modal mode(via AppShellContext.isMobile)。\n // AppShell 一律只 render `{aside}` 一次,AppShellAside 內部根據 isMobile 決定 render 形式。\n\n if (layout === 'primary-header') {\n // primary-header layout(2026-05-21 v2 — user clarification「primary-header = primary-sidebar + 一條 global header」):\n // 結構:row1 globalHeader(全寬 global bar)/ row2 [sidebar][main col: localHeader + main][aside]\n // - globalHeader = 跨頁 account / workspace switcher / notifications(對齊 GitHub top nav / Slack workspace bar)\n // - header = local page header,**仍存在**(per page actions / breadcrumb,對齊 GitHub repo header / Slack channel header / Gmail email-list toolbar 2-layer 慣例)\n // Consumer 必傳 `<Sidebar viewportInsetTop=\"var(--chrome-header-height)\">` 讓 sidebar 從 globalHeader 下方起算。\n return (\n <AppShellContext.Provider value={ctxValue}>\n <div\n ref={ref}\n className={cn('flex h-svh w-full flex-col overflow-hidden bg-canvas', className)}\n {...props}\n >\n <SkipToMain />\n {/* Row 1:Global header(account / workspace switcher / notifications,橫跨整 viewport)*/}\n {globalHeader && <div className=\"flex-shrink-0\">{globalHeader}</div>}\n {/* Row 2:[sidebar][main col][aside]horizontal row */}\n <div className=\"flex flex-1 min-h-0 w-full\">\n {sidebar}\n {/* Main column:local header + main content(per user model「primary-header = primary-sidebar + global header」)*/}\n <div className=\"flex flex-col flex-1 min-w-0 min-h-0 overflow-hidden\">\n {header && <div className=\"flex-shrink-0\">{header}</div>}\n <main\n id=\"app-shell-main\"\n tabIndex={-1}\n className=\"flex-1 min-w-0 min-h-0 overflow-y-auto focus-visible:focus-ring-inset\"\n >\n {children}\n </main>\n </div>\n {aside}\n </div>\n </div>\n </AppShellContext.Provider>\n )\n }\n\n // primary-sidebar layout\n return (\n <AppShellContext.Provider value={ctxValue}>\n <div\n ref={ref}\n className={cn('flex h-svh w-full overflow-hidden bg-canvas', className)}\n {...props}\n >\n <SkipToMain />\n {/* Sidebar — 頂天 */}\n {sidebar}\n {/* Main column(header + main 垂直堆)*/}\n <div className=\"flex flex-col flex-1 min-w-0 min-h-0 overflow-hidden\">\n {header && (\n // Header 在 main column 內(main col sibling,非 main descendant)。ChromeHeader 自\n // 2026-05-20 起 render `<header>`(chrome-header.tsx);依 W3C HTML-AAM,`<div>` wrapper\n // 不 scope out banner(只有 main/article/aside/nav/section 能)→ 仍得 implicit banner ✓\n // (對齊 app-shell.spec.md「W3C ARIA in HTML banner rule」段)。\n <div className=\"flex-shrink-0\">{header}</div>\n )}\n <main\n id=\"app-shell-main\"\n tabIndex={-1}\n className=\"flex-1 min-h-0 overflow-y-auto focus-visible:focus-ring-inset\"\n >\n {children}\n </main>\n </div>\n {/* Aside slot — desktop inline OR mobile Sheet,內部自決 */}\n {aside}\n </div>\n </AppShellContext.Provider>\n )\n }\n)\nAppShell.displayName = 'AppShell'\n\n// ── AppShellAside sub-component ──────────────────────────────────────────────\n\n/**\n * AppShellAside — right panel:standard inline(desktop) vs modal overlay(mobile)。\n *\n * Desktop(viewport ≥ 768px):\n * - Render 直接放 layout grid 右側(AppShell 恆 render {aside};AppShellAside 於 !asideOpen 自行 return null — 內部自決,非 via parent)\n * - 不蓋 mask / background 可操作 / 佔 layout 寬\n * - Vertical extent:primary-sidebar → 頂天立地 / primary-header → header 下方\n *\n * Mobile(viewport < 768px):\n * - Render 走 Sheet primitive(side=\"right\",per sheet.spec.md)\n * - Mask 蓋 / background 不可操作 / 不佔 layout 寬\n * - title 強制(aria-labelledby per sheet.spec.md:98)\n */\nconst AppShellAside = React.forwardRef<HTMLElement, AppShellAsideProps>(\n ({ title, width, children, className }, ref) => {\n const { asideOpen, setAsideOpen, isMobile } = useAppShell()\n const isXl = useIsXl()\n const resolvedWidth = resolveAsideWidth(width, isXl)\n\n // Shared frame:always-on header(title + close X)+ body(ScrollArea + layoutSpace 規則 1B 父層 padding)\n // 對齊 codex Layer B 2026-05-20「container mode 可變,panel role/content 不該變」+ Notion/Figma right\n // panel 共識(modal vs inline 結構相同,host wrapper 不同)。\n // 2026-05-20 migrate 消費 ChromeHeader primitive(撤回自刻 + 撤回 bg-surface 疊加 — 對齊\n // `header-canonical.spec.md`「6. Background ownership」段「Nested chrome header 透明繼承\n // parent」:aside container 自身已 bg-surface,內 header 不該再畫 bg 避免疊加 drift)。\n // titleNode 依 mode 不同:modal(Sheet)用 SheetTitle 綁 Radix Dialog titleId — 否則\n // SheetContent 的 aria-labelledby 指向不存在的 id → SR 念「unnamed dialog」+ Radix\n // TitleWarning console.error(違 sheet.spec.md:98「Sheet content 必有可見 title」)。\n // inline(aside)用純 <h2>:aside 自身已 aria-label={title},且無 Dialog context 不可用 SheetTitle。\n // R1:mobile Sheet 關閉後焦點還原目標(open 時 snapshot)\n const asideOpenerRef = React.useRef<HTMLElement | null>(null)\n\n const renderFrame = (titleNode: React.ReactNode) => (\n <>\n <ChromeHeader>\n {titleNode}\n <Button\n iconOnly\n dismiss\n size=\"sm\"\n startIcon={XIcon}\n aria-label=\"關閉\"\n onClick={() => setAsideOpen(false)}\n />\n </ChromeHeader>\n <ScrollArea className=\"flex-1 min-h-0\">\n {children}\n </ScrollArea>\n </>\n )\n\n // Modal mode(mobile)— Sheet from right\n // 2026-07-14 dim-9 修:同步轉交 forwarded ref + 合併 consumer className —— 原 mobile 分支\n // 兩者皆遺失,同一 API(ref / className)隨 breakpoint 靜默失效(desktop 有效、mobile 無效)。\n if (isMobile) {\n return (\n <Sheet open={asideOpen} onOpenChange={setAsideOpen}>\n <SheetContent\n ref={ref as React.Ref<HTMLDivElement>}\n side=\"right\"\n // R1 WCAG 2.4.3(2026-07-14,Claude+codex A′ 共識):controlled Sheet 無 SheetTrigger →\n // Radix onCloseAutoFocus triggerRef=null + preventDefault 吃掉 fallback → 焦點掉 <body>。\n // 解:開啟時 snapshot opener(事件 dispatch 時 Radix 尚未移焦;MUI/React Aria/Ant snapshot\n // 共識 + DatePicker Range 先例 date-picker.tsx:1061),關閉手動還原;opener 已 unmount →\n // fallback skip-link main(#app-shell-main tabIndex=-1)。\n onOpenAutoFocus={(e) => {\n asideOpenerRef.current = document.activeElement instanceof HTMLElement ? document.activeElement : null\n handleSheetOpenAutoFocus(e) // 接力 DS 預設 body-focus(自訂 handler 覆寫預設,故顯式呼叫)\n }}\n onCloseAutoFocus={(e) => {\n e.preventDefault()\n const opener = asideOpenerRef.current\n asideOpenerRef.current = null\n if (opener?.isConnected) { opener.focus({ preventScroll: true }); return }\n document.getElementById('app-shell-main')?.focus({ preventScroll: true })\n }}\n className={cn(\n 'w-[min(90vw,var(--app-shell-aside-modal-width))] flex flex-col p-0 [&>button]:hidden',\n className\n )}\n style={{ ['--app-shell-aside-modal-width' as string]: `${resolvedWidth}px` }}\n >\n {renderFrame(<SheetTitle className=\"flex-1\">{title}</SheetTitle>)}\n </SheetContent>\n </Sheet>\n )\n }\n\n // Standard inline mode(desktop)\n if (!asideOpen) return null\n\n return (\n <aside\n ref={ref}\n aria-label={title}\n className={cn(\n 'flex flex-col h-full min-h-0 overflow-hidden',\n 'bg-surface border-l border-divider',\n className\n )}\n style={{ width: resolvedWidth }}\n >\n {renderFrame(<h2 className=\"text-body-lg font-medium flex-1 truncate\">{title}</h2>)}\n </aside>\n )\n }\n)\nAppShellAside.displayName = 'AppShellAside'\n\n// ── Exports ──────────────────────────────────────────────────────────────────\n\n// Story auto-compile metadata — canonical migration(2026-08-01)\n// AppShell is a page-level composition primitive. `layout` is a structural mode,\n// not a visual variant axis; slot controls own their interaction states and sizes.\nexport const appShellMeta = {\n component: 'AppShell',\n family: 'composite',\n variants: {},\n sizes: {},\n states: [],\n tokens: {\n bg: ['bg-canvas', 'bg-surface'],\n fg: ['text-foreground'],\n border: ['border-divider'],\n ring: ['focus-ring-inset', '--ring'],\n },\n defaultVariant: undefined,\n defaultSize: undefined,\n} as const\n\n// code-quality-allow: dead-export useAppShell — public compound API hook(consumer 可自拼 custom aside layout,\n// 對齊 Radix `useDialogContext` / MUI `useFormControl` 慣例)。內部 AppShellAside 已消費(L294),\n// audit script 抓「無 cross-file import」是 false positive(2026-05-21 D2 codify)。\nexport { AppShell, AppShellAside, useAppShell }\n"],"names":["XIcon"],"mappings":"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"}
|
|
@@ -19,6 +19,88 @@ import { type CategoricalColor } from '../../tokens/categorical-color';
|
|
|
19
19
|
* square → rounded-md (4px),用於實體(專案、組織、App)
|
|
20
20
|
*/
|
|
21
21
|
type ColorKey = CategoricalColor;
|
|
22
|
+
/**
|
|
23
|
+
* @internal
|
|
24
|
+
* 第 2 項起往左疊的量。CSS 雙生:`tokens/uiSize/uiSize.css` `--avatar-stack-overlap`(class 字串裡用它;
|
|
25
|
+
* 這個數字給量寬與遮罩幾何用),兩邊相等由 scripts/token-twin-invariant.mjs 鎖住。
|
|
26
|
+
*/
|
|
27
|
+
export declare const AVATAR_STACK_OVERLAP_PX = 2;
|
|
28
|
+
/**
|
|
29
|
+
* @internal
|
|
30
|
+
* 縫寬 = DS 通用的「疊在一起的兩樣東西」之間的縫(`tokens/uiSize/stack-gap.ts`;全域焦點框的 outline-offset、
|
|
31
|
+
* 狀態圓點 / 計數徽章 / 移除 × 的挖空縫、上傳列進度條的縫、步驟條外圈的縫都是它)。
|
|
32
|
+
* 2026-04-02 起舊外圈的寬度(`ring-2`)也是 2px,白底上看起來與舊版相同。
|
|
33
|
+
* Primer 是 1px(`--avatar-border-width: 1px`,`AvatarStack.module.css` L3)——選 2px 是 AI 推導,理由見 avatar.spec.md「頭像堆疊」段。
|
|
34
|
+
*/
|
|
35
|
+
export declare const AVATAR_STACK_GAP_PX = 2;
|
|
36
|
+
/** @internal 移除 × 的幾何(person-display.tsx AvatarDismissOverlay 用同一組畫 ×,這裡用來挖它底下的洞)。 */
|
|
37
|
+
export declare const AVATAR_DISMISS_OVERLAY_PX: {
|
|
38
|
+
readonly top: -1;
|
|
39
|
+
readonly right: -4;
|
|
40
|
+
readonly size: 12;
|
|
41
|
+
};
|
|
42
|
+
/**
|
|
43
|
+
* @internal 自己的挖空(狀態圓點 / 徽章 / 移除 ×)的 class。移除 × 的洞平常半徑為 0(看不出來),
|
|
44
|
+
* 標籤被滑過或焦點在標籤內時(`group/avatar`,與 × 本身顯示的條件相同)才打開 —— 洞的半徑經
|
|
45
|
+
* `--avatar-dismiss-on`(0 / 1)乘進去,半徑本身寫在 style(`--avatar-dismiss-r`),class 裡不寫數字。
|
|
46
|
+
*/
|
|
47
|
+
export declare const AVATAR_OWN_CUTOUT_CLASS = "[mask-image:var(--avatar-own-cutout)] [mask-composite:var(--avatar-own-composite)] [--avatar-dismiss-on:0] group-hover/avatar:[--avatar-dismiss-on:1] group-focus-within/avatar:[--avatar-dismiss-on:1]";
|
|
48
|
+
/** @internal 堆疊 + 自己的挖空(結構選擇器同 `AVATAR_STACK_CUTOUT_CLASS`)。 */
|
|
49
|
+
export declare const AVATAR_STACK_OWN_CUTOUT_CLASS = "[:has(>[data-avatar-stack]):not([hidden])~*>[data-avatar-stack]>&]:[mask-image:var(--avatar-stack-cutout),var(--avatar-own-cutout)] [:has(>[data-avatar-stack]):not([hidden])~*>[data-avatar-stack]>&]:[mask-composite:intersect,var(--avatar-own-composite)]";
|
|
50
|
+
/**
|
|
51
|
+
* @internal
|
|
52
|
+
* 算出自己的挖空遮罩。所有座標相對於「圓」那一層(與最外層同大),用 `100%` 表示右 / 下緣,fill 模式也成立。
|
|
53
|
+
* - 狀態圓點:貼右下角,直徑 dotSize,縫 dotGap。
|
|
54
|
+
* - 移除 ×:`AVATAR_DISMISS_OVERLAY_PX`(右上,凸出 4px),縫 = 通用縫;半徑乘 `--avatar-dismiss-on`。
|
|
55
|
+
* - 計數徽章:`-top-1 -right-1`(各凸出 4px)的膠囊,寬高由 ResizeObserver 量(字數不同寬度不同),縫 = 通用縫。
|
|
56
|
+
* 回 `null` = 沒有任何東西疊在上面,不掛遮罩(照舊)。
|
|
57
|
+
*/
|
|
58
|
+
export declare function avatarOverlayCutout({ dot, dismiss, badge }: {
|
|
59
|
+
dot?: {
|
|
60
|
+
sizePx: number;
|
|
61
|
+
gapPx: number;
|
|
62
|
+
};
|
|
63
|
+
dismiss?: boolean;
|
|
64
|
+
badge?: {
|
|
65
|
+
widthPx: number;
|
|
66
|
+
heightPx: number;
|
|
67
|
+
} | null;
|
|
68
|
+
}): {
|
|
69
|
+
image: string;
|
|
70
|
+
composite: string;
|
|
71
|
+
} | null;
|
|
72
|
+
/**
|
|
73
|
+
* @internal
|
|
74
|
+
* 挖空遮罩:把「左邊那顆(同尺寸的圓)+ 一圈縫」從這一層挖掉。sizePx = 堆疊裡每顆的直徑。
|
|
75
|
+
*/
|
|
76
|
+
export declare function avatarStackCutoutImage(sizePx: number): string;
|
|
77
|
+
/**
|
|
78
|
+
* @internal
|
|
79
|
+
* 被挖那一層(圓)的 className + style。className 是字面值(Tailwind 要能掃到),
|
|
80
|
+
* 幾何只從上方兩個常數來(經 CSS 變數 `--avatar-stack-cutout` 傳入),不在 class 字串裡重寫數字。
|
|
81
|
+
* `-webkit-mask-image` 由 Tailwind v4 的 Lightning CSS 自動補(建置產物實測兩個屬性都在)。
|
|
82
|
+
*/
|
|
83
|
+
export declare const AVATAR_STACK_CUTOUT_CLASS = "[:has(>[data-avatar-stack]):not([hidden])~*>[data-avatar-stack]>&]:[mask-image:var(--avatar-stack-cutout)]";
|
|
84
|
+
/** @internal 被挖那一層的 style(`--avatar-stack-cutout`),搭配 `AVATAR_STACK_CUTOUT_CLASS`。 */
|
|
85
|
+
export declare function avatarStackCutoutStyle(sizePx: number): React.CSSProperties;
|
|
86
|
+
/**
|
|
87
|
+
* @internal
|
|
88
|
+
* 堆疊列(每一項的父層)。`isolate` = 疊放順序只在這一列裡比,不跟頁面上其他層(黏住的表頭等)比;
|
|
89
|
+
* 對齊 Primer `.AvatarStack { isolation: isolate }`(`AvatarStack.module.css` L12)。
|
|
90
|
+
*/
|
|
91
|
+
export declare const AVATAR_STACK_CLASS = "isolate";
|
|
92
|
+
/**
|
|
93
|
+
* @internal
|
|
94
|
+
* 每一項(item)的外層 className + style(`avatarStackItemStyle`):
|
|
95
|
+
* - 第 2 項起往左疊 `AVATAR_STACK_OVERLAP_PX`;
|
|
96
|
+
* - 越前面越上層(第一顆在最上面,+N 在最下面)—— 重疊那 2px 滑過時開的是看得到的那顆(上面那顆)的名片;
|
|
97
|
+
* - 裡面有東西拿到鍵盤焦點(`:focus-visible`)時整項升到最上層:焦點框往外畫(focus-canonical.md「疊層/徽章不算鄰居」),
|
|
98
|
+
* 不升的話左半圈會被上面那顆蓋住。對齊 SegmentedControl `focus-visible:z-20`「讓往外長的框壓在相鄰 item 之上」。
|
|
99
|
+
* count = 項目總數(含 +N)。
|
|
100
|
+
*/
|
|
101
|
+
export declare const AVATAR_STACK_ITEM_CLASS = "relative inline-flex z-[var(--avatar-stack-z)] has-[:focus-visible]:z-[var(--avatar-stack-z-focus)]";
|
|
102
|
+
/** @internal 每一項的 style,搭配 `AVATAR_STACK_ITEM_CLASS`(說明見上)。 */
|
|
103
|
+
export declare function avatarStackItemStyle(index: number, count: number): React.CSSProperties;
|
|
22
104
|
export interface AvatarProps extends React.HTMLAttributes<HTMLDivElement> {
|
|
23
105
|
/** 尺寸:number (px) 或 'fill'(填滿父容器,由父層決定大小)。預設 32 */
|
|
24
106
|
size?: number | 'fill';
|
|
@@ -58,6 +140,18 @@ export interface AvatarProps extends React.HTMLAttributes<HTMLDivElement> {
|
|
|
58
140
|
* 只有人員 avatar 需要傳;實體 avatar(專案、組織)不傳。
|
|
59
141
|
*/
|
|
60
142
|
hoverCard?: React.ReactNode;
|
|
143
|
+
/**
|
|
144
|
+
* 這顆頭像是頭像堆疊裡的一項(avatar.spec.md「頭像堆疊(疊在一起時)」)。
|
|
145
|
+
* true → 最外層帶 `data-avatar-stack`,圓那一層被左邊那顆蓋住的地方 + 2px 縫整片挖掉;
|
|
146
|
+
* 第一顆(前面沒有看得見的堆疊項目)不挖。只在固定尺寸(`size` 為數字)時生效。
|
|
147
|
+
*/
|
|
148
|
+
stacked?: boolean;
|
|
149
|
+
/**
|
|
150
|
+
* @internal
|
|
151
|
+
* 編輯態人員標籤的移除 ×(person-display.tsx AvatarDismissOverlay)浮在這顆頭像右上;
|
|
152
|
+
* 標籤被滑過 / 焦點在標籤內時(`group/avatar`,與 × 顯示的條件相同)把 × 底下連縫從圓上挖掉。
|
|
153
|
+
*/
|
|
154
|
+
dismissCutout?: boolean;
|
|
61
155
|
}
|
|
62
156
|
export interface AvatarData {
|
|
63
157
|
/** 圖片 URL */
|
|
@@ -84,7 +178,7 @@ export declare const avatarMeta: {
|
|
|
84
178
|
readonly tokens: {
|
|
85
179
|
readonly bg: readonly ["bg-surface-raised"];
|
|
86
180
|
readonly fg: readonly ["--foreground", "--on-emphasis"];
|
|
87
|
-
readonly ring: readonly ["ring
|
|
181
|
+
readonly ring: readonly ["--ring"];
|
|
88
182
|
};
|
|
89
183
|
};
|
|
90
184
|
declare const Avatar: React.MemoExoticComponent<React.ForwardRefExoticComponent<AvatarProps & React.RefAttributes<HTMLDivElement>>>;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"avatar.d.ts","sourceRoot":"","sources":["../../../src/components/Avatar/avatar.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAA;AAE9B,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,cAAc,CAAA;AAE9C,OAAO,EAAuC,KAAK,gBAAgB,EAAE,MAAM,0CAA0C,CAAA;
|
|
1
|
+
{"version":3,"file":"avatar.d.ts","sourceRoot":"","sources":["../../../src/components/Avatar/avatar.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAA;AAE9B,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,cAAc,CAAA;AAE9C,OAAO,EAAuC,KAAK,gBAAgB,EAAE,MAAM,0CAA0C,CAAA;AAOrH;;;;;;;;;;;;;;;;GAgBG;AAQH,KAAK,QAAQ,GAAG,gBAAgB,CAAA;AAoFhC;;;;GAIG;AACH,eAAO,MAAM,uBAAuB,IAAI,CAAA;AACxC;;;;;;GAMG;AACH,eAAO,MAAM,mBAAmB,IAAe,CAAA;AAgB/C,sFAAsF;AACtF,eAAO,MAAM,yBAAyB;;;;CAA4C,CAAA;AAClF;;;;GAIG;AACH,eAAO,MAAM,uBAAuB,4MACuK,CAAA;AAC3M,gEAAgE;AAChE,eAAO,MAAM,6BAA6B,kQACuN,CAAA;AASjQ;;;;;;;GAOG;AACH,wBAAgB,mBAAmB,CAAC,EAAE,GAAG,EAAE,OAAO,EAAE,KAAK,EAAE,EAAE;IAC3D,GAAG,CAAC,EAAE;QAAE,MAAM,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,CAAA;IACvC,OAAO,CAAC,EAAE,OAAO,CAAA;IACjB,KAAK,CAAC,EAAE;QAAE,OAAO,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAA;KAAE,GAAG,IAAI,CAAA;CACrD,GAAG;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,SAAS,EAAE,MAAM,CAAA;CAAE,GAAG,IAAI,CA0B9C;AAED;;;GAGG;AACH,wBAAgB,sBAAsB,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAK7D;AAED;;;;;GAKG;AACH,eAAO,MAAM,yBAAyB,+GACwE,CAAA;AAC9G,sFAAsF;AACtF,wBAAgB,sBAAsB,CAAC,MAAM,EAAE,MAAM,GAAG,KAAK,CAAC,aAAa,CAE1E;AAED;;;;GAIG;AACH,eAAO,MAAM,kBAAkB,YAAY,CAAA;AAC3C;;;;;;;;GAQG;AACH,eAAO,MAAM,uBAAuB,wGACmE,CAAA;AACvG,+DAA+D;AAC/D,wBAAgB,oBAAoB,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,KAAK,CAAC,aAAa,CAMtF;AAID,MAAM,WAAW,WAAY,SAAQ,KAAK,CAAC,cAAc,CAAC,cAAc,CAAC;IACvE,mDAAmD;IACnD,IAAI,CAAC,EAAE,MAAM,GAAG,MAAM,CAAA;IACtB,0CAA0C;IAC1C,KAAK,CAAC,EAAE,QAAQ,GAAG,QAAQ,CAAA;IAC3B,aAAa;IACb,GAAG,CAAC,EAAE,MAAM,CAAA;IACZ,+BAA+B;IAC/B,GAAG,CAAC,EAAE,MAAM,CAAA;IACZ,0BAA0B;IAC1B,IAAI,CAAC,EAAE,UAAU,CAAA;IACjB,2CAA2C;IAC3C,KAAK,CAAC,EAAE,QAAQ,CAAA;IAChB,yGAAyG;IACzG,KAAK,CAAC,EAAE,OAAO,CAAA;IACf;;;;OAIG;IACH,MAAM,CAAC,EAAE,QAAQ,GAAG,MAAM,GAAG,MAAM,GAAG,SAAS,CAAA;IAC/C;;;;;;OAMG;IACH,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB;;;OAGG;IACH,cAAc,CAAC,EAAE,MAAM,CAAA;IACvB;;;OAGG;IACH,SAAS,CAAC,EAAE,KAAK,CAAC,SAAS,CAAA;IAC3B;;;;OAIG;IACH,OAAO,CAAC,EAAE,OAAO,CAAA;IACjB;;;;OAIG;IACH,aAAa,CAAC,EAAE,OAAO,CAAA;CACxB;AA4PD,MAAM,WAAW,UAAU;IACzB,aAAa;IACb,GAAG,CAAC,EAAE,MAAM,CAAA;IACZ,+BAA+B;IAC/B,GAAG,EAAE,MAAM,CAAA;IACX,2CAA2C;IAC3C,KAAK,CAAC,EAAE,QAAQ,CAAA;IAChB;;;;;;OAMG;IACH,SAAS,CAAC,EAAE,KAAK,CAAC,SAAS,CAAA;CAC5B;AAID,eAAO,MAAM,UAAU;;;;;;;;;;;CAab,CAAA;AAGV,QAAA,MAAM,MAAM,+GAA0B,CAAA;AAEtC,OAAO,EAAE,MAAM,EAAE,CAAA"}
|
|
@@ -7,6 +7,7 @@ import { HoverCard, HoverCardTrigger, HoverCardContent } from "../HoverCard/hove
|
|
|
7
7
|
import { MOTION_DELAY_CLOSE_MS, MOTION_DELAY_RICH_MS } from "../../tokens/motion/motion.js";
|
|
8
8
|
import { Badge } from "../Badge/badge.js";
|
|
9
9
|
import { useTableIsScrolling, useResolvedFieldDisabled } from "../Field/field-context.js";
|
|
10
|
+
import { STACK_GAP_PX } from "../../tokens/uiSize/stack-gap.js";
|
|
10
11
|
const COLOR_MAP = {
|
|
11
12
|
subtle: {
|
|
12
13
|
neutral: { bg: "var(--muted)", text: "var(--foreground)" },
|
|
@@ -44,8 +45,60 @@ function useDocumentTheme() {
|
|
|
44
45
|
}, []);
|
|
45
46
|
return theme;
|
|
46
47
|
}
|
|
48
|
+
const AVATAR_STACK_OVERLAP_PX = 2;
|
|
49
|
+
const AVATAR_STACK_GAP_PX = STACK_GAP_PX;
|
|
50
|
+
const AVATAR_DISMISS_OVERLAY_PX = { top: -1, right: -4, size: 12 };
|
|
51
|
+
const AVATAR_OWN_CUTOUT_CLASS = "[mask-image:var(--avatar-own-cutout)] [mask-composite:var(--avatar-own-composite)] [--avatar-dismiss-on:0] group-hover/avatar:[--avatar-dismiss-on:1] group-focus-within/avatar:[--avatar-dismiss-on:1]";
|
|
52
|
+
const AVATAR_STACK_OWN_CUTOUT_CLASS = "[:has(>[data-avatar-stack]):not([hidden])~*>[data-avatar-stack]>&]:[mask-image:var(--avatar-stack-cutout),var(--avatar-own-cutout)] [:has(>[data-avatar-stack]):not([hidden])~*>[data-avatar-stack]>&]:[mask-composite:intersect,var(--avatar-own-composite)]";
|
|
53
|
+
const keepOutsideCircle = (cx, cy, r) => `radial-gradient(circle at ${cx} ${cy}, transparent calc(${r} - 0.25px), #000 calc(${r} + 0.25px))`;
|
|
54
|
+
const holeCircle = (cx, cy, r) => `radial-gradient(circle at ${cx} ${cy}, #000 calc(${r} - 0.25px), transparent calc(${r} + 0.25px))`;
|
|
55
|
+
function avatarOverlayCutout({ dot, dismiss, badge }) {
|
|
56
|
+
const keeps = [];
|
|
57
|
+
if (dot) {
|
|
58
|
+
const c = `calc(100% - ${dot.sizePx / 2}px)`;
|
|
59
|
+
keeps.push(keepOutsideCircle(c, c, `${dot.sizePx / 2 + dot.gapPx}px`));
|
|
60
|
+
}
|
|
61
|
+
if (dismiss) {
|
|
62
|
+
const { top, right, size } = AVATAR_DISMISS_OVERLAY_PX;
|
|
63
|
+
keeps.push(keepOutsideCircle(`calc(100% - ${right}px - ${size / 2}px)`, `${top + size / 2}px`, "calc(var(--avatar-dismiss-r) * var(--avatar-dismiss-on))"));
|
|
64
|
+
}
|
|
65
|
+
const layers = [...keeps];
|
|
66
|
+
const ops = keeps.map(() => "intersect");
|
|
67
|
+
if (badge && badge.widthPx > 0 && badge.heightPx > 0) {
|
|
68
|
+
const { widthPx: w, heightPx: h } = badge;
|
|
69
|
+
const top = -4, right = -4, g = STACK_GAP_PX;
|
|
70
|
+
const cy = `${top + h / 2}px`, r = `${h / 2 + g}px`;
|
|
71
|
+
const xL = `calc(100% - ${right}px - ${w}px + ${h / 2}px)`, xR = `calc(100% - ${right}px - ${h / 2}px)`;
|
|
72
|
+
const full = "linear-gradient(#000 0 0)";
|
|
73
|
+
const left = holeCircle(xL, cy, r), rightC = holeCircle(xR, cy, r);
|
|
74
|
+
const bandX = `linear-gradient(to right, transparent ${xL}, #000 ${xL}, #000 ${xR}, transparent ${xR})`;
|
|
75
|
+
const bandY = `linear-gradient(to bottom, transparent ${top - g}px, #000 ${top - g}px, #000 ${top + h + g}px, transparent ${top + h + g}px)`;
|
|
76
|
+
layers.push(full, left, rightC, bandX, bandY);
|
|
77
|
+
ops.push("subtract", "add", "add", "intersect");
|
|
78
|
+
}
|
|
79
|
+
if (layers.length === 0) return null;
|
|
80
|
+
return { image: layers.join(", "), composite: ops.length ? ops.join(", ") : "add" };
|
|
81
|
+
}
|
|
82
|
+
function avatarStackCutoutImage(sizePx) {
|
|
83
|
+
const edge = sizePx / 2 + AVATAR_STACK_GAP_PX;
|
|
84
|
+
const centerX = AVATAR_STACK_OVERLAP_PX - sizePx / 2;
|
|
85
|
+
return `radial-gradient(circle at ${centerX}px 50%, transparent ${edge - 0.25}px, #000 ${edge + 0.25}px)`;
|
|
86
|
+
}
|
|
87
|
+
const AVATAR_STACK_CUTOUT_CLASS = "[:has(>[data-avatar-stack]):not([hidden])~*>[data-avatar-stack]>&]:[mask-image:var(--avatar-stack-cutout)]";
|
|
88
|
+
function avatarStackCutoutStyle(sizePx) {
|
|
89
|
+
return { ["--avatar-stack-cutout"]: avatarStackCutoutImage(sizePx) };
|
|
90
|
+
}
|
|
91
|
+
const AVATAR_STACK_CLASS = "isolate";
|
|
92
|
+
const AVATAR_STACK_ITEM_CLASS = "relative inline-flex z-[var(--avatar-stack-z)] has-[:focus-visible]:z-[var(--avatar-stack-z-focus)]";
|
|
93
|
+
function avatarStackItemStyle(index, count) {
|
|
94
|
+
return {
|
|
95
|
+
marginLeft: index > 0 ? -AVATAR_STACK_OVERLAP_PX : void 0,
|
|
96
|
+
["--avatar-stack-z"]: count - index,
|
|
97
|
+
["--avatar-stack-z-focus"]: count + 1
|
|
98
|
+
};
|
|
99
|
+
}
|
|
47
100
|
const AvatarInner = React.forwardRef(
|
|
48
|
-
({ size = 32, shape = "circle", src, alt, icon: Icon, color = "neutral", solid = false, status, badgeCount, badgeAriaLabel, hoverCard, className, style, ...props }, ref) => {
|
|
101
|
+
({ size = 32, shape = "circle", src, alt, icon: Icon, color = "neutral", solid = false, status, badgeCount, badgeAriaLabel, hoverCard, stacked = false, dismissCutout = false, className, style, ...props }, ref) => {
|
|
49
102
|
var _a;
|
|
50
103
|
const [imgError, setImgError] = React.useState(false);
|
|
51
104
|
const [prevSrc, setPrevSrc] = React.useState(src);
|
|
@@ -68,7 +121,28 @@ const AvatarInner = React.forwardRef(
|
|
|
68
121
|
const showText = !showImage && !showIcon && alt;
|
|
69
122
|
const FallbackIcon = Icon ?? User;
|
|
70
123
|
const dotSize = isFill ? 10 : Math.max(8, Math.min(16, Math.round(numSize * 0.28)));
|
|
71
|
-
const
|
|
124
|
+
const dotGap = dotSize >= 12 ? STACK_GAP_PX + 1 : STACK_GAP_PX;
|
|
125
|
+
const isStackItem = stacked && !isFill;
|
|
126
|
+
const showBadge = typeof badgeCount === "number" && badgeCount > 0;
|
|
127
|
+
const badgeRef = React.useRef(null);
|
|
128
|
+
const [badgeBox, setBadgeBox] = React.useState(null);
|
|
129
|
+
React.useLayoutEffect(() => {
|
|
130
|
+
const el = badgeRef.current;
|
|
131
|
+
if (!el) {
|
|
132
|
+
setBadgeBox(null);
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
const read = () => setBadgeBox((prev) => prev && prev.widthPx === el.offsetWidth && prev.heightPx === el.offsetHeight ? prev : { widthPx: el.offsetWidth, heightPx: el.offsetHeight });
|
|
136
|
+
read();
|
|
137
|
+
const obs = new ResizeObserver(read);
|
|
138
|
+
obs.observe(el);
|
|
139
|
+
return () => obs.disconnect();
|
|
140
|
+
}, [showBadge, badgeCount]);
|
|
141
|
+
const ownCutout = avatarOverlayCutout({
|
|
142
|
+
dot: status ? { sizePx: dotSize, gapPx: dotGap } : void 0,
|
|
143
|
+
dismiss: dismissCutout,
|
|
144
|
+
badge: showBadge ? badgeBox : null
|
|
145
|
+
});
|
|
72
146
|
const avatarEl = /* @__PURE__ */ jsxs(
|
|
73
147
|
"div",
|
|
74
148
|
{
|
|
@@ -77,13 +151,22 @@ const AvatarInner = React.forwardRef(
|
|
|
77
151
|
isFill && "w-full h-full",
|
|
78
152
|
// 2026-05-13 R3.5 self-dim:Avatar 在 disabled Field wrapper context 內自 dim
|
|
79
153
|
// (取代 field-wrapper.tsx default/bare/naked disabled blanket opacity-disabled 逃生艙)
|
|
80
|
-
isDisabledInField && "opacity-disabled"
|
|
154
|
+
isDisabledInField && "opacity-disabled",
|
|
155
|
+
isStackItem && !ownCutout && AVATAR_STACK_CUTOUT_CLASS,
|
|
156
|
+
ownCutout && AVATAR_OWN_CUTOUT_CLASS,
|
|
157
|
+
ownCutout && isStackItem && AVATAR_STACK_OWN_CUTOUT_CLASS
|
|
81
158
|
),
|
|
82
159
|
style: {
|
|
83
160
|
...isFill ? { containerType: "inline-size" } : { width: numSize, height: numSize },
|
|
84
161
|
borderRadius: radius,
|
|
85
162
|
backgroundColor: showImage ? void 0 : colors.bg,
|
|
86
|
-
color: showImage ? void 0 : colors.text
|
|
163
|
+
color: showImage ? void 0 : colors.text,
|
|
164
|
+
...isStackItem ? avatarStackCutoutStyle(numSize) : null,
|
|
165
|
+
...ownCutout ? {
|
|
166
|
+
["--avatar-own-cutout"]: ownCutout.image,
|
|
167
|
+
["--avatar-own-composite"]: ownCutout.composite,
|
|
168
|
+
["--avatar-dismiss-r"]: `${AVATAR_DISMISS_OVERLAY_PX.size / 2 + STACK_GAP_PX}px`
|
|
169
|
+
} : null
|
|
87
170
|
},
|
|
88
171
|
"data-avatar-size": isFill ? "fill" : numSize,
|
|
89
172
|
role: !showImage && alt && !hoverCard ? "img" : void 0,
|
|
@@ -119,7 +202,8 @@ const AvatarInner = React.forwardRef(
|
|
|
119
202
|
} : {};
|
|
120
203
|
const focusableClass = hoverCard ? cn("", shape === "circle" ? "rounded-full" : "rounded-md") : "";
|
|
121
204
|
const boxClass = isFill ? "" : "w-fit";
|
|
122
|
-
const
|
|
205
|
+
const stackAttr = isStackItem ? { "data-avatar-stack": "" } : {};
|
|
206
|
+
const baseEl = !hasOverlay ? /* @__PURE__ */ jsx("div", { ref, className: cn("inline-flex shrink-0", boxClass, focusableClass, className), style, ...stackAttr, ...focusableProps, ...props, children: avatarEl }) : /* @__PURE__ */ jsxs("div", { ref, className: cn("relative inline-flex shrink-0", boxClass, focusableClass, className), style, ...stackAttr, ...focusableProps, ...props, children: [
|
|
123
207
|
avatarEl,
|
|
124
208
|
status && /* @__PURE__ */ jsx(
|
|
125
209
|
"span",
|
|
@@ -130,25 +214,20 @@ const AvatarInner = React.forwardRef(
|
|
|
130
214
|
height: dotSize,
|
|
131
215
|
bottom: 0,
|
|
132
216
|
right: 0,
|
|
133
|
-
backgroundColor: STATUS_DOT_COLOR[status]
|
|
134
|
-
boxShadow: `0 0 0 ${dotBorder}px var(--surface-raised, var(--canvas))`
|
|
217
|
+
backgroundColor: STATUS_DOT_COLOR[status]
|
|
135
218
|
},
|
|
136
219
|
"aria-hidden": true
|
|
137
220
|
}
|
|
138
221
|
),
|
|
139
|
-
|
|
222
|
+
showBadge && /* @__PURE__ */ jsx("span", { ref: badgeRef, className: "absolute -top-1 -right-1 inline-flex", children: /* @__PURE__ */ jsx(
|
|
140
223
|
Badge,
|
|
141
224
|
{
|
|
142
225
|
variant: "critical",
|
|
143
226
|
count: badgeCount,
|
|
144
227
|
max: 99,
|
|
145
|
-
className: "absolute -top-1 -right-1",
|
|
146
|
-
style: {
|
|
147
|
-
boxShadow: `0 0 0 2px var(--surface-raised, var(--canvas))`
|
|
148
|
-
},
|
|
149
228
|
"aria-label": badgeAriaLabel ?? `${badgeCount} unread`
|
|
150
229
|
}
|
|
151
|
-
)
|
|
230
|
+
) })
|
|
152
231
|
] });
|
|
153
232
|
if (!hoverCard || isTableScrolling) return baseEl;
|
|
154
233
|
return /* @__PURE__ */ jsxs(HoverCard, { openDelay: MOTION_DELAY_RICH_MS, closeDelay: MOTION_DELAY_CLOSE_MS, children: [
|
|
@@ -178,13 +257,25 @@ const avatarMeta = {
|
|
|
178
257
|
tokens: {
|
|
179
258
|
bg: ["bg-surface-raised"],
|
|
180
259
|
fg: ["--foreground", "--on-emphasis"],
|
|
181
|
-
ring: ["ring
|
|
260
|
+
ring: ["--ring"]
|
|
182
261
|
}
|
|
183
262
|
};
|
|
184
263
|
AvatarInner.displayName = "Avatar";
|
|
185
264
|
const Avatar = React.memo(AvatarInner);
|
|
186
265
|
export {
|
|
266
|
+
AVATAR_DISMISS_OVERLAY_PX,
|
|
267
|
+
AVATAR_OWN_CUTOUT_CLASS,
|
|
268
|
+
AVATAR_STACK_CLASS,
|
|
269
|
+
AVATAR_STACK_CUTOUT_CLASS,
|
|
270
|
+
AVATAR_STACK_GAP_PX,
|
|
271
|
+
AVATAR_STACK_ITEM_CLASS,
|
|
272
|
+
AVATAR_STACK_OVERLAP_PX,
|
|
273
|
+
AVATAR_STACK_OWN_CUTOUT_CLASS,
|
|
187
274
|
Avatar,
|
|
188
|
-
avatarMeta
|
|
275
|
+
avatarMeta,
|
|
276
|
+
avatarOverlayCutout,
|
|
277
|
+
avatarStackCutoutImage,
|
|
278
|
+
avatarStackCutoutStyle,
|
|
279
|
+
avatarStackItemStyle
|
|
189
280
|
};
|
|
190
281
|
//# sourceMappingURL=avatar.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"avatar.js","sources":["../../../src/components/Avatar/avatar.tsx"],"sourcesContent":["// @benchmark-unverified-blanket: file-level retraction per M22 (d) — claims herein not individually URL-cited; treat as unverified visual/usage rumor unless retrofit per-claim. Hook escape preserved.\nimport * as React from 'react'\nimport { User } from 'lucide-react'\nimport type { LucideIcon } from 'lucide-react'\nimport { cn } from '@/lib/utils'\nimport { CAT_SUBTLE_TOKENS, CAT_SOLID_TOKENS, type CategoricalColor } from '@/design-system/tokens/categorical-color'\nimport { HoverCard, HoverCardTrigger, HoverCardContent } from '@/design-system/components/HoverCard/hover-card'\nimport { MOTION_DELAY_RICH_MS, MOTION_DELAY_CLOSE_MS } from '@/design-system/tokens/motion/motion'\nimport { Badge } from '@/design-system/components/Badge/badge'\nimport { useResolvedFieldDisabled, useTableIsScrolling } from '@/design-system/components/Field/field-context'\n\n/**\n * Avatar — 頭像元件\n *\n * 三種內容模式(按優先順序):\n * 1. src → 圖片\n * 2. icon → icon 在底色圓/方形內\n * 3. alt → 取首字作文字 fallback\n * 4. 都沒有 → 預設 User icon\n *\n * ── 尺寸 ──\n * size 接受任意 px 值,icon 自動 = round_even(size × 0.6)\n * 文字 fallback 字體 = size × 0.5\n *\n * ── 形狀 ──\n * circle(預設)→ rounded-full,用於人物\n * square → rounded-md (4px),用於實體(專案、組織、App)\n */\n\n// ── 色彩 ──\n// **消費 categorical-color SSOT**(CAT_SUBTLE_TOKENS / CAT_SOLID_TOKENS,key X 一律對 `--color-X-*`,\n// 1:1 零 offset)。subtle=step-1 底 + step-7 字;solid=step-6 全色底 + on-emphasis 字\n//(亮 hue yellow/amber/orange/lime 用 --on-emphasis-dark 深字;green 白字例外)。neutral 非色相,自處理(subtle=muted、solid=neutral-9)。\n// 2026-06-04 修:原 `red` 誤接 `--color-deep-orange-*`(red=品牌紅 hue-25 ≠ deep-orange);\n// 改消費 SSOT 後 red→`--color-red-*`,並補齊全 12 色相。\ntype ColorKey = CategoricalColor\ntype VariantKey = 'subtle' | 'solid'\n\nconst COLOR_MAP: Record<VariantKey, Record<ColorKey, { bg: string; text: string }>> = {\n subtle: {\n neutral: { bg: 'var(--muted)', text: 'var(--foreground)' },\n ...CAT_SUBTLE_TOKENS,\n },\n solid: {\n neutral: { bg: 'var(--color-neutral-9)', text: 'var(--inverse-fg)' },\n ...CAT_SOLID_TOKENS,\n },\n}\n\n// ── Icon size: round to nearest even, ≈ 60% ──\nfunction getIconSize(avatarSize: number): number {\n return Math.round((avatarSize * 0.6) / 2) * 2\n}\n\n// ── Text fallback: first character ──\nfunction getInitial(text: string): string {\n return text.trim().charAt(0).toUpperCase()\n}\n\n// Semantic presence tokens — 見 color/semantic.css\n// Module-level constant(2026-04-22 D3 perf audit):從 render body 移到 module scope,\n// 避免每次 Avatar render 都 re-declare 此 4-entry object(Low impact 但渲染大量 avatars 時累積可觀)\nconst STATUS_DOT_COLOR: Record<string, string> = {\n online: 'var(--status-online)',\n away: 'var(--status-away)',\n busy: 'var(--status-busy)',\n offline: 'var(--status-offline)',\n}\n\n// ── useDocumentTheme(2026-04-23;M3 Portal 逃脫防線,scope verified 2026-04-25)──\n// 讀 `<html data-theme>` 並 observe mutation。用於 Avatar hoverCard ProfileCard:\n// Portal 後的 HoverCardContent 會繼承 trigger subtree theme(如 OverflowIndicator\n// dark tooltip 內部),造成 ProfileCard 被污染成 dark。顯式 bind app-level theme\n// 確保 ProfileCard 永遠跟 app 本身 theme 一致(light-in-light-app / dark-in-dark-app)。\n//\n// 範圍 audit 2026-04-25:觀察對象是 `document.documentElement` 自有 DOM,非 3rd-party\n// lib 內部(不屬 M2 scope);attributeFilter 限定 `data-theme` 單一 attr,re-render 成本\n// 為每次全站 theme 切換 × Avatar 數量,可接受。\nfunction useDocumentTheme(): string | null {\n const [theme, setTheme] = React.useState<string | null>(() =>\n typeof document !== 'undefined' ? document.documentElement.getAttribute('data-theme') : null,\n )\n React.useEffect(() => {\n if (typeof document === 'undefined') return\n const root = document.documentElement\n const update = () => setTheme(root.getAttribute('data-theme'))\n update()\n const obs = new MutationObserver(update)\n obs.observe(root, { attributes: true, attributeFilter: ['data-theme'] })\n return () => obs.disconnect()\n }, [])\n return theme\n}\n\n// ── Component ──\n\nexport interface AvatarProps extends React.HTMLAttributes<HTMLDivElement> {\n /** 尺寸:number (px) 或 'fill'(填滿父容器,由父層決定大小)。預設 32 */\n size?: number | 'fill'\n /** 形狀:circle(人物)或 square(實體),預設 circle */\n shape?: 'circle' | 'square'\n /** 圖片 URL */\n src?: string\n /** 替代文字(圖片失敗時取首字作 fallback) */\n alt?: string\n /** Icon 模式(LucideIcon) */\n icon?: LucideIcon\n /** Icon / text fallback 的背景色,預設 neutral */\n color?: ColorKey\n /** 深底模式(step-6 背景 + on-emphasis 配對前景;亮色 hue yellow/amber/orange/lime 用深字 --on-emphasis-dark),預設 false */\n solid?: boolean\n /**\n * 在線狀態指示器(presence),顯示在 avatar **右下角**。\n * 世界級對照:Slack / Teams / Discord — `online` 是最廣泛被理解的術語。\n * 位置語義:右下 = \"此人的 presence\"(使用者聚焦於「這個人是誰 + 現在 在不在」)。\n */\n status?: 'online' | 'away' | 'busy' | 'offline'\n /**\n * 未讀 / 通知計數 badge,顯示在 avatar **右上角**。\n * 世界級對照:chat app(iMessage / Slack thread / LINE / WhatsApp)一律右上角。\n * 位置語義:右上 = \"關於此對話的新事件數量\"(使用者聚焦於「有多少未處理」);\n * 與右下的 presence 共存不衝突(不同角、不同語義)。\n * `> 99` 自動顯示 \"99+\"(交給內部 Badge 的 `max` 行為)。\n */\n badgeCount?: number\n /**\n * badgeCount overlay 的 aria-label override(i18n)。預設英文 `${badgeCount} unread`;\n * consumer 傳此 prop 本地化(對齊 `Notice` dismissAriaLabel pattern,a11y/i18n)。\n */\n badgeAriaLabel?: string\n /**\n * 傳入 HoverCard 內容(如 ProfileCard),hover avatar 時自動顯示。\n * 只有人員 avatar 需要傳;實體 avatar(專案、組織)不傳。\n */\n hoverCard?: React.ReactNode\n}\n\n// code-quality-allow: long-function — foundational composite main body — 拆 sub-fn 會複雜化 local state / ref / context binding\n// 2026-05-13 (a) perf fix part-2(per codex Layer C Roadmap rich-cell dominant + user 拍 Path (a)):\n// `React.memo` wrap forwardRef Avatar — Roadmap 13 columns 含 person/multiPerson,每 row 多 avatar\n// × HoverCard subtree + useDocumentTheme observer = 重渲染 hotspot。memo shallow-equal props,\n// HoverCard / themeRef stable across scroll 時 skip re-render。對齊 codex Profile Plan step 5\n// (filter Avatar/PeoplePicker/FieldSurfaceProvider remounts)。\n// code-quality-allow: long-function — size × shape × color × solid × status × badgeCount × hoverCard × img-fallback 多軸 prop 組合,拆 sub-fn 會跨 fn 傳 imgError state + isTableScrolling observer 結果\nconst AvatarInner = React.forwardRef<HTMLDivElement, AvatarProps>(\n ({ size = 32, shape = 'circle', src, alt, icon: Icon, color = 'neutral', solid = false, status, badgeCount, badgeAriaLabel, hoverCard, className, style, ...props }, ref) => {\n const [imgError, setImgError] = React.useState(false)\n // 2026-07-05 D4:imgError 隨 src 變更 reset — 原 state 首次 onError 後永久 true,consumer 之後\n // 換有效新 src(重傳頭像 / signed URL 刷新)時 showImage 永遠 false,<img> 不再掛載 → 新圖\n // 永不嘗試載入、終身卡 initials fallback。React 官方 adjust-state-on-prop-change 寫法\n //(render-time derived reset,較 useEffect 少一輪 render);對齊 Radix useImageLoadingStatus /\n // MUI useLoaded 皆隨 src reset 的 canonical。\n const [prevSrc, setPrevSrc] = React.useState(src)\n if (prevSrc !== src) {\n setPrevSrc(src)\n setImgError(false)\n }\n const documentTheme = useDocumentTheme()\n const isTableScrolling = useTableIsScrolling()\n // 2026-05-13 R3.5(per codex Q3 verdict + user 拍「想盡辦法 auto-handle prereq」):\n // Avatar self-dim when in disabled Field wrapper context(取代既有 wrapper opacity-disabled blanket\n // 逃生艙 — color.spec.md:729 specific-disabled-color canonical)。\n // Scope:`useResolvedFieldDisabled()`(= fieldCtx.disabled,涵蓋 <Field disabled> 與 <Field mode=\"disabled\">),標準 Field\n // 家族 wrapper disabled 時才 dim;**沒包在 Field wrapper 內的 standalone Avatar**(ProfileCard / FileItem /\n // HoverCard / Dialog 等 display 場景)**backward compat 不變**。對齊 avatar.spec.md「Avatar 在 disabled\n // 元件內 host-controlled opacity」canonical — 升級成「Avatar self-managed via fieldCtx」。\n // 2026-06-08 SSOT:欄位內 Avatar 跟隨 <Field disabled>/<Field mode=\"disabled\"> 變淡(fieldCtx-scoped,cell 無 fieldCtx → 不影響)\n const isDisabledInField = useResolvedFieldDisabled()\n const isFill = size === 'fill'\n // Fill 模式下 icon 用 60% 寬高、text 用 50cqi(container query inline-size);\n // 數字模式下用既有 px 計算\n const numSize = isFill ? 32 : (size as number)\n const iconPx = getIconSize(numSize)\n const fontSizePx = Math.round(numSize * 0.5)\n const variantKey: VariantKey = solid ? 'solid' : 'subtle'\n const colors = COLOR_MAP[variantKey]?.[color] ?? COLOR_MAP.subtle.neutral\n // 2026-07-14 dim-20 修:inline style 不能用 rounded-* class,但可消費 radius token\n // (radius.spec.md:18 canonical「所有 4px 圓角一律用 rounded-md」+ :86 禁硬寫值;\n // token 改值 Avatar 跟隨 — M17 SSOT 傳播)。\n const radius = shape === 'circle' ? 'var(--radius-full)' : 'var(--radius-md)'\n\n // 決定內容\n const showImage = src && !imgError\n const showIcon = !showImage && (Icon || (!alt))\n const showText = !showImage && !showIcon && alt\n\n const FallbackIcon = Icon ?? User\n\n // Status dot 尺寸:avatar 的 28%(Slack / Teams / Discord 世界級平均),\n // clamp [8, 16] — floor 8 保小 avatar 仍可辨識但不喧賓奪主(10 floor 會讓 24px\n // avatar 的 dot 占 42% 太大);ceiling 16 防大 avatar dot 過度放大\n const dotSize = isFill ? 10 : Math.max(8, Math.min(16, Math.round(numSize * 0.28)))\n // Border ring 在 surface 上分離 dot 與 avatar,dotSize ≥ 12 時升階到 3px 保持視覺比例\n const dotBorder = dotSize >= 12 ? 3 : 2\n\n const avatarEl = (\n <div\n className={cn(\n 'inline-flex items-center justify-center shrink-0 overflow-hidden select-none',\n isFill && 'w-full h-full',\n // 2026-05-13 R3.5 self-dim:Avatar 在 disabled Field wrapper context 內自 dim\n // (取代 field-wrapper.tsx default/bare/naked disabled blanket opacity-disabled 逃生艙)\n isDisabledInField && 'opacity-disabled',\n )}\n style={{\n ...(isFill\n ? { containerType: 'inline-size' as React.CSSProperties['containerType'] }\n : { width: numSize, height: numSize }),\n borderRadius: radius,\n backgroundColor: showImage ? undefined : colors.bg,\n color: showImage ? undefined : colors.text,\n }}\n data-avatar-size={isFill ? 'fill' : numSize}\n role={!showImage && alt && !hoverCard ? 'img' : undefined}\n aria-label={!showImage && alt && !hoverCard ? alt : undefined}\n >\n {showImage && (\n <img\n src={src}\n alt={alt ?? ''}\n className=\"w-full h-full object-cover\"\n onError={() => setImgError(true)}\n />\n )}\n {showIcon && (\n isFill\n ? <FallbackIcon className=\"w-[60%] h-[60%]\" aria-hidden />\n : <FallbackIcon size={iconPx} aria-hidden />\n )}\n {showText && (\n <span\n className=\"font-medium leading-none\"\n style={{ fontSize: isFill ? '50cqi' : fontSizePx }}\n aria-hidden\n >\n {getInitial(alt!)}\n </span>\n )}\n </div>\n )\n\n const hasOverlay = status || typeof badgeCount === 'number'\n // Keyboard access canonical(D4 2026-04-22 finding;2026-07-06 user 拍板修正 ARIA 空承諾):\n // Avatar with `hoverCard` 需 keyboard 可達 — Radix `HoverCardTrigger asChild` 不自動加\n // tabIndex,non-focusable `<div>` 會讓 keyboard-only user 無法 reach popover(WCAG 2.1.1)。\n // 解:`tabIndex=0` + `role=\"img\"` + `aria-label` + focus ring(focus 時 Radix 自動開卡)。\n // **不掛 `role=\"button\"` / `aria-haspopup`**(2026-07-06 拆除):原本宣告了卻沒有\n // Enter/Space 行為、沒有 aria-expanded = 對 AT 的空承諾;對齊 Radix hover-card 官方\n // (刻意零 popup ARIA,hover card 定位 sighted-user 輔助)+ GitHub hovercard(trigger\n // 純連結無 popup ARIA)。若無 hoverCard 則維持純展示 `<div>`。\n // **`role=\"img\"`(2026-07-07 CI axe 補修)**:aria-label 在無 role 的 generic `<div>` 上\n // 是 ARIA 禁用組合(axe aria-prohibited-attr,serious;2026-07-06 拆 role=\"button\" 後裸露)。\n // Avatar 語義本質 = 身份圖像 → `role=\"img\"` 讓 aria-label 合法、子孫自動 presentational、\n // 且不對 AT 承諾任何互動行為 — 與「拆假承諾」拍板意圖一致(emoji span / MUI Avatar img alt 同款語義)。\n // a11y:role=\"img\" 讓子孫 presentational → 內層 Badge 的 count 語義被壓平,SR 讀不到。\n // 解:badgeCount 併入外層 aria-label(對齊 Slack「單一 accessible name 含未讀數」做法)。\n const focusableProps = hoverCard\n ? {\n tabIndex: 0,\n role: 'img',\n 'aria-label':\n typeof badgeCount === 'number' && badgeCount > 0\n ? `${alt ?? 'View profile'}, ${badgeAriaLabel ?? `${badgeCount} unread`}`\n : (alt ?? 'View profile'),\n }\n : {}\n // 2026-05-31:focus ring 圓角跟隨 shape(circle→rounded-full / square→rounded-md,對齊 body 的 `radius` 變數 var(--radius-full)/var(--radius-md)),\n // 原寫死 rounded-full 會讓方形 avatar(實體)配 hoverCard 時出現圓形 ring。hoverCard 為通用行為(任意內容),\n // 方形 avatar 合法可配(內容非 ProfileCard 而已),故 ring 必跟形狀。\n const focusableClass = hoverCard\n ? cn('', shape === 'circle' ? 'rounded-full' : 'rounded-md')\n : ''\n // 外框 = 可見圓(avatar.spec.md「外框 = 可見圓」):固定尺寸模式下最外層寬高必須等於 size,\n // 不得被父容器沿交叉軸拉伸。尺寸寫在內層(下方 numSize),root 只有 inline-flex shrink-0 ——\n // 而 shrink-0 只擋主軸收縮,擋不住交叉軸拉伸(直向 flex 拉寬;grid 由 justify-self 管,align-self 也碰不到),\n // 所以要鎖寬只能用 w-fit。root 一被撐寬會同時壞三處:狀態圓點 / badgeCount(相對 root 絕對定位)、\n // :focus-visible 焦點框(畫在 root 的 border box,帶 rounded-full → 變膠囊)、hoverCard 的 Radix 錨點(浮層橫向偏移)。\n // fill 模式的語意就是填滿父容器,必須排除。機械閘:scripts/avatar-anchor-box-invariant.mjs。\n const boxClass = isFill ? '' : 'w-fit'\n const baseEl = !hasOverlay\n ? <div ref={ref} className={cn('inline-flex shrink-0', boxClass, focusableClass, className)} style={style} {...focusableProps} {...props}>{avatarEl}</div>\n : (\n <div ref={ref} className={cn('relative inline-flex shrink-0', boxClass, focusableClass, className)} style={style} {...focusableProps} {...props}>\n {avatarEl}\n {/* Status dot:bottom-right(presence — 世界級對照 Slack / Teams / Discord),\n 落在 circle avatar 圓周 45° 位置 / square avatar 右下直角;\n border ring 用 surface 色讓 dot 從 avatar 邊界視覺分離。\n a11y:`aria-hidden` — presence 資訊整合到 parent avatar 的 aria-label\n (world-class Slack 做法),避免多 `role=\"status\"` 造成 screen reader 洪水 */}\n {status && (\n <span\n className=\"absolute block rounded-full\"\n style={{\n width: dotSize,\n height: dotSize,\n bottom: 0,\n right: 0,\n backgroundColor: STATUS_DOT_COLOR[status],\n boxShadow: `0 0 0 ${dotBorder}px var(--surface-raised, var(--canvas))`,\n }}\n aria-hidden\n />\n )}\n {/* Count badge:top-right(chat 未讀 / 通知計數 — 世界級對照 iMessage /\n Slack thread / LINE / WhatsApp)。消費 DS Badge(critical variant),\n 再加 ring 與 avatar 分離 */}\n {typeof badgeCount === 'number' && badgeCount > 0 && (\n <Badge\n variant=\"critical\"\n count={badgeCount}\n max={99}\n className=\"absolute -top-1 -right-1\"\n style={{\n boxShadow: `0 0 0 2px var(--surface-raised, var(--canvas))`,\n }}\n aria-label={badgeAriaLabel ?? `${badgeCount} unread`} // i18n-allow: DS default 英文(consumer 可傳 badgeAriaLabel i18n)\n />\n )}\n </div>\n )\n\n // 2026-05-13 (c) scroll-defer perf(per user 拍 Path (c) + codex Q3 verdict):\n // DataTable scrolling 期間跳 HoverCard wrapper(Portal + useDocumentTheme observer 是\n // Roadmap 重渲 hotspot,per codex Layer C 分析)。scroll 結束 → context flips false →\n // re-render 接回完整 HoverCard tree(ProfileCard 仍可 hover 顯示)。\n // 對齊 AG Grid `deferRender` for slow React cell components / MUI X DataGrid scroll-defer。\n if (!hoverCard || isTableScrolling) return baseEl\n\n return (\n <HoverCard openDelay={MOTION_DELAY_RICH_MS} closeDelay={MOTION_DELAY_CLOSE_MS}>\n <HoverCardTrigger asChild>\n {baseEl}\n </HoverCardTrigger>\n {/* HoverCardContent canonical(2026-04-23):\n - 無 inner padding(consumer ProfileCard 自帶 `px-4 py-3` chrome)\n - `overflow-hidden` + `rounded-lg` → child(ProfileCard)圓角裁切\n - **不設 max-height**:ProfileCard 自己消費 `--radix-hover-card-content-available-height`\n 自約束高度 + 內部 ScrollArea 處理捲動\n - `data-theme={documentTheme}`:ProfileCard 永遠跟隨 **app-level theme**(從 `<html data-theme>`\n 動態讀),不受 trigger subtree theme 污染。範例:Avatar 位於 OverflowIndicator 的 dark\n tooltip 內,其 Portal 會繼承該 subtree dark theme → ProfileCard 變全黑。顯式設回 app theme\n 確保 ProfileCard 永遠 light-in-light-app / dark-in-dark-app。 */}\n <HoverCardContent\n data-theme={documentTheme ?? undefined}\n className=\"bg-surface-raised rounded-lg border border-border overflow-hidden\"\n style={{ boxShadow: 'var(--elevation-200)' }}\n >\n {hoverCard}\n </HoverCardContent>\n </HoverCard>\n )\n }\n)\n\n// ── AvatarData ─────────────────────────────────────────────────────────────\n// 資料型別,讓 consumer 傳資料而非 ReactNode。\n// 接收端內部用 Avatar 元件渲染,統一控制尺寸與 fallback。\n\nexport interface AvatarData {\n /** 圖片 URL */\n src?: string\n /** 替代文字(圖片失敗時取首字作 fallback) */\n alt: string\n /** Icon / text fallback 的背景色,預設 neutral */\n color?: ColorKey\n /**\n * Person avatar hover ProfileCard(DS-wide canonical,person avatar 預設必有,見 avatar.spec.md)。\n * Entity avatar(專案 / 組織 logo)不帶 → consumer 不傳 hoverCard 即豁免。\n * 消費 AvatarData 的 row primitive(MenuItem menu-item.tsx / SelectionItem selection-item.tsx,\n * DropdownMenu / SelectMenu 經 MenuItem)需 forward 此 prop 到內部 <Avatar hoverCard={...} />。\n * 例外:ProfileCard 內部 Avatar 不 forward(卡片本身就是 hover 目的地,nested hover-in-hover 不合理)。\n */\n hoverCard?: React.ReactNode\n}\n\n// Story auto-compile metadata — Phase 1 mechanical migration(2026-04-24)\n// Phase 2 fill needed: purpose descriptions + when rationale + world-class refs\nexport const avatarMeta = {\n component: 'Avatar',\n family: null, // non-family composite / overlay / layout\n // 2026-07-04 audit 標記:Avatar 無 variant 軸、size 為任意 number(px,由 consumer context 決定\n // — item-anatomy AVATAR_SIZE 查表 / ProfileCard 40 等),無 discrete tier → 兩欄留空為 intentional 非漏填\n variants: {},\n sizes: {},\n states: ['default', 'focus-visible', 'disabled'], // 2026-06-11 R2:本身無自有 hover/active(spec「StateBehavior 範疇」段;hoverCard 互動屬 HoverCard),\n tokens: {\n bg: ['bg-surface-raised'],\n fg: ['--foreground', '--on-emphasis'],\n ring: ['ring-ring'],\n },\n} as const\n\nAvatarInner.displayName = 'Avatar'\nconst Avatar = React.memo(AvatarInner)\n\nexport { Avatar }\n"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"avatar.js","sources":["../../../src/components/Avatar/avatar.tsx"],"sourcesContent":["// @benchmark-unverified-blanket: file-level retraction per M22 (d) — claims herein not individually URL-cited; treat as unverified visual/usage rumor unless retrofit per-claim. Hook escape preserved.\nimport * as React from 'react'\nimport { User } from 'lucide-react'\nimport type { LucideIcon } from 'lucide-react'\nimport { cn } from '@/lib/utils'\nimport { CAT_SUBTLE_TOKENS, CAT_SOLID_TOKENS, type CategoricalColor } from '@/design-system/tokens/categorical-color'\nimport { HoverCard, HoverCardTrigger, HoverCardContent } from '@/design-system/components/HoverCard/hover-card'\nimport { MOTION_DELAY_RICH_MS, MOTION_DELAY_CLOSE_MS } from '@/design-system/tokens/motion/motion'\nimport { Badge } from '@/design-system/components/Badge/badge'\nimport { useResolvedFieldDisabled, useTableIsScrolling } from '@/design-system/components/Field/field-context'\nimport { STACK_GAP_PX } from '@/design-system/tokens/uiSize/stack-gap'\n\n/**\n * Avatar — 頭像元件\n *\n * 三種內容模式(按優先順序):\n * 1. src → 圖片\n * 2. icon → icon 在底色圓/方形內\n * 3. alt → 取首字作文字 fallback\n * 4. 都沒有 → 預設 User icon\n *\n * ── 尺寸 ──\n * size 接受任意 px 值,icon 自動 = round_even(size × 0.6)\n * 文字 fallback 字體 = size × 0.5\n *\n * ── 形狀 ──\n * circle(預設)→ rounded-full,用於人物\n * square → rounded-md (4px),用於實體(專案、組織、App)\n */\n\n// ── 色彩 ──\n// **消費 categorical-color SSOT**(CAT_SUBTLE_TOKENS / CAT_SOLID_TOKENS,key X 一律對 `--color-X-*`,\n// 1:1 零 offset)。subtle=step-1 底 + step-7 字;solid=step-6 全色底 + on-emphasis 字\n//(亮 hue yellow/amber/orange/lime 用 --on-emphasis-dark 深字;green 白字例外)。neutral 非色相,自處理(subtle=muted、solid=neutral-9)。\n// 2026-06-04 修:原 `red` 誤接 `--color-deep-orange-*`(red=品牌紅 hue-25 ≠ deep-orange);\n// 改消費 SSOT 後 red→`--color-red-*`,並補齊全 12 色相。\ntype ColorKey = CategoricalColor\ntype VariantKey = 'subtle' | 'solid'\n\nconst COLOR_MAP: Record<VariantKey, Record<ColorKey, { bg: string; text: string }>> = {\n subtle: {\n neutral: { bg: 'var(--muted)', text: 'var(--foreground)' },\n ...CAT_SUBTLE_TOKENS,\n },\n solid: {\n neutral: { bg: 'var(--color-neutral-9)', text: 'var(--inverse-fg)' },\n ...CAT_SOLID_TOKENS,\n },\n}\n\n// ── Icon size: round to nearest even, ≈ 60% ──\nfunction getIconSize(avatarSize: number): number {\n return Math.round((avatarSize * 0.6) / 2) * 2\n}\n\n// ── Text fallback: first character ──\nfunction getInitial(text: string): string {\n return text.trim().charAt(0).toUpperCase()\n}\n\n// Semantic presence tokens — 見 color/semantic.css\n// Module-level constant(2026-04-22 D3 perf audit):從 render body 移到 module scope,\n// 避免每次 Avatar render 都 re-declare 此 4-entry object(Low impact 但渲染大量 avatars 時累積可觀)\nconst STATUS_DOT_COLOR: Record<string, string> = {\n online: 'var(--status-online)',\n away: 'var(--status-away)',\n busy: 'var(--status-busy)',\n offline: 'var(--status-offline)',\n}\n\n// ── useDocumentTheme(2026-04-23;M3 Portal 逃脫防線,scope verified 2026-04-25)──\n// 讀 `<html data-theme>` 並 observe mutation。用於 Avatar hoverCard ProfileCard:\n// Portal 後的 HoverCardContent 會繼承 trigger subtree theme(如 OverflowIndicator\n// dark tooltip 內部),造成 ProfileCard 被污染成 dark。顯式 bind app-level theme\n// 確保 ProfileCard 永遠跟 app 本身 theme 一致(light-in-light-app / dark-in-dark-app)。\n//\n// 範圍 audit 2026-04-25:觀察對象是 `document.documentElement` 自有 DOM,非 3rd-party\n// lib 內部(不屬 M2 scope);attributeFilter 限定 `data-theme` 單一 attr,re-render 成本\n// 為每次全站 theme 切換 × Avatar 數量,可接受。\nfunction useDocumentTheme(): string | null {\n const [theme, setTheme] = React.useState<string | null>(() =>\n typeof document !== 'undefined' ? document.documentElement.getAttribute('data-theme') : null,\n )\n React.useEffect(() => {\n if (typeof document === 'undefined') return\n const root = document.documentElement\n const update = () => setTheme(root.getAttribute('data-theme'))\n update()\n const obs = new MutationObserver(update)\n obs.observe(root, { attributes: true, attributeFilter: ['data-theme'] })\n return () => obs.disconnect()\n }, [])\n return theme\n}\n\n// ── 頭像堆疊(avatar stack)—— 疊在一起時怎麼分開(SSOT,2026-09-26 採用 Primer 的挖空做法,待辦總帳 L3 / R21)──\n//\n// 規格:avatar.spec.md「頭像堆疊(疊在一起時)」段。這裡只放機制,不重述理由。\n//\n// **挖空,不是外圈**:後面那顆被前面那顆蓋住的地方,連同外面一圈縫,整片從畫面上拿掉。\n// 縫裡露出的是**真正在後面的東西**(畫布 / 唯讀欄位 / 滑過的表格列 / 深色欄位),所以:\n// - 半透明的底色(neutral `--muted`、深色主題的彩色淡底、+N)不會再疊在別人身上透出下面那顆;\n// - 不必猜「後面是什麼顏色」—— 舊做法 `ring-2 ring-[var(--surface)]` 寫死 `--surface`,\n// 深色主題它自己是半透明(變成一道亮月牙),唯讀欄位 / 滑過列上又變成一圈白邊。\n// 一手出處:GitHub Primer AvatarStack(@primer/react 38.40.0,`AvatarStack.module.css` L168-L194:\n// 第 2 顆起 `mask-composite: exclude` + 圓形 `radial-gradient` 遮罩,挖掉「前一顆的圓 + `--avatar-border-width`」)\n// https://github.com/primer/react/blob/%40primer/react%4038.40.0/packages/react/src/AvatarStack/AvatarStack.module.css#L168-L194\n// 本 DS 用**單層** radial-gradient 表達同一個幾何(圓內透明、圓外保留),不需要 mask-composite。\n//\n// **結構契約**(每一處堆疊都照這個長相;+N 算最後一項,同尺寸):\n// 堆疊列(AVATAR_STACK_CLASS)> 每一項(item,兄弟節點;AVATAR_STACK_ITEM_CLASS + avatarStackItemStyle,第一項最上層)\n// > 擁有者(Avatar `stacked` 的最外層 / OverflowIndicator circle 的觸發點,帶 `data-avatar-stack`)> 圓(被挖的那一層)\n// - 挖空只掛在「圓」那一層,**不掛在擁有者上**:擁有者身上畫的是全域 `:focus-visible` 外描邊,\n// 遮罩會把畫在 border box 外面的焦點框整圈裁掉(CSS Masking:mask 以外的區域不繪製)。\n// - 何時挖由結構決定:所在那一項**前面有一個看得見的堆疊項目**才挖 —— 第一項不挖;\n// Combobox 量寬時被 `hidden` 掉的項目不算。所以 PersonAvatarTag(拿不到自己是第幾顆)也能正確。\n//\n// 以下匯出都標 `@internal`:DS 內部的堆疊(MultiPersonDisplay / PersonAvatarTag / OverflowIndicator / story)用,\n// 不進 root barrel;consumer 要顯示多人時用 PeoplePicker 檢視模式,自組時經 subpath 包裝後使用\n// (ui-development.md「符號級 @internal 排除」)。未來 Avatar.Group 會把這段收進元件(overflow-indicator.spec.md)。\n/**\n * @internal\n * 第 2 項起往左疊的量。CSS 雙生:`tokens/uiSize/uiSize.css` `--avatar-stack-overlap`(class 字串裡用它;\n * 這個數字給量寬與遮罩幾何用),兩邊相等由 scripts/token-twin-invariant.mjs 鎖住。\n */\nexport const AVATAR_STACK_OVERLAP_PX = 2\n/**\n * @internal\n * 縫寬 = DS 通用的「疊在一起的兩樣東西」之間的縫(`tokens/uiSize/stack-gap.ts`;全域焦點框的 outline-offset、\n * 狀態圓點 / 計數徽章 / 移除 × 的挖空縫、上傳列進度條的縫、步驟條外圈的縫都是它)。\n * 2026-04-02 起舊外圈的寬度(`ring-2`)也是 2px,白底上看起來與舊版相同。\n * Primer 是 1px(`--avatar-border-width: 1px`,`AvatarStack.module.css` L3)——選 2px 是 AI 推導,理由見 avatar.spec.md「頭像堆疊」段。\n */\nexport const AVATAR_STACK_GAP_PX = STACK_GAP_PX\n\n// ── 疊在頭像上的東西(狀態圓點 / 計數徽章 / 移除 ×)—— 同樣挖空,不畫外圈(2026-09-26,待辦總帳 N49)──\n//\n// 舊做法是在圓點 / 徽章上畫一圈 `box-shadow` 當「縫」,顏色寫死 `var(--surface-raised, var(--canvas))`:\n// 淺色兩者都是白、看不出問題;深色 `--surface-raised` 是不透明的 #1F1F1F,頭像放在頁面(#0A0A0A)、半透明卡片、\n// 滑過的列上時,那一圈都是一道跟底不同色的灰邊(實測 2026-09-26,步驟條外圈同一種病、當天已改)。\n// 挖空 = 把圓點 / 徽章底下連同外面一圈縫從「圓」那一層拿掉,露出真正在後面的東西 —— 與上方頭像堆疊同一個原則。\n//\n// 做法:圓那一層的 `mask-image` 疊多層,每一層一個形狀,用 `mask-composite` 合成:\n// - 「留下圓外」層(狀態圓點、移除 ×):圓內透明、圓外保留 → 與下面的結果取交集(intersect);\n// - 徽章是膠囊,不能用一個漸層畫:先把「左圓 ∪ 右圓 ∪ 中間矩形」畫成洞(add),再從整張(subtract)扣掉;\n// 矩形 = 直帶 ∩ 橫帶(兩個 linear-gradient 取交集)。\n// 合成順序由下往上(CSS Masking 1 §「mask-composite」:每一層與它下面所有層的合成結果做運算)。\n// 堆疊的挖空(`--avatar-stack-cutout`)另由結構選擇器決定要不要掛,所以分成兩個 class 字串:\n// 只有自己的挖空 / 堆疊 + 自己的挖空(後者選擇器命中時蓋過前者)。\n/** @internal 移除 × 的幾何(person-display.tsx AvatarDismissOverlay 用同一組畫 ×,這裡用來挖它底下的洞)。 */\nexport const AVATAR_DISMISS_OVERLAY_PX = { top: -1, right: -4, size: 12 } as const\n/**\n * @internal 自己的挖空(狀態圓點 / 徽章 / 移除 ×)的 class。移除 × 的洞平常半徑為 0(看不出來),\n * 標籤被滑過或焦點在標籤內時(`group/avatar`,與 × 本身顯示的條件相同)才打開 —— 洞的半徑經\n * `--avatar-dismiss-on`(0 / 1)乘進去,半徑本身寫在 style(`--avatar-dismiss-r`),class 裡不寫數字。\n */\nexport const AVATAR_OWN_CUTOUT_CLASS =\n '[mask-image:var(--avatar-own-cutout)] [mask-composite:var(--avatar-own-composite)] [--avatar-dismiss-on:0] group-hover/avatar:[--avatar-dismiss-on:1] group-focus-within/avatar:[--avatar-dismiss-on:1]'\n/** @internal 堆疊 + 自己的挖空(結構選擇器同 `AVATAR_STACK_CUTOUT_CLASS`)。 */\nexport const AVATAR_STACK_OWN_CUTOUT_CLASS =\n '[:has(>[data-avatar-stack]):not([hidden])~*>[data-avatar-stack]>&]:[mask-image:var(--avatar-stack-cutout),var(--avatar-own-cutout)] [:has(>[data-avatar-stack]):not([hidden])~*>[data-avatar-stack]>&]:[mask-composite:intersect,var(--avatar-own-composite)]'\n\n/** 圓外保留(圓內透明);±0.25px 過渡 = 邊緣反鋸齒,不改變縫寬。 */\nconst keepOutsideCircle = (cx: string, cy: string, r: string) =>\n `radial-gradient(circle at ${cx} ${cy}, transparent calc(${r} - 0.25px), #000 calc(${r} + 0.25px))`\n/** 圓內為洞(圓內不透明、圓外透明);給 subtract 用。 */\nconst holeCircle = (cx: string, cy: string, r: string) =>\n `radial-gradient(circle at ${cx} ${cy}, #000 calc(${r} - 0.25px), transparent calc(${r} + 0.25px))`\n\n/**\n * @internal\n * 算出自己的挖空遮罩。所有座標相對於「圓」那一層(與最外層同大),用 `100%` 表示右 / 下緣,fill 模式也成立。\n * - 狀態圓點:貼右下角,直徑 dotSize,縫 dotGap。\n * - 移除 ×:`AVATAR_DISMISS_OVERLAY_PX`(右上,凸出 4px),縫 = 通用縫;半徑乘 `--avatar-dismiss-on`。\n * - 計數徽章:`-top-1 -right-1`(各凸出 4px)的膠囊,寬高由 ResizeObserver 量(字數不同寬度不同),縫 = 通用縫。\n * 回 `null` = 沒有任何東西疊在上面,不掛遮罩(照舊)。\n */\nexport function avatarOverlayCutout({ dot, dismiss, badge }: {\n dot?: { sizePx: number; gapPx: number }\n dismiss?: boolean\n badge?: { widthPx: number; heightPx: number } | null\n}): { image: string; composite: string } | null {\n const keeps: string[] = []\n if (dot) {\n const c = `calc(100% - ${dot.sizePx / 2}px)`\n keeps.push(keepOutsideCircle(c, c, `${dot.sizePx / 2 + dot.gapPx}px`))\n }\n if (dismiss) {\n const { top, right, size } = AVATAR_DISMISS_OVERLAY_PX\n keeps.push(keepOutsideCircle(`calc(100% - ${right}px - ${size / 2}px)`, `${top + size / 2}px`, 'calc(var(--avatar-dismiss-r) * var(--avatar-dismiss-on))'))\n }\n const layers = [...keeps]\n const ops = keeps.map(() => 'intersect')\n if (badge && badge.widthPx > 0 && badge.heightPx > 0) {\n const { widthPx: w, heightPx: h } = badge\n const top = -4, right = -4, g = STACK_GAP_PX // `-top-1 -right-1` = 各凸出 4px(Tailwind spacing 1 = 4px)\n const cy = `${top + h / 2}px`, r = `${h / 2 + g}px`\n const xL = `calc(100% - ${right}px - ${w}px + ${h / 2}px)`, xR = `calc(100% - ${right}px - ${h / 2}px)`\n const full = 'linear-gradient(#000 0 0)'\n const left = holeCircle(xL, cy, r), rightC = holeCircle(xR, cy, r)\n const bandX = `linear-gradient(to right, transparent ${xL}, #000 ${xL}, #000 ${xR}, transparent ${xR})`\n const bandY = `linear-gradient(to bottom, transparent ${top - g}px, #000 ${top - g}px, #000 ${top + h + g}px, transparent ${top + h + g}px)`\n layers.push(full, left, rightC, bandX, bandY)\n ops.push('subtract', 'add', 'add', 'intersect')\n }\n if (layers.length === 0) return null\n return { image: layers.join(', '), composite: ops.length ? ops.join(', ') : 'add' }\n}\n\n/**\n * @internal\n * 挖空遮罩:把「左邊那顆(同尺寸的圓)+ 一圈縫」從這一層挖掉。sizePx = 堆疊裡每顆的直徑。\n */\nexport function avatarStackCutoutImage(sizePx: number): string {\n const edge = sizePx / 2 + AVATAR_STACK_GAP_PX\n const centerX = AVATAR_STACK_OVERLAP_PX - sizePx / 2\n // ±0.25px 的過渡 = 邊緣反鋸齒(DPR 2 下 1 個裝置像素),不改變縫寬\n return `radial-gradient(circle at ${centerX}px 50%, transparent ${edge - 0.25}px, #000 ${edge + 0.25}px)`\n}\n\n/**\n * @internal\n * 被挖那一層(圓)的 className + style。className 是字面值(Tailwind 要能掃到),\n * 幾何只從上方兩個常數來(經 CSS 變數 `--avatar-stack-cutout` 傳入),不在 class 字串裡重寫數字。\n * `-webkit-mask-image` 由 Tailwind v4 的 Lightning CSS 自動補(建置產物實測兩個屬性都在)。\n */\nexport const AVATAR_STACK_CUTOUT_CLASS =\n '[:has(>[data-avatar-stack]):not([hidden])~*>[data-avatar-stack]>&]:[mask-image:var(--avatar-stack-cutout)]'\n/** @internal 被挖那一層的 style(`--avatar-stack-cutout`),搭配 `AVATAR_STACK_CUTOUT_CLASS`。 */\nexport function avatarStackCutoutStyle(sizePx: number): React.CSSProperties {\n return { ['--avatar-stack-cutout' as string]: avatarStackCutoutImage(sizePx) }\n}\n\n/**\n * @internal\n * 堆疊列(每一項的父層)。`isolate` = 疊放順序只在這一列裡比,不跟頁面上其他層(黏住的表頭等)比;\n * 對齊 Primer `.AvatarStack { isolation: isolate }`(`AvatarStack.module.css` L12)。\n */\nexport const AVATAR_STACK_CLASS = 'isolate'\n/**\n * @internal\n * 每一項(item)的外層 className + style(`avatarStackItemStyle`):\n * - 第 2 項起往左疊 `AVATAR_STACK_OVERLAP_PX`;\n * - 越前面越上層(第一顆在最上面,+N 在最下面)—— 重疊那 2px 滑過時開的是看得到的那顆(上面那顆)的名片;\n * - 裡面有東西拿到鍵盤焦點(`:focus-visible`)時整項升到最上層:焦點框往外畫(focus-canonical.md「疊層/徽章不算鄰居」),\n * 不升的話左半圈會被上面那顆蓋住。對齊 SegmentedControl `focus-visible:z-20`「讓往外長的框壓在相鄰 item 之上」。\n * count = 項目總數(含 +N)。\n */\nexport const AVATAR_STACK_ITEM_CLASS =\n 'relative inline-flex z-[var(--avatar-stack-z)] has-[:focus-visible]:z-[var(--avatar-stack-z-focus)]'\n/** @internal 每一項的 style,搭配 `AVATAR_STACK_ITEM_CLASS`(說明見上)。 */\nexport function avatarStackItemStyle(index: number, count: number): React.CSSProperties {\n return {\n marginLeft: index > 0 ? -AVATAR_STACK_OVERLAP_PX : undefined,\n ['--avatar-stack-z' as string]: count - index,\n ['--avatar-stack-z-focus' as string]: count + 1,\n }\n}\n\n// ── Component ──\n\nexport interface AvatarProps extends React.HTMLAttributes<HTMLDivElement> {\n /** 尺寸:number (px) 或 'fill'(填滿父容器,由父層決定大小)。預設 32 */\n size?: number | 'fill'\n /** 形狀:circle(人物)或 square(實體),預設 circle */\n shape?: 'circle' | 'square'\n /** 圖片 URL */\n src?: string\n /** 替代文字(圖片失敗時取首字作 fallback) */\n alt?: string\n /** Icon 模式(LucideIcon) */\n icon?: LucideIcon\n /** Icon / text fallback 的背景色,預設 neutral */\n color?: ColorKey\n /** 深底模式(step-6 背景 + on-emphasis 配對前景;亮色 hue yellow/amber/orange/lime 用深字 --on-emphasis-dark),預設 false */\n solid?: boolean\n /**\n * 在線狀態指示器(presence),顯示在 avatar **右下角**。\n * 世界級對照:Slack / Teams / Discord — `online` 是最廣泛被理解的術語。\n * 位置語義:右下 = \"此人的 presence\"(使用者聚焦於「這個人是誰 + 現在 在不在」)。\n */\n status?: 'online' | 'away' | 'busy' | 'offline'\n /**\n * 未讀 / 通知計數 badge,顯示在 avatar **右上角**。\n * 世界級對照:chat app(iMessage / Slack thread / LINE / WhatsApp)一律右上角。\n * 位置語義:右上 = \"關於此對話的新事件數量\"(使用者聚焦於「有多少未處理」);\n * 與右下的 presence 共存不衝突(不同角、不同語義)。\n * `> 99` 自動顯示 \"99+\"(交給內部 Badge 的 `max` 行為)。\n */\n badgeCount?: number\n /**\n * badgeCount overlay 的 aria-label override(i18n)。預設英文 `${badgeCount} unread`;\n * consumer 傳此 prop 本地化(對齊 `Notice` dismissAriaLabel pattern,a11y/i18n)。\n */\n badgeAriaLabel?: string\n /**\n * 傳入 HoverCard 內容(如 ProfileCard),hover avatar 時自動顯示。\n * 只有人員 avatar 需要傳;實體 avatar(專案、組織)不傳。\n */\n hoverCard?: React.ReactNode\n /**\n * 這顆頭像是頭像堆疊裡的一項(avatar.spec.md「頭像堆疊(疊在一起時)」)。\n * true → 最外層帶 `data-avatar-stack`,圓那一層被左邊那顆蓋住的地方 + 2px 縫整片挖掉;\n * 第一顆(前面沒有看得見的堆疊項目)不挖。只在固定尺寸(`size` 為數字)時生效。\n */\n stacked?: boolean\n /**\n * @internal\n * 編輯態人員標籤的移除 ×(person-display.tsx AvatarDismissOverlay)浮在這顆頭像右上;\n * 標籤被滑過 / 焦點在標籤內時(`group/avatar`,與 × 顯示的條件相同)把 × 底下連縫從圓上挖掉。\n */\n dismissCutout?: boolean\n}\n\n// code-quality-allow: long-function — foundational composite main body — 拆 sub-fn 會複雜化 local state / ref / context binding\n// 2026-05-13 (a) perf fix part-2(per codex Layer C Roadmap rich-cell dominant + user 拍 Path (a)):\n// `React.memo` wrap forwardRef Avatar — Roadmap 13 columns 含 person/multiPerson,每 row 多 avatar\n// × HoverCard subtree + useDocumentTheme observer = 重渲染 hotspot。memo shallow-equal props,\n// HoverCard / themeRef stable across scroll 時 skip re-render。對齊 codex Profile Plan step 5\n// (filter Avatar/PeoplePicker/FieldSurfaceProvider remounts)。\n// code-quality-allow: long-function — size × shape × color × solid × status × badgeCount × hoverCard × img-fallback 多軸 prop 組合,拆 sub-fn 會跨 fn 傳 imgError state + isTableScrolling observer 結果\nconst AvatarInner = React.forwardRef<HTMLDivElement, AvatarProps>(\n ({ size = 32, shape = 'circle', src, alt, icon: Icon, color = 'neutral', solid = false, status, badgeCount, badgeAriaLabel, hoverCard, stacked = false, dismissCutout = false, className, style, ...props }, ref) => {\n const [imgError, setImgError] = React.useState(false)\n // 2026-07-05 D4:imgError 隨 src 變更 reset — 原 state 首次 onError 後永久 true,consumer 之後\n // 換有效新 src(重傳頭像 / signed URL 刷新)時 showImage 永遠 false,<img> 不再掛載 → 新圖\n // 永不嘗試載入、終身卡 initials fallback。React 官方 adjust-state-on-prop-change 寫法\n //(render-time derived reset,較 useEffect 少一輪 render);對齊 Radix useImageLoadingStatus /\n // MUI useLoaded 皆隨 src reset 的 canonical。\n const [prevSrc, setPrevSrc] = React.useState(src)\n if (prevSrc !== src) {\n setPrevSrc(src)\n setImgError(false)\n }\n const documentTheme = useDocumentTheme()\n const isTableScrolling = useTableIsScrolling()\n // 2026-05-13 R3.5(per codex Q3 verdict + user 拍「想盡辦法 auto-handle prereq」):\n // Avatar self-dim when in disabled Field wrapper context(取代既有 wrapper opacity-disabled blanket\n // 逃生艙 — color.spec.md:729 specific-disabled-color canonical)。\n // Scope:`useResolvedFieldDisabled()`(= fieldCtx.disabled,涵蓋 <Field disabled> 與 <Field mode=\"disabled\">),標準 Field\n // 家族 wrapper disabled 時才 dim;**沒包在 Field wrapper 內的 standalone Avatar**(ProfileCard / FileItem /\n // HoverCard / Dialog 等 display 場景)**backward compat 不變**。對齊 avatar.spec.md「Avatar 在 disabled\n // 元件內 host-controlled opacity」canonical — 升級成「Avatar self-managed via fieldCtx」。\n // 2026-06-08 SSOT:欄位內 Avatar 跟隨 <Field disabled>/<Field mode=\"disabled\"> 變淡(fieldCtx-scoped,cell 無 fieldCtx → 不影響)\n const isDisabledInField = useResolvedFieldDisabled()\n const isFill = size === 'fill'\n // Fill 模式下 icon 用 60% 寬高、text 用 50cqi(container query inline-size);\n // 數字模式下用既有 px 計算\n const numSize = isFill ? 32 : (size as number)\n const iconPx = getIconSize(numSize)\n const fontSizePx = Math.round(numSize * 0.5)\n const variantKey: VariantKey = solid ? 'solid' : 'subtle'\n const colors = COLOR_MAP[variantKey]?.[color] ?? COLOR_MAP.subtle.neutral\n // 2026-07-14 dim-20 修:inline style 不能用 rounded-* class,但可消費 radius token\n // (radius.spec.md:18 canonical「所有 4px 圓角一律用 rounded-md」+ :86 禁硬寫值;\n // token 改值 Avatar 跟隨 — M17 SSOT 傳播)。\n const radius = shape === 'circle' ? 'var(--radius-full)' : 'var(--radius-md)'\n\n // 決定內容\n const showImage = src && !imgError\n const showIcon = !showImage && (Icon || (!alt))\n const showText = !showImage && !showIcon && alt\n\n const FallbackIcon = Icon ?? User\n\n // Status dot 尺寸:avatar 的 28%(Slack / Teams / Discord 世界級平均),\n // clamp [8, 16] — floor 8 保小 avatar 仍可辨識但不喧賓奪主(10 floor 會讓 24px\n // avatar 的 dot 占 42% 太大);ceiling 16 防大 avatar dot 過度放大\n const dotSize = isFill ? 10 : Math.max(8, Math.min(16, Math.round(numSize * 0.28)))\n // 圓點與頭像之間的縫 = DS 通用縫;dotSize ≥ 12 時多 1px 保持視覺比例(avatar.spec.md「Status dot 尺寸與比例」)。\n // 縫是**挖空**露出真正的底,不是畫一圈(上方「疊在頭像上的東西」段)。\n const dotGap = dotSize >= 12 ? STACK_GAP_PX + 1 : STACK_GAP_PX\n // 頭像堆疊(上方「頭像堆疊」段):挖空掛在這一層(圓),不掛在最外層 —— 焦點框畫在最外層。\n const isStackItem = stacked && !isFill\n // 計數徽章是膠囊、寬度隨字數變 —— 量它的盒子才知道要挖多大的洞\n const showBadge = typeof badgeCount === 'number' && badgeCount > 0\n const badgeRef = React.useRef<HTMLSpanElement>(null)\n const [badgeBox, setBadgeBox] = React.useState<{ widthPx: number; heightPx: number } | null>(null)\n React.useLayoutEffect(() => {\n const el = badgeRef.current\n if (!el) { setBadgeBox(null); return }\n const read = () => setBadgeBox((prev) =>\n prev && prev.widthPx === el.offsetWidth && prev.heightPx === el.offsetHeight ? prev : { widthPx: el.offsetWidth, heightPx: el.offsetHeight })\n read()\n const obs = new ResizeObserver(read)\n obs.observe(el)\n return () => obs.disconnect()\n }, [showBadge, badgeCount])\n const ownCutout = avatarOverlayCutout({\n dot: status ? { sizePx: dotSize, gapPx: dotGap } : undefined,\n dismiss: dismissCutout,\n badge: showBadge ? badgeBox : null,\n })\n\n const avatarEl = (\n <div\n className={cn(\n 'inline-flex items-center justify-center shrink-0 overflow-hidden select-none',\n isFill && 'w-full h-full',\n // 2026-05-13 R3.5 self-dim:Avatar 在 disabled Field wrapper context 內自 dim\n // (取代 field-wrapper.tsx default/bare/naked disabled blanket opacity-disabled 逃生艙)\n isDisabledInField && 'opacity-disabled',\n isStackItem && !ownCutout && AVATAR_STACK_CUTOUT_CLASS,\n ownCutout && AVATAR_OWN_CUTOUT_CLASS,\n ownCutout && isStackItem && AVATAR_STACK_OWN_CUTOUT_CLASS,\n )}\n style={{\n ...(isFill\n ? { containerType: 'inline-size' as React.CSSProperties['containerType'] }\n : { width: numSize, height: numSize }),\n borderRadius: radius,\n backgroundColor: showImage ? undefined : colors.bg,\n color: showImage ? undefined : colors.text,\n ...(isStackItem ? avatarStackCutoutStyle(numSize) : null),\n ...(ownCutout ? {\n ['--avatar-own-cutout' as string]: ownCutout.image,\n ['--avatar-own-composite' as string]: ownCutout.composite,\n ['--avatar-dismiss-r' as string]: `${AVATAR_DISMISS_OVERLAY_PX.size / 2 + STACK_GAP_PX}px`,\n } : null),\n }}\n data-avatar-size={isFill ? 'fill' : numSize}\n role={!showImage && alt && !hoverCard ? 'img' : undefined}\n aria-label={!showImage && alt && !hoverCard ? alt : undefined}\n >\n {showImage && (\n <img\n src={src}\n alt={alt ?? ''}\n className=\"w-full h-full object-cover\"\n onError={() => setImgError(true)}\n />\n )}\n {showIcon && (\n isFill\n ? <FallbackIcon className=\"w-[60%] h-[60%]\" aria-hidden />\n : <FallbackIcon size={iconPx} aria-hidden />\n )}\n {showText && (\n <span\n className=\"font-medium leading-none\"\n style={{ fontSize: isFill ? '50cqi' : fontSizePx }}\n aria-hidden\n >\n {getInitial(alt!)}\n </span>\n )}\n </div>\n )\n\n const hasOverlay = status || typeof badgeCount === 'number'\n // Keyboard access canonical(D4 2026-04-22 finding;2026-07-06 user 拍板修正 ARIA 空承諾):\n // Avatar with `hoverCard` 需 keyboard 可達 — Radix `HoverCardTrigger asChild` 不自動加\n // tabIndex,non-focusable `<div>` 會讓 keyboard-only user 無法 reach popover(WCAG 2.1.1)。\n // 解:`tabIndex=0` + `role=\"img\"` + `aria-label` + focus ring(focus 時 Radix 自動開卡)。\n // **不掛 `role=\"button\"` / `aria-haspopup`**(2026-07-06 拆除):原本宣告了卻沒有\n // Enter/Space 行為、沒有 aria-expanded = 對 AT 的空承諾;對齊 Radix hover-card 官方\n // (刻意零 popup ARIA,hover card 定位 sighted-user 輔助)+ GitHub hovercard(trigger\n // 純連結無 popup ARIA)。若無 hoverCard 則維持純展示 `<div>`。\n // **`role=\"img\"`(2026-07-07 CI axe 補修)**:aria-label 在無 role 的 generic `<div>` 上\n // 是 ARIA 禁用組合(axe aria-prohibited-attr,serious;2026-07-06 拆 role=\"button\" 後裸露)。\n // Avatar 語義本質 = 身份圖像 → `role=\"img\"` 讓 aria-label 合法、子孫自動 presentational、\n // 且不對 AT 承諾任何互動行為 — 與「拆假承諾」拍板意圖一致(emoji span / MUI Avatar img alt 同款語義)。\n // a11y:role=\"img\" 讓子孫 presentational → 內層 Badge 的 count 語義被壓平,SR 讀不到。\n // 解:badgeCount 併入外層 aria-label(對齊 Slack「單一 accessible name 含未讀數」做法)。\n const focusableProps = hoverCard\n ? {\n tabIndex: 0,\n role: 'img',\n 'aria-label':\n typeof badgeCount === 'number' && badgeCount > 0\n ? `${alt ?? 'View profile'}, ${badgeAriaLabel ?? `${badgeCount} unread`}`\n : (alt ?? 'View profile'),\n }\n : {}\n // 2026-05-31:focus ring 圓角跟隨 shape(circle→rounded-full / square→rounded-md,對齊 body 的 `radius` 變數 var(--radius-full)/var(--radius-md)),\n // 原寫死 rounded-full 會讓方形 avatar(實體)配 hoverCard 時出現圓形 ring。hoverCard 為通用行為(任意內容),\n // 方形 avatar 合法可配(內容非 ProfileCard 而已),故 ring 必跟形狀。\n const focusableClass = hoverCard\n ? cn('', shape === 'circle' ? 'rounded-full' : 'rounded-md')\n : ''\n // 外框 = 可見圓(avatar.spec.md「外框 = 可見圓」):固定尺寸模式下最外層寬高必須等於 size,\n // 不得被父容器沿交叉軸拉伸。尺寸寫在內層(下方 numSize),root 只有 inline-flex shrink-0 ——\n // 而 shrink-0 只擋主軸收縮,擋不住交叉軸拉伸(直向 flex 拉寬;grid 由 justify-self 管,align-self 也碰不到),\n // 所以要鎖寬只能用 w-fit。root 一被撐寬會同時壞三處:狀態圓點 / badgeCount(相對 root 絕對定位)、\n // :focus-visible 焦點框(畫在 root 的 border box,帶 rounded-full → 變膠囊)、hoverCard 的 Radix 錨點(浮層橫向偏移)。\n // fill 模式的語意就是填滿父容器,必須排除。機械閘:scripts/avatar-anchor-box-invariant.mjs。\n const boxClass = isFill ? '' : 'w-fit'\n const stackAttr = isStackItem ? { 'data-avatar-stack': '' } : {}\n const baseEl = !hasOverlay\n ? <div ref={ref} className={cn('inline-flex shrink-0', boxClass, focusableClass, className)} style={style} {...stackAttr} {...focusableProps} {...props}>{avatarEl}</div>\n : (\n <div ref={ref} className={cn('relative inline-flex shrink-0', boxClass, focusableClass, className)} style={style} {...stackAttr} {...focusableProps} {...props}>\n {avatarEl}\n {/* Status dot:bottom-right(presence — 世界級對照 Slack / Teams / Discord),\n 落在 circle avatar 圓周 45° 位置 / square avatar 右下直角;\n 與頭像之間的縫是從圓上挖出來的(上方「疊在頭像上的東西」段),圓點本身不畫外圈。\n a11y:`aria-hidden` — presence 資訊整合到 parent avatar 的 aria-label\n (world-class Slack 做法),避免多 `role=\"status\"` 造成 screen reader 洪水 */}\n {status && (\n <span\n className=\"absolute block rounded-full\"\n style={{\n width: dotSize,\n height: dotSize,\n bottom: 0,\n right: 0,\n backgroundColor: STATUS_DOT_COLOR[status],\n }}\n aria-hidden\n />\n )}\n {/* Count badge:top-right(chat 未讀 / 通知計數 — 世界級對照 iMessage /\n Slack thread / LINE / WhatsApp)。消費 DS Badge(critical variant);\n 與頭像之間的縫同樣從圓上挖出來(外層 span 供 ResizeObserver 量膠囊的寬高) */}\n {showBadge && (\n <span ref={badgeRef} className=\"absolute -top-1 -right-1 inline-flex\">\n <Badge\n variant=\"critical\"\n count={badgeCount}\n max={99}\n aria-label={badgeAriaLabel ?? `${badgeCount} unread`} // i18n-allow: DS default 英文(consumer 可傳 badgeAriaLabel i18n)\n />\n </span>\n )}\n </div>\n )\n\n // 2026-05-13 (c) scroll-defer perf(per user 拍 Path (c) + codex Q3 verdict):\n // DataTable scrolling 期間跳 HoverCard wrapper(Portal + useDocumentTheme observer 是\n // Roadmap 重渲 hotspot,per codex Layer C 分析)。scroll 結束 → context flips false →\n // re-render 接回完整 HoverCard tree(ProfileCard 仍可 hover 顯示)。\n // 對齊 AG Grid `deferRender` for slow React cell components / MUI X DataGrid scroll-defer。\n if (!hoverCard || isTableScrolling) return baseEl\n\n return (\n <HoverCard openDelay={MOTION_DELAY_RICH_MS} closeDelay={MOTION_DELAY_CLOSE_MS}>\n <HoverCardTrigger asChild>\n {baseEl}\n </HoverCardTrigger>\n {/* HoverCardContent canonical(2026-04-23):\n - 無 inner padding(consumer ProfileCard 自帶 `px-4 py-3` chrome)\n - `overflow-hidden` + `rounded-lg` → child(ProfileCard)圓角裁切\n - **不設 max-height**:ProfileCard 自己消費 `--radix-hover-card-content-available-height`\n 自約束高度 + 內部 ScrollArea 處理捲動\n - `data-theme={documentTheme}`:ProfileCard 永遠跟隨 **app-level theme**(從 `<html data-theme>`\n 動態讀),不受 trigger subtree theme 污染。範例:Avatar 位於 OverflowIndicator 的 dark\n tooltip 內,其 Portal 會繼承該 subtree dark theme → ProfileCard 變全黑。顯式設回 app theme\n 確保 ProfileCard 永遠 light-in-light-app / dark-in-dark-app。 */}\n <HoverCardContent\n data-theme={documentTheme ?? undefined}\n className=\"bg-surface-raised rounded-lg border border-border overflow-hidden\"\n style={{ boxShadow: 'var(--elevation-200)' }}\n >\n {hoverCard}\n </HoverCardContent>\n </HoverCard>\n )\n }\n)\n\n// ── AvatarData ─────────────────────────────────────────────────────────────\n// 資料型別,讓 consumer 傳資料而非 ReactNode。\n// 接收端內部用 Avatar 元件渲染,統一控制尺寸與 fallback。\n\nexport interface AvatarData {\n /** 圖片 URL */\n src?: string\n /** 替代文字(圖片失敗時取首字作 fallback) */\n alt: string\n /** Icon / text fallback 的背景色,預設 neutral */\n color?: ColorKey\n /**\n * Person avatar hover ProfileCard(DS-wide canonical,person avatar 預設必有,見 avatar.spec.md)。\n * Entity avatar(專案 / 組織 logo)不帶 → consumer 不傳 hoverCard 即豁免。\n * 消費 AvatarData 的 row primitive(MenuItem menu-item.tsx / SelectionItem selection-item.tsx,\n * DropdownMenu / SelectMenu 經 MenuItem)需 forward 此 prop 到內部 <Avatar hoverCard={...} />。\n * 例外:ProfileCard 內部 Avatar 不 forward(卡片本身就是 hover 目的地,nested hover-in-hover 不合理)。\n */\n hoverCard?: React.ReactNode\n}\n\n// Story auto-compile metadata — Phase 1 mechanical migration(2026-04-24)\n// Phase 2 fill needed: purpose descriptions + when rationale + world-class refs\nexport const avatarMeta = {\n component: 'Avatar',\n family: null, // non-family composite / overlay / layout\n // 2026-07-04 audit 標記:Avatar 無 variant 軸、size 為任意 number(px,由 consumer context 決定\n // — item-anatomy AVATAR_SIZE 查表 / ProfileCard 40 等),無 discrete tier → 兩欄留空為 intentional 非漏填\n variants: {},\n sizes: {},\n states: ['default', 'focus-visible', 'disabled'], // 2026-06-11 R2:本身無自有 hover/active(spec「StateBehavior 範疇」段;hoverCard 互動屬 HoverCard),\n tokens: {\n bg: ['bg-surface-raised'],\n fg: ['--foreground', '--on-emphasis'],\n ring: ['--ring'],\n },\n} as const\n\nAvatarInner.displayName = 'Avatar'\nconst Avatar = React.memo(AvatarInner)\n\nexport { Avatar }\n"],"names":[],"mappings":"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"}
|