@qijenchen/design-system 0.1.0-beta.80 → 0.1.0-beta.82
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CLAUDE.md +1 -1
- package/dist/components/Alert/alert.d.ts +5 -0
- package/dist/components/Alert/alert.d.ts.map +1 -1
- package/dist/components/Alert/alert.js +4 -1
- package/dist/components/Alert/alert.js.map +1 -1
- package/dist/components/AppShell/app-shell.d.ts +1 -1
- package/dist/components/AppShell/app-shell.js.map +1 -1
- package/dist/components/AspectRatio/aspect-ratio.d.ts +2 -1
- package/dist/components/AspectRatio/aspect-ratio.d.ts.map +1 -1
- package/dist/components/AspectRatio/aspect-ratio.js +1 -1
- package/dist/components/AspectRatio/aspect-ratio.js.map +1 -1
- package/dist/components/Avatar/avatar.d.ts +3 -2
- package/dist/components/Avatar/avatar.d.ts.map +1 -1
- package/dist/components/Avatar/avatar.js +8 -3
- package/dist/components/Avatar/avatar.js.map +1 -1
- package/dist/components/Badge/badge.d.ts.map +1 -1
- package/dist/components/Badge/badge.js +2 -1
- package/dist/components/Badge/badge.js.map +1 -1
- package/dist/components/Breadcrumb/breadcrumb.d.ts +14 -1
- package/dist/components/Breadcrumb/breadcrumb.d.ts.map +1 -1
- package/dist/components/Breadcrumb/breadcrumb.js +9 -3
- package/dist/components/Breadcrumb/breadcrumb.js.map +1 -1
- package/dist/components/BulkActionBar/bulk-action-bar.d.ts +4 -1
- package/dist/components/BulkActionBar/bulk-action-bar.d.ts.map +1 -1
- package/dist/components/BulkActionBar/bulk-action-bar.js +2 -2
- package/dist/components/BulkActionBar/bulk-action-bar.js.map +1 -1
- package/dist/components/Button/button.d.ts +4 -4
- package/dist/components/Button/button.d.ts.map +1 -1
- package/dist/components/Button/button.js +73 -44
- package/dist/components/Button/button.js.map +1 -1
- package/dist/components/Button/index.d.ts +1 -0
- package/dist/components/Button/index.d.ts.map +1 -1
- package/dist/components/Button/index.js +3 -0
- package/dist/components/Button/index.js.map +1 -1
- package/dist/components/Calendar/calendar.d.ts +3 -1
- package/dist/components/Calendar/calendar.d.ts.map +1 -1
- package/dist/components/Calendar/calendar.js +40 -17
- package/dist/components/Calendar/calendar.js.map +1 -1
- package/dist/components/Carousel/carousel.d.ts +1 -1
- package/dist/components/Carousel/carousel.d.ts.map +1 -1
- package/dist/components/Carousel/carousel.js +3 -1
- package/dist/components/Carousel/carousel.js.map +1 -1
- package/dist/components/Chart/chart.d.ts.map +1 -1
- package/dist/components/Chart/chart.js +1 -1
- package/dist/components/Chart/chart.js.map +1 -1
- package/dist/components/Checkbox/checkbox.d.ts +6 -3
- package/dist/components/Checkbox/checkbox.d.ts.map +1 -1
- package/dist/components/Checkbox/checkbox.js +26 -4
- package/dist/components/Checkbox/checkbox.js.map +1 -1
- package/dist/components/Checkbox/index.d.ts +1 -0
- package/dist/components/Checkbox/index.d.ts.map +1 -1
- package/dist/components/Checkbox/index.js +3 -0
- package/dist/components/Checkbox/index.js.map +1 -1
- package/dist/components/Chip/chip.d.ts +9 -3
- package/dist/components/Chip/chip.d.ts.map +1 -1
- package/dist/components/Chip/chip.js +46 -13
- package/dist/components/Chip/chip.js.map +1 -1
- package/dist/components/Coachmark/coachmark.d.ts +5 -3
- package/dist/components/Coachmark/coachmark.d.ts.map +1 -1
- package/dist/components/Coachmark/coachmark.js +10 -7
- package/dist/components/Coachmark/coachmark.js.map +1 -1
- package/dist/components/Combobox/combobox.d.ts +3 -1
- package/dist/components/Combobox/combobox.d.ts.map +1 -1
- package/dist/components/Combobox/combobox.js +11 -7
- package/dist/components/Combobox/combobox.js.map +1 -1
- package/dist/components/Command/command.d.ts +2 -2
- package/dist/components/Command/command.d.ts.map +1 -1
- package/dist/components/Command/command.js +4 -2
- package/dist/components/Command/command.js.map +1 -1
- package/dist/components/DataTable/active-editor-controller.d.ts +4 -3
- package/dist/components/DataTable/active-editor-controller.d.ts.map +1 -1
- package/dist/components/DataTable/cell-registry.d.ts.map +1 -1
- package/dist/components/DataTable/cell-registry.js +3 -0
- package/dist/components/DataTable/cell-registry.js.map +1 -1
- package/dist/components/DataTable/data-table-filter-panel.d.ts.map +1 -1
- package/dist/components/DataTable/data-table-interaction-layer.d.ts +4 -3
- package/dist/components/DataTable/data-table-interaction-layer.d.ts.map +1 -1
- package/dist/components/DataTable/data-table-interaction-layer.js.map +1 -1
- package/dist/components/DataTable/data-table.d.ts +38 -7
- package/dist/components/DataTable/data-table.d.ts.map +1 -1
- package/dist/components/DataTable/data-table.js +213 -63
- package/dist/components/DataTable/data-table.js.map +1 -1
- package/dist/components/DataTable/filter-tree.d.ts +5 -0
- package/dist/components/DataTable/filter-tree.d.ts.map +1 -1
- package/dist/components/DateGrid/date-grid.d.ts +3 -3
- package/dist/components/DateGrid/date-grid.d.ts.map +1 -1
- package/dist/components/DateGrid/date-grid.js +52 -6
- package/dist/components/DateGrid/date-grid.js.map +1 -1
- package/dist/components/DatePicker/date-picker.d.ts +2 -8
- package/dist/components/DatePicker/date-picker.d.ts.map +1 -1
- package/dist/components/DatePicker/date-picker.js +139 -109
- package/dist/components/DatePicker/date-picker.js.map +1 -1
- package/dist/components/DescriptionList/description-list.d.ts.map +1 -1
- package/dist/components/DescriptionList/description-list.js +1 -1
- package/dist/components/DescriptionList/description-list.js.map +1 -1
- package/dist/components/Dialog/dialog.d.ts +1 -1
- package/dist/components/Dialog/dialog.d.ts.map +1 -1
- package/dist/components/Dialog/dialog.js +2 -1
- package/dist/components/Dialog/dialog.js.map +1 -1
- package/dist/components/DropdownMenu/dropdown-menu.d.ts +4 -4
- package/dist/components/DropdownMenu/dropdown-menu.d.ts.map +1 -1
- package/dist/components/DropdownMenu/dropdown-menu.js +25 -8
- package/dist/components/DropdownMenu/dropdown-menu.js.map +1 -1
- package/dist/components/Empty/empty.d.ts.map +1 -1
- package/dist/components/Empty/empty.js +1 -1
- package/dist/components/Empty/empty.js.map +1 -1
- package/dist/components/Field/field-wrapper.d.ts +1 -0
- package/dist/components/Field/field-wrapper.d.ts.map +1 -1
- package/dist/components/Field/field-wrapper.js +27 -7
- package/dist/components/Field/field-wrapper.js.map +1 -1
- package/dist/components/Field/field.d.ts +1 -1
- package/dist/components/Field/field.d.ts.map +1 -1
- package/dist/components/Field/field.js +2 -1
- package/dist/components/Field/field.js.map +1 -1
- package/dist/components/Field/use-form-validation.d.ts +5 -1
- package/dist/components/Field/use-form-validation.d.ts.map +1 -1
- package/dist/components/Field/use-form-validation.js +31 -21
- package/dist/components/Field/use-form-validation.js.map +1 -1
- package/dist/components/FieldControlGroup/field-control-group.d.ts +3 -3
- package/dist/components/FieldControlGroup/field-control-group.js.map +1 -1
- package/dist/components/FileItem/file-item.d.ts.map +1 -1
- package/dist/components/FileItem/file-item.js +3 -2
- package/dist/components/FileItem/file-item.js.map +1 -1
- package/dist/components/FileUpload/file-upload.d.ts +1 -1
- package/dist/components/FileUpload/file-upload.d.ts.map +1 -1
- package/dist/components/FileUpload/file-upload.js +22 -10
- package/dist/components/FileUpload/file-upload.js.map +1 -1
- package/dist/components/FileViewer/can-render-image.d.ts +6 -0
- package/dist/components/FileViewer/can-render-image.d.ts.map +1 -0
- package/dist/components/FileViewer/can-render-image.js +11 -0
- package/dist/components/FileViewer/can-render-image.js.map +1 -0
- package/dist/components/FileViewer/file-viewer.d.ts +2 -2
- package/dist/components/FileViewer/file-viewer.d.ts.map +1 -1
- package/dist/components/FileViewer/file-viewer.js +24 -16
- package/dist/components/FileViewer/file-viewer.js.map +1 -1
- package/dist/components/FileViewer/image-renderer.d.ts +0 -5
- package/dist/components/FileViewer/image-renderer.d.ts.map +1 -1
- package/dist/components/FileViewer/image-renderer.js +1 -9
- package/dist/components/FileViewer/image-renderer.js.map +1 -1
- package/dist/components/HoverCard/hover-card.d.ts.map +1 -1
- package/dist/components/HoverCard/hover-card.js +3 -1
- package/dist/components/HoverCard/hover-card.js.map +1 -1
- package/dist/components/Input/input.d.ts +26 -3
- package/dist/components/Input/input.d.ts.map +1 -1
- package/dist/components/Input/input.js +12 -8
- package/dist/components/Input/input.js.map +1 -1
- package/dist/components/LinkInput/link-input.d.ts +3 -3
- package/dist/components/LinkInput/link-input.d.ts.map +1 -1
- package/dist/components/LinkInput/link-input.js +29 -28
- package/dist/components/LinkInput/link-input.js.map +1 -1
- package/dist/components/Menu/menu-item.d.ts.map +1 -1
- package/dist/components/Menu/menu-item.js +5 -4
- package/dist/components/Menu/menu-item.js.map +1 -1
- package/dist/components/Notice/notice.d.ts +5 -0
- package/dist/components/Notice/notice.d.ts.map +1 -1
- package/dist/components/Notice/notice.js +11 -5
- package/dist/components/Notice/notice.js.map +1 -1
- package/dist/components/NumberInput/number-input.d.ts +1 -1
- package/dist/components/NumberInput/number-input.d.ts.map +1 -1
- package/dist/components/NumberInput/number-input.js +17 -8
- package/dist/components/NumberInput/number-input.js.map +1 -1
- package/dist/components/OverflowIndicator/overflow-indicator.d.ts +1 -1
- package/dist/components/OverflowIndicator/overflow-indicator.d.ts.map +1 -1
- package/dist/components/OverflowIndicator/overflow-indicator.js +2 -1
- package/dist/components/OverflowIndicator/overflow-indicator.js.map +1 -1
- package/dist/components/Pagination/index.d.ts +2 -0
- package/dist/components/Pagination/index.d.ts.map +1 -0
- package/dist/components/Pagination/index.js +6 -0
- package/dist/components/Pagination/index.js.map +1 -0
- package/dist/components/Pagination/pagination.d.ts +49 -0
- package/dist/components/Pagination/pagination.d.ts.map +1 -0
- package/dist/components/Pagination/pagination.js +182 -0
- package/dist/components/Pagination/pagination.js.map +1 -0
- package/dist/components/PeoplePicker/people-picker.d.ts +3 -3
- package/dist/components/PeoplePicker/people-picker.d.ts.map +1 -1
- package/dist/components/PeoplePicker/people-picker.js +38 -36
- package/dist/components/PeoplePicker/people-picker.js.map +1 -1
- package/dist/components/PeoplePicker/person-display.d.ts +4 -0
- package/dist/components/PeoplePicker/person-display.d.ts.map +1 -1
- package/dist/components/PeoplePicker/person-display.js +3 -3
- package/dist/components/PeoplePicker/person-display.js.map +1 -1
- package/dist/components/Popover/popover.d.ts.map +1 -1
- package/dist/components/Popover/popover.js +58 -32
- package/dist/components/Popover/popover.js.map +1 -1
- package/dist/components/ProfileCard/profile-card.d.ts +11 -3
- package/dist/components/ProfileCard/profile-card.d.ts.map +1 -1
- package/dist/components/ProfileCard/profile-card.js +14 -4
- package/dist/components/ProfileCard/profile-card.js.map +1 -1
- package/dist/components/RadioGroup/radio-group.d.ts +1 -1
- package/dist/components/RadioGroup/radio-group.d.ts.map +1 -1
- package/dist/components/RadioGroup/radio-group.js +28 -3
- package/dist/components/RadioGroup/radio-group.js.map +1 -1
- package/dist/components/Rating/rating.js +3 -3
- 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 +2 -2
- package/dist/components/ScrollArea/scroll-area.js.map +1 -1
- package/dist/components/SegmentedControl/segmented-control.d.ts +2 -2
- package/dist/components/SegmentedControl/segmented-control.d.ts.map +1 -1
- package/dist/components/SegmentedControl/segmented-control.js +23 -10
- package/dist/components/SegmentedControl/segmented-control.js.map +1 -1
- package/dist/components/Select/select.d.ts +10 -4
- package/dist/components/Select/select.d.ts.map +1 -1
- package/dist/components/Select/select.js +57 -14
- package/dist/components/Select/select.js.map +1 -1
- package/dist/components/SelectMenu/index.js +4 -2
- package/dist/components/SelectMenu/select-menu.d.ts +31 -6
- package/dist/components/SelectMenu/select-menu.d.ts.map +1 -1
- package/dist/components/SelectMenu/select-menu.js +169 -85
- package/dist/components/SelectMenu/select-menu.js.map +1 -1
- package/dist/components/Separator/separator.js +1 -1
- package/dist/components/Separator/separator.js.map +1 -1
- package/dist/components/Sheet/sheet.d.ts +1 -1
- package/dist/components/Sheet/sheet.d.ts.map +1 -1
- package/dist/components/Sheet/sheet.js +3 -2
- package/dist/components/Sheet/sheet.js.map +1 -1
- package/dist/components/Sidebar/sidebar.d.ts +2 -2
- package/dist/components/Sidebar/sidebar.d.ts.map +1 -1
- package/dist/components/Sidebar/sidebar.js +28 -10
- package/dist/components/Sidebar/sidebar.js.map +1 -1
- package/dist/components/Skeleton/skeleton.d.ts +1 -1
- package/dist/components/Skeleton/skeleton.js +2 -2
- package/dist/components/Skeleton/skeleton.js.map +1 -1
- package/dist/components/Slider/slider.d.ts +1 -1
- package/dist/components/Slider/slider.d.ts.map +1 -1
- package/dist/components/Slider/slider.js +7 -3
- package/dist/components/Slider/slider.js.map +1 -1
- package/dist/components/Steps/steps.d.ts.map +1 -1
- package/dist/components/Steps/steps.js +6 -3
- package/dist/components/Steps/steps.js.map +1 -1
- package/dist/components/Switch/switch.d.ts +2 -2
- package/dist/components/Switch/switch.d.ts.map +1 -1
- package/dist/components/Switch/switch.js +47 -9
- package/dist/components/Switch/switch.js.map +1 -1
- package/dist/components/Tabs/tabs.d.ts +8 -5
- package/dist/components/Tabs/tabs.d.ts.map +1 -1
- package/dist/components/Tabs/tabs.js +45 -33
- package/dist/components/Tabs/tabs.js.map +1 -1
- package/dist/components/Tag/tag.d.ts.map +1 -1
- package/dist/components/Tag/tag.js +20 -4
- package/dist/components/Tag/tag.js.map +1 -1
- package/dist/components/Textarea/textarea.d.ts +2 -1
- package/dist/components/Textarea/textarea.d.ts.map +1 -1
- package/dist/components/Textarea/textarea.js +24 -13
- package/dist/components/Textarea/textarea.js.map +1 -1
- package/dist/components/TimePicker/time-picker.d.ts +2 -2
- package/dist/components/TimePicker/time-picker.d.ts.map +1 -1
- package/dist/components/TimePicker/time-picker.js +23 -12
- package/dist/components/TimePicker/time-picker.js.map +1 -1
- package/dist/components/Toast/toast.d.ts +1 -1
- package/dist/components/Toast/toast.d.ts.map +1 -1
- package/dist/components/Toast/toast.js +7 -3
- package/dist/components/Toast/toast.js.map +1 -1
- package/dist/components/Tooltip/tooltip.js +1 -1
- package/dist/components/Tooltip/tooltip.js.map +1 -1
- package/dist/components/TreeView/tree-view.d.ts +1 -1
- package/dist/components/TreeView/tree-view.d.ts.map +1 -1
- package/dist/components/TreeView/tree-view.js +11 -5
- package/dist/components/TreeView/tree-view.js.map +1 -1
- package/dist/hooks/use-controllable.d.ts.map +1 -1
- package/dist/hooks/use-controllable.js +4 -2
- package/dist/hooks/use-controllable.js.map +1 -1
- package/dist/hooks/use-is-touch-device.d.ts.map +1 -1
- package/dist/hooks/use-is-touch-device.js +3 -1
- package/dist/hooks/use-is-touch-device.js.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +14 -0
- package/dist/index.js.map +1 -1
- package/dist/patterns/element-anatomy/item-anatomy.d.ts +2 -2
- package/dist/patterns/element-anatomy/item-anatomy.d.ts.map +1 -1
- package/dist/patterns/element-anatomy/item-anatomy.js.map +1 -1
- package/dist/patterns/overlay-surface/overlay-surface.js.map +1 -1
- package/dist/tokens/categorical-color.d.ts +1 -1
- package/dist/tokens/categorical-color.js.map +1 -1
- package/dist/tokens/uiSize/icon-size.d.ts +2 -2
- package/ds-canonical/fork/governance.lock +1 -1
- package/ds-canonical/fork/hooks/check_opacity_token_usage.sh +10 -1
- package/ds-canonical/hooks/check_audit_post_report_validator.sh +13 -0
- package/ds-canonical/hooks/check_full_story_visual_interaction_sweep.sh +4 -3
- package/ds-canonical/hooks/check_opacity_token_usage.sh +10 -1
- package/ds-canonical/hooks/check_plugin_fork_health.sh +30 -22
- package/ds-canonical/hooks/check_story_invariants.sh +17 -5
- package/ds-canonical/hooks/check_substantive_edit_approval_preflight.sh +5 -2
- package/ds-canonical/references/principle-dim-map.json +100 -0
- package/ds-canonical/references/story-baseline-registry.json +4 -3
- package/ds-canonical/skills/deep-audit-cross-codex/SKILL.md +1 -1
- package/ds-canonical/skills/design-system-audit/SKILL.md +2 -2
- package/ds-story-manifest.json +41 -18
- package/llms-full.txt +11 -1
- package/llms.txt +3 -1
- package/package.json +1 -1
- package/src/components/Alert/alert.anatomy.stories.tsx +9 -6
- package/src/components/Alert/alert.spec.md +3 -3
- package/src/components/Alert/alert.tsx +9 -1
- package/src/components/AppShell/app-shell.anatomy.stories.tsx +3 -0
- package/src/components/AppShell/app-shell.spec.md +3 -2
- package/src/components/AppShell/app-shell.tsx +1 -1
- package/src/components/AspectRatio/aspect-ratio.spec.md +1 -2
- package/src/components/AspectRatio/aspect-ratio.tsx +3 -2
- package/src/components/Avatar/avatar.principles.stories.tsx +1 -1
- package/src/components/Avatar/avatar.spec.md +10 -30
- package/src/components/Avatar/avatar.stories.tsx +1 -1
- package/src/components/Avatar/avatar.tsx +30 -16
- package/src/components/Badge/badge.anatomy.stories.tsx +2 -2
- package/src/components/Badge/badge.spec.md +5 -4
- package/src/components/Badge/badge.tsx +4 -1
- package/src/components/Breadcrumb/breadcrumb.anatomy.stories.tsx +2 -2
- package/src/components/Breadcrumb/breadcrumb.principles.stories.tsx +15 -10
- package/src/components/Breadcrumb/breadcrumb.spec.md +5 -3
- package/src/components/Breadcrumb/breadcrumb.tsx +10 -4
- package/src/components/BulkActionBar/bulk-action-bar.anatomy.stories.tsx +3 -2
- package/src/components/BulkActionBar/bulk-action-bar.spec.md +15 -14
- package/src/components/BulkActionBar/bulk-action-bar.tsx +10 -4
- package/src/components/Button/button.anatomy.stories.tsx +6 -6
- package/src/components/Button/button.spec.md +3 -2
- package/src/components/Button/button.tsx +67 -27
- package/src/components/Button/index.ts +1 -0
- package/src/components/Calendar/calendar.anatomy.stories.tsx +5 -3
- package/src/components/Calendar/calendar.principles.stories.tsx +3 -1
- package/src/components/Calendar/calendar.spec.md +9 -2
- package/src/components/Calendar/calendar.stories.tsx +3 -1
- package/src/components/Calendar/calendar.tsx +53 -19
- package/src/components/Carousel/carousel.principles.stories.tsx +1 -1
- package/src/components/Carousel/carousel.tsx +4 -1
- package/src/components/Chart/chart.spec.md +1 -1
- package/src/components/Chart/chart.tsx +2 -1
- package/src/components/Checkbox/checkbox.principles.stories.tsx +1 -1
- package/src/components/Checkbox/checkbox.spec.md +1 -1
- package/src/components/Checkbox/checkbox.tsx +41 -7
- package/src/components/Checkbox/index.ts +1 -0
- package/src/components/Chip/chip.anatomy.stories.tsx +9 -9
- package/src/components/Chip/chip.principles.stories.tsx +3 -3
- package/src/components/Chip/chip.spec.md +7 -4
- package/src/components/Chip/chip.tsx +60 -15
- package/src/components/CircularProgress/circular-progress.anatomy.stories.tsx +1 -1
- package/src/components/CircularProgress/circular-progress.principles.stories.tsx +7 -5
- package/src/components/CircularProgress/circular-progress.spec.md +2 -0
- package/src/components/CircularProgress/circular-progress.stories.tsx +3 -11
- package/src/components/Coachmark/coachmark.anatomy.stories.tsx +2 -2
- package/src/components/Coachmark/coachmark.principles.stories.tsx +1 -1
- package/src/components/Coachmark/coachmark.spec.md +3 -3
- package/src/components/Coachmark/coachmark.tsx +23 -13
- package/src/components/Combobox/combobox.anatomy.stories.tsx +4 -4
- package/src/components/Combobox/combobox.spec.md +1 -1
- package/src/components/Combobox/combobox.tsx +30 -17
- package/src/components/Command/command.principles.stories.tsx +10 -12
- package/src/components/Command/command.spec.md +5 -5
- package/src/components/Command/command.stories.tsx +4 -4
- package/src/components/Command/command.tsx +3 -2
- package/src/components/DataTable/active-editor-controller.ts +4 -3
- package/src/components/DataTable/cell-registry.tsx +8 -0
- package/src/components/DataTable/data-table-filter-panel.tsx +8 -2
- package/src/components/DataTable/data-table-interaction-layer.tsx +8 -7
- package/src/components/DataTable/data-table.anatomy.stories.tsx +4 -4
- package/src/components/DataTable/data-table.css +2 -2
- package/src/components/DataTable/data-table.spec.md +72 -115
- package/src/components/DataTable/data-table.stories.tsx +51 -3
- package/src/components/DataTable/data-table.tsx +348 -63
- package/src/components/DataTable/filter-operators.spec.md +4 -4
- package/src/components/DataTable/filter-tree.ts +192 -42
- package/src/components/DateGrid/date-grid.anatomy.stories.tsx +1 -1
- package/src/components/DateGrid/date-grid.spec.md +11 -8
- package/src/components/DateGrid/date-grid.tsx +63 -8
- package/src/components/DatePicker/date-picker.principles.stories.tsx +1 -1
- package/src/components/DatePicker/date-picker.spec.md +1 -0
- package/src/components/DatePicker/date-picker.stories.tsx +19 -0
- package/src/components/DatePicker/date-picker.tsx +52 -15
- package/src/components/DescriptionList/description-list.spec.md +1 -1
- package/src/components/DescriptionList/description-list.tsx +3 -1
- package/src/components/Dialog/dialog.spec.md +1 -1
- package/src/components/Dialog/dialog.tsx +2 -1
- package/src/components/DropdownMenu/dropdown-menu.anatomy.stories.tsx +3 -1
- package/src/components/DropdownMenu/dropdown-menu.spec.md +8 -5
- package/src/components/DropdownMenu/dropdown-menu.tsx +33 -7
- package/src/components/Empty/empty.anatomy.stories.tsx +1 -1
- package/src/components/Empty/empty.spec.md +2 -2
- package/src/components/Empty/empty.stories.tsx +4 -15
- package/src/components/Empty/empty.tsx +4 -1
- package/src/components/Field/field-controls.spec.md +2 -1
- package/src/components/Field/field-wrapper.tsx +28 -8
- package/src/components/Field/field.anatomy.stories.tsx +7 -7
- package/src/components/Field/field.principles.stories.tsx +1 -1
- package/src/components/Field/field.spec.md +9 -7
- package/src/components/Field/field.tsx +2 -1
- package/src/components/Field/form-validation.spec.md +6 -3
- package/src/components/Field/use-form-validation.ts +49 -21
- package/src/components/FieldControlGroup/field-control-group.anatomy.stories.tsx +11 -10
- package/src/components/FieldControlGroup/field-control-group.principles.stories.tsx +3 -3
- package/src/components/FieldControlGroup/field-control-group.spec.md +3 -3
- package/src/components/FieldControlGroup/field-control-group.tsx +3 -3
- package/src/components/FileItem/file-item.anatomy.stories.tsx +6 -4
- package/src/components/FileItem/file-item.tsx +3 -2
- package/src/components/FileUpload/file-upload.anatomy.stories.tsx +1 -1
- package/src/components/FileUpload/file-upload.spec.md +4 -1
- package/src/components/FileUpload/file-upload.tsx +36 -10
- package/src/components/FileViewer/can-render-image.ts +15 -0
- package/src/components/FileViewer/file-viewer.anatomy.stories.tsx +6 -4
- package/src/components/FileViewer/file-viewer.spec.md +2 -2
- package/src/components/FileViewer/file-viewer.tsx +47 -21
- package/src/components/FileViewer/image-renderer.tsx +3 -8
- package/src/components/HoverCard/hover-card.spec.md +1 -1
- package/src/components/HoverCard/hover-card.tsx +3 -1
- package/src/components/Input/input.anatomy.stories.tsx +4 -4
- package/src/components/Input/input.principles.stories.tsx +1 -1
- package/src/components/Input/input.spec.md +1 -1
- package/src/components/Input/input.stories.tsx +11 -5
- package/src/components/Input/input.tsx +11 -9
- package/src/components/LinkInput/link-input.anatomy.stories.tsx +2 -0
- package/src/components/LinkInput/link-input.tsx +49 -34
- package/src/components/Menu/menu-item.anatomy.stories.tsx +1 -1
- package/src/components/Menu/menu-item.spec.md +4 -3
- package/src/components/Menu/menu-item.tsx +15 -4
- package/src/components/Notice/notice.anatomy.stories.tsx +10 -5
- package/src/components/Notice/notice.spec.md +4 -3
- package/src/components/Notice/notice.tsx +22 -5
- package/src/components/NumberInput/number-input.anatomy.stories.tsx +5 -4
- package/src/components/NumberInput/number-input.principles.stories.tsx +2 -1
- package/src/components/NumberInput/number-input.spec.md +2 -1
- package/src/components/NumberInput/number-input.tsx +25 -9
- package/src/components/OverflowIndicator/overflow-indicator.anatomy.stories.tsx +3 -3
- package/src/components/OverflowIndicator/overflow-indicator.spec.md +1 -1
- package/src/components/OverflowIndicator/overflow-indicator.tsx +2 -1
- package/src/components/Pagination/index.ts +5 -0
- package/src/components/Pagination/pagination.anatomy.stories.tsx +143 -0
- package/src/components/Pagination/pagination.principles.stories.tsx +81 -0
- package/src/components/Pagination/pagination.spec.md +150 -0
- package/src/components/Pagination/pagination.stories.tsx +98 -0
- package/src/components/Pagination/pagination.tsx +263 -0
- package/src/components/PeoplePicker/people-picker.anatomy.stories.tsx +1 -1
- package/src/components/PeoplePicker/people-picker.spec.md +4 -4
- package/src/components/PeoplePicker/people-picker.stories.tsx +1 -1
- package/src/components/PeoplePicker/people-picker.tsx +65 -55
- package/src/components/PeoplePicker/person-display.tsx +10 -4
- package/src/components/Popover/popover.anatomy.stories.tsx +2 -2
- package/src/components/Popover/popover.spec.md +13 -14
- package/src/components/Popover/popover.tsx +88 -39
- package/src/components/ProfileCard/profile-card.anatomy.stories.tsx +4 -4
- package/src/components/ProfileCard/profile-card.principles.stories.tsx +2 -2
- package/src/components/ProfileCard/profile-card.spec.md +1 -2
- package/src/components/ProfileCard/profile-card.stories.tsx +1 -1
- package/src/components/ProfileCard/profile-card.tsx +29 -8
- package/src/components/RadioGroup/radio-group.principles.stories.tsx +1 -1
- package/src/components/RadioGroup/radio-group.spec.md +2 -2
- package/src/components/RadioGroup/radio-group.stories.tsx +3 -1
- package/src/components/RadioGroup/radio-group.tsx +37 -3
- package/src/components/Rating/rating.anatomy.stories.tsx +11 -8
- package/src/components/Rating/rating.principles.stories.tsx +1 -1
- package/src/components/Rating/rating.spec.md +2 -2
- package/src/components/Rating/rating.stories.tsx +1 -1
- package/src/components/Rating/rating.tsx +3 -3
- package/src/components/ScrollArea/scroll-area.spec.md +1 -0
- package/src/components/ScrollArea/scroll-area.tsx +1 -1
- package/src/components/SegmentedControl/segmented-control.anatomy.stories.tsx +3 -3
- package/src/components/SegmentedControl/segmented-control.spec.md +7 -3
- package/src/components/SegmentedControl/segmented-control.tsx +29 -11
- package/src/components/Select/select.anatomy.stories.tsx +2 -2
- package/src/components/Select/select.spec.md +3 -2
- package/src/components/Select/select.tsx +62 -19
- package/src/components/SelectMenu/select-menu.anatomy.stories.tsx +1 -1
- package/src/components/SelectMenu/select-menu.principles.stories.tsx +1 -1
- package/src/components/SelectMenu/select-menu.spec.md +12 -8
- package/src/components/SelectMenu/select-menu.tsx +176 -20
- package/src/components/SelectionControl/selection-item.anatomy.stories.tsx +1 -1
- package/src/components/SelectionControl/selection-item.principles.stories.tsx +2 -2
- package/src/components/SelectionControl/selection-item.spec.md +2 -1
- package/src/components/SelectionControl/selection-item.stories.tsx +1 -1
- package/src/components/Separator/separator.anatomy.stories.tsx +2 -2
- package/src/components/Separator/separator.tsx +1 -1
- package/src/components/Sheet/sheet.spec.md +2 -1
- package/src/components/Sheet/sheet.tsx +4 -2
- package/src/components/Sidebar/sidebar.anatomy.stories.tsx +1 -1
- package/src/components/Sidebar/sidebar.spec.md +2 -1
- package/src/components/Sidebar/sidebar.stories.tsx +6 -4
- package/src/components/Sidebar/sidebar.tsx +41 -13
- package/src/components/Skeleton/skeleton.principles.stories.tsx +1 -1
- package/src/components/Skeleton/skeleton.spec.md +1 -2
- package/src/components/Skeleton/skeleton.tsx +1 -1
- package/src/components/Slider/slider.anatomy.stories.tsx +4 -4
- package/src/components/Slider/slider.spec.md +19 -85
- package/src/components/Slider/slider.tsx +13 -5
- package/src/components/Steps/steps.anatomy.stories.tsx +1 -1
- package/src/components/Steps/steps.spec.md +6 -4
- package/src/components/Steps/steps.tsx +11 -3
- package/src/components/Switch/switch.spec.md +4 -3
- package/src/components/Switch/switch.tsx +59 -10
- package/src/components/Tabs/tabs.anatomy.stories.tsx +7 -6
- package/src/components/Tabs/tabs.principles.stories.tsx +5 -3
- package/src/components/Tabs/tabs.spec.md +15 -48
- package/src/components/Tabs/tabs.tsx +70 -34
- package/src/components/Tag/tag.principles.stories.tsx +2 -2
- package/src/components/Tag/tag.spec.md +11 -43
- package/src/components/Tag/tag.tsx +30 -4
- package/src/components/Textarea/textarea.principles.stories.tsx +2 -1
- package/src/components/Textarea/textarea.spec.md +2 -1
- package/src/components/Textarea/textarea.tsx +20 -10
- package/src/components/TimePicker/time-picker.anatomy.stories.tsx +2 -1
- package/src/components/TimePicker/time-picker.spec.md +2 -2
- package/src/components/TimePicker/time-picker.tsx +35 -16
- package/src/components/Toast/toast.anatomy.stories.tsx +5 -6
- package/src/components/Toast/toast.principles.stories.tsx +10 -11
- package/src/components/Toast/toast.spec.md +5 -5
- package/src/components/Toast/toast.stories.tsx +5 -1
- package/src/components/Toast/toast.tsx +13 -7
- package/src/components/Tooltip/tooltip.spec.md +3 -3
- package/src/components/Tooltip/tooltip.tsx +1 -1
- package/src/components/TreeView/tree-view.anatomy.stories.tsx +6 -1
- package/src/components/TreeView/tree-view.principles.stories.tsx +2 -2
- package/src/components/TreeView/tree-view.spec.md +2 -2
- package/src/components/TreeView/tree-view.tsx +27 -5
- package/src/hooks/use-controllable.ts +7 -2
- package/src/hooks/use-is-touch-device.ts +8 -1
- package/src/index.ts +2 -1
- package/src/patterns/element-anatomy/element-anatomy.spec.md +3 -3
- package/src/patterns/element-anatomy/inline-action.spec.md +1 -1
- package/src/patterns/element-anatomy/item-anatomy.spec.md +5 -14
- package/src/patterns/element-anatomy/item-anatomy.stories.tsx +1 -0
- package/src/patterns/element-anatomy/item-anatomy.tsx +5 -6
- package/src/patterns/header-canonical/header-canonical.css +1 -2
- package/src/patterns/header-canonical/header-canonical.spec.md +5 -5
- package/src/patterns/horizontal-overflow/horizontal-overflow.spec.md +5 -4
- package/src/patterns/horizontal-overflow/horizontal-overflow.stories.tsx +137 -0
- package/src/patterns/overlay-surface/overlay-surface.spec.md +8 -7
- package/src/patterns/overlay-surface/overlay-surface.stories.tsx +116 -0
- package/src/patterns/overlay-surface/overlay-surface.tsx +5 -5
- package/src/patterns/resize-handle/resize-handle.spec.md +1 -1
- package/src/tokens/categorical-color.ts +1 -1
- package/src/tokens/color/color.spec.md +11 -13
- package/src/tokens/color/color.stories.tsx +1 -1
- package/src/tokens/color/primitives.css +7 -5
- package/src/tokens/color/semantic.css +8 -2
- package/src/tokens/density/density.spec.md +2 -0
- package/src/tokens/elevation/elevation.spec.md +1 -1
- package/src/tokens/layoutSpace/layoutSpace.spec.md +2 -1
- package/src/tokens/layoutSpace/layoutSpace.stories.tsx +3 -3
- package/src/tokens/motion/motion.spec.md +1 -1
- package/src/tokens/motion/motion.stories.tsx +142 -0
- package/src/tokens/opacity/opacity.spec.md +1 -1
- package/src/tokens/opacity/opacity.stories.tsx +7 -11
- package/src/tokens/radius/radius.spec.md +1 -1
- package/src/tokens/typography/typography.css +1 -1
- package/src/tokens/typography/typography.spec.md +1 -1
- package/src/tokens/uiSize/icon-size.ts +2 -2
- package/src/tokens/uiSize/uiSize.spec.md +12 -11
- package/src/tokens/uiSize/uiSize.stories.tsx +103 -0
- package/src/tokens/utility-registry.json +5 -4
- package/dist/node_modules/react-hook-form/dist/index.esm.js +0 -1950
- package/dist/node_modules/react-hook-form/dist/index.esm.js.map +0 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"popover.js","sources":["../../../src/components/Popover/popover.tsx"],"sourcesContent":["// @benchmark-unverified-blanket: file-level retraction per M22 (d) — claims herein not individually URL-cited; treat as unverified visual/usage rumor unless retrofit per-claim. Hook escape preserved.\nimport * as React from \"react\"\nimport * as PopoverPrimitive from \"@radix-ui/react-popover\"\nimport { X as XIcon } from \"lucide-react\"\n\nimport { cn } from \"@/lib/utils\"\nimport { SurfaceHeader, SurfaceBody, SurfaceFooter, COMPACT_HEADER_SLOT } from \"@/design-system/patterns/overlay-surface/overlay-surface\"\nimport { Button } from \"@/design-system/components/Button/button\"\nimport { OVERLAY_SIDE_OFFSET, OVERLAY_COLLISION_PADDING } from \"@/design-system/tokens/elevation/overlay-geometry\"\n\n/**\n * Popover — Radix Popover + 設計系統 token\n *\n * ── 視覺 ──\n * 與 Dialog 對齊:bg-surface-raised / rounded-lg / border-border / elevation-200。\n * layout-space 鎖 md(輕量浮層 header py-tight 保持精簡);ui-size 繼承 page(內部控件對齊觸發點,2026-06-15 改,原 data-density master switch)。\n *\n * ── 結構 ──\n * PopoverContent:外殼(bg / border / radius / shadow / density),無內距。\n * PopoverHeader / PopoverBody / PopoverFooter:消費 overlay-surface pattern\n * 共用的 SurfaceHeader / SurfaceBody / SurfaceFooter primitives(padding SSOT)。\n *\n * ── Header dismiss X(2026-04-20 決策) ──\n * 所有 PopoverHeader 一律附右上 X 按鈕(對齊 Dialog 的 canonical)。Popover 雖是\n * non-modal + click-outside-to-close,但有 header 的 Popover 通常結構化程度高\n * (title / 多區塊),明確的「關閉」入口讓使用者更易退出。無 header 的簡單 Popover\n * 不加 X(click-outside / Esc 即可)。\n */\n\nconst Popover = PopoverPrimitive.Root\nconst PopoverTrigger = PopoverPrimitive.Trigger\nconst PopoverAnchor = PopoverPrimitive.Anchor\nconst PopoverClose = PopoverPrimitive.Close\n\n// AutoFocus canonical(對齊 Dialog / Sheet / Material / Polaris)—\n// 開啟時 focus 落在 body 第一個有意義互動元素,避免 focus 到 close X 觸發 tooltip leak\nconst handlePopoverOpenAutoFocus = (e: Event) => {\n e.preventDefault()\n const content = e.currentTarget as HTMLElement\n const firstBodyTarget = content.querySelector<HTMLElement>(\n '[data-popover-body] input:not([disabled]),[data-popover-body] textarea:not([disabled]),[data-popover-body] select:not([disabled]),[data-popover-body] button:not([disabled]):not([data-dismiss]),input:not([disabled]),textarea:not([disabled]),button:not([disabled]):not([data-dismiss])'\n )\n const firstFooterButton = content.querySelector<HTMLElement>(\n '[data-popover-footer] button:not([disabled]):not([data-dismiss])'\n )\n ;(firstBodyTarget ?? firstFooterButton ?? content).focus({ preventScroll: true })\n}\n\nconst PopoverContent = React.forwardRef<\n React.ElementRef<typeof PopoverPrimitive.Content>,\n React.ComponentPropsWithoutRef<typeof PopoverPrimitive.Content>\n>(({ className, align = \"center\", sideOffset = OVERLAY_SIDE_OFFSET, collisionPadding = OVERLAY_COLLISION_PADDING, onOpenAutoFocus, ...props }, ref) => (\n <PopoverPrimitive.Portal>\n <PopoverPrimitive.Content\n ref={ref}\n align={align}\n sideOffset={sideOffset}\n collisionPadding={collisionPadding}\n // Layout-space lock(2026-06-15 canonical,原 data-density=\"md\" 改為只鎖 layout-space):Popover 是\n // 輕量浮層,header/footer 用 py-tight(layout-space)→ 鎖 layout-space=md 保持精簡 padding;但\n // **ui-size 不鎖、繼承 page** → 內部 field 控件 / dismiss 按鈕隨 page 放大,跟觸發點一致(decouple)。\n // [data-layout-space=\"md\"] 有 reset selector(layoutSpace.css L31)→ lg page 上正確 reset 回 md。\n data-layout-space=\"md\"\n onOpenAutoFocus={onOpenAutoFocus ?? handlePopoverOpenAutoFocus}\n className={cn(\n \"z-50 w-72 rounded-lg border border-border bg-surface-raised text-foreground shadow-[var(--elevation-200)] outline-none\",\n // 2026-05-04 viewport-aware max-h SSOT(從 ProfileCard 升 DS-wide):header/footer 永遠 in-viewport,body 壓縮 scroll\n // 2026-05-05 audit dim 35 補:加 `min-h-0` 完成 M25 chain invariant(flex item default min-h: auto 阻 shrink)\n \"max-h-[var(--radix-popover-content-available-height,100vh)] flex flex-col overflow-hidden min-h-0\",\n \"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 motion-reduce:animate-none\",\n \"data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2\",\n \"origin-[var(--radix-popover-content-transform-origin)]\",\n className\n )}\n {...props}\n />\n </PopoverPrimitive.Portal>\n))\nPopoverContent.displayName = PopoverPrimitive.Content.displayName\n\n// PopoverHeader: SurfaceHeader + Close X(對齊 Dialog 的 canonical,見 docblock)\n// justify-between 讓 children 與 Close 分左右。\ninterface PopoverHeaderProps extends React.HTMLAttributes<HTMLDivElement> {\n /**\n * 隱藏右上 close X(預設 false,顯示)。\n * Coachmark / Tour 類 composition 用 Skip / Done 自管 close,不需 X。\n */\n hideClose?: boolean\n}\n\nconst PopoverHeader = React.forwardRef<HTMLDivElement, PopoverHeaderProps>(\n ({ className, children, hideClose = false, ...props }, ref) => (\n // Popover lightweight chrome canonical(2026-05-04 重思 v2;2026-06-16 slot 改衍生 + 收斂 SSOT):\n // 走 `COMPACT_HEADER_SLOT`(= calc(--font-body-size×1.5) = 14×1.5 = 21 = PopoverTitle text-body line-box,\n // 衍生非寫死,取代舊各自寫死的 1.25rem / 20 巧合值)→ slot = title 行高,title 字級改 slot 自動跟。\n // Header 維持 padding-based:max(21 title, 21 slot) + py-tight(12*2) = 45 → 自然比 Dialog/Sheet 48 輕一級。\n // Q10 穩定:title-only / title+close 都 = title + py 主導(slot=title)。無 min-h / 無 py override。\n <SurfaceHeader\n ref={ref}\n className={cn(\"justify-between\", COMPACT_HEADER_SLOT, className)}\n {...props}\n >\n <div className=\"flex-1 min-w-0\">{children}</div>\n {!hideClose && (\n // Dismiss X = native sm,SurfaceHeader 負 my trick 讓 layout 佔位縮到 slot(21 = title line-box)\n <PopoverPrimitive.Close asChild>\n <Button data-dismiss iconOnly dismiss size=\"sm\" startIcon={XIcon} aria-label=\"關閉\" />\n </PopoverPrimitive.Close>\n )}\n </SurfaceHeader>\n ),\n)\nPopoverHeader.displayName = \"PopoverHeader\"\n\n// PopoverBody / PopoverFooter: wrap SurfaceBody / SurfaceFooter with data-popover-*\n// attributes so handlePopoverOpenAutoFocus 可正確定位 body 內第一個 interactive 元素\n//\n// ── List-as-region 場景(menu / Cmd+K / nav)──\n// 不再提供 `flush` variant(2026-05-01 移除,對齊 DialogBody / SheetBody canonical)。\n// consumer 用 SurfaceBody className override 撤掉 chrome padding + 自管 list outer wrapper:\n// `<PopoverBody className=\"!px-0 !py-0\"><div className=\"py-2\">{items}</div></PopoverBody>`\n// 或乾脆不用 PopoverBody,直接 PopoverContent > 自管 list 結構(naked popover)。\n// 詳 DialogBody comment + `tokens/layoutSpace/layoutSpace.spec.md`「List-as-region in overlay body」\nconst PopoverBody = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(\n ({ ...props }, ref) => <SurfaceBody ref={ref} data-popover-body {...props} />,\n)\nPopoverBody.displayName = \"PopoverBody\"\n\nconst PopoverFooter = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(\n ({ ...props }, ref) => <SurfaceFooter ref={ref} data-popover-footer {...props} />\n)\nPopoverFooter.displayName = \"PopoverFooter\"\n\n// PopoverTitle(2026-04-22 v4 non-modal canonical):`text-body font-medium`(14px)\n// Rationale:Popover / Coachmark 屬 **non-modal 輕量浮層**,跟 density 鎖 md 同源的視覺語言 —\n// chrome 全體輕量:\n// - density 鎖 md(不隨 page 放大)\n// - dismiss X 透過 v5 unbounded trick layout 佔位 24\n// - **title `text-body`(14px)跟 Dialog / Sheet modal 的 body-lg(16px)形成重量級 vs 輕量級視覺區分**\n// 世界級對照:Figma / Notion / Linear / Material 的 popover / filter panel / inline settings\n// 的 header title 多半比 modal dialog title 小一級(16→14 或同比),視覺宣告「此浮層可忽略」\n//\n// Coachmark 同樣消費 PopoverTitle 作 header 小標籤(如 \"新功能介紹\" / \"Tip 1 of 3\"),\n// CoachmarkBody 的主 title 另走 text-body-lg(見 coachmark.tsx CoachmarkBody 主 title <h3>)。\nconst PopoverTitle = React.forwardRef<\n HTMLHeadingElement,\n React.HTMLAttributes<HTMLHeadingElement>\n>(({ className, ...props }, ref) => (\n <h2\n ref={ref}\n className={cn(\"text-body font-medium truncate\", className)}\n {...props}\n />\n))\nPopoverTitle.displayName = \"PopoverTitle\"\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 popoverMeta = {\n component: 'Popover',\n family: null, // non-family composite / overlay / layout\n variants: {\n\n },\n sizes: {\n\n },\n states: ['default'], // 浮層容器 primitive,無自己的 variant/size/disabled 互動 state(spec「為何無 Inspector」段;對齊 separator.tsx meta canonical)\n tokens: {\n bg: ['bg-surface-raised'],\n fg: ['text-foreground'],\n ring: [],\n },\n} as const\n\nexport {\n Popover,\n PopoverTrigger,\n PopoverAnchor,\n PopoverContent,\n PopoverClose,\n PopoverHeader,\n PopoverBody,\n PopoverFooter,\n PopoverTitle,\n}\n"],"names":["XIcon"],"mappings":";;;;;;;;AA6BA,MAAM,UAAU,iBAAiB;AACjC,MAAM,iBAAiB,iBAAiB;AACxC,MAAM,gBAAgB,iBAAiB;AACvC,MAAM,eAAe,iBAAiB;AAItC,MAAM,6BAA6B,CAAC,MAAa;AAC/C,IAAE,eAAA;AACF,QAAM,UAAU,EAAE;AAClB,QAAM,kBAAkB,QAAQ;AAAA,IAC9B;AAAA,EAAA;AAEF,QAAM,oBAAoB,QAAQ;AAAA,IAChC;AAAA,EAAA;AAED,GAAC,mBAAmB,qBAAqB,SAAS,MAAM,EAAE,eAAe,MAAM;AAClF;AAEA,MAAM,iBAAiB,MAAM,WAG3B,CAAC,EAAE,WAAW,QAAQ,UAAU,aAAa,qBAAqB,mBAAmB,2BAA2B,iBAAiB,GAAG,MAAA,GAAS,QAC7I,oBAAC,iBAAiB,QAAjB,EACC,UAAA;AAAA,EAAC,iBAAiB;AAAA,EAAjB;AAAA,IACC;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IAKA,qBAAkB;AAAA,IAClB,iBAAiB,mBAAmB;AAAA,IACpC,WAAW;AAAA,MACT;AAAA;AAAA;AAAA,MAGA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,IAAA;AAAA,IAED,GAAG;AAAA,EAAA;AACN,GACF,CACD;AACD,eAAe,cAAc,iBAAiB,QAAQ;AAYtD,MAAM,gBAAgB,MAAM;AAAA,EAC1B,CAAC,EAAE,WAAW,UAAU,YAAY,OAAO,GAAG,SAAS;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,IAMrD;AAAA,MAAC;AAAA,MAAA;AAAA,QACC;AAAA,QACA,WAAW,GAAG,mBAAmB,qBAAqB,SAAS;AAAA,QAC9D,GAAG;AAAA,QAEJ,UAAA;AAAA,UAAA,oBAAC,OAAA,EAAI,WAAU,kBAAkB,SAAA,CAAS;AAAA,UACzC,CAAC;AAAA,UAEA,oBAAC,iBAAiB,OAAjB,EAAuB,SAAO,MAC7B,UAAA,oBAAC,UAAO,gBAAY,MAAC,UAAQ,MAAC,SAAO,MAAC,MAAK,MAAK,WAAWA,GAAO,cAAW,MAAK,EAAA,CACpF;AAAA,QAAA;AAAA,MAAA;AAAA,IAAA;AAAA;AAIR;AACA,cAAc,cAAc;AAW5B,MAAM,cAAc,MAAM;AAAA,EACxB,CAAC,EAAE,GAAG,SAAS,QAAQ,oBAAC,aAAA,EAAY,KAAU,qBAAiB,MAAE,GAAG,MAAA,CAAO;AAC7E;AACA,YAAY,cAAc;AAE1B,MAAM,gBAAgB,MAAM;AAAA,EAC1B,CAAC,EAAE,GAAG,SAAS,QAAQ,oBAAC,eAAA,EAAc,KAAU,uBAAmB,MAAE,GAAG,MAAA,CAAO;AACjF;AACA,cAAc,cAAc;AAa5B,MAAM,eAAe,MAAM,WAGzB,CAAC,EAAE,WAAW,GAAG,MAAA,GAAS,QAC1B;AAAA,EAAC;AAAA,EAAA;AAAA,IACC;AAAA,IACA,WAAW,GAAG,kCAAkC,SAAS;AAAA,IACxD,GAAG;AAAA,EAAA;AACN,CACD;AACD,aAAa,cAAc;AAIpB,MAAM,cAAc;AAAA,EACzB,WAAW;AAAA,EACX,QAAQ;AAAA;AAAA,EACR,UAAU,CAAA;AAAA,EAGV,OAAO,CAAA;AAAA,EAGP,QAAQ,CAAC,SAAS;AAAA;AAAA,EAClB,QAAQ;AAAA,IACN,IAAI,CAAC,mBAAmB;AAAA,IACxB,IAAI,CAAC,iBAAiB;AAAA,IACtB,MAAM,CAAA;AAAA,EAAC;AAEX;"}
|
|
1
|
+
{"version":3,"file":"popover.js","sources":["../../../src/components/Popover/popover.tsx"],"sourcesContent":["// @benchmark-unverified-blanket: file-level retraction per M22 (d) — claims herein not individually URL-cited; treat as unverified visual/usage rumor unless retrofit per-claim. Hook escape preserved.\nimport * as React from \"react\"\nimport * as PopoverPrimitive from \"@radix-ui/react-popover\"\nimport { X as XIcon } from \"lucide-react\"\n\nimport { cn } from \"@/lib/utils\"\nimport { SurfaceHeader, SurfaceBody, SurfaceFooter, COMPACT_HEADER_SLOT } from \"@/design-system/patterns/overlay-surface/overlay-surface\"\nimport { Button } from \"@/design-system/components/Button/button\"\nimport { OVERLAY_SIDE_OFFSET, OVERLAY_COLLISION_PADDING } from \"@/design-system/tokens/elevation/overlay-geometry\"\n\n/**\n * Popover — Radix Popover + 設計系統 token\n *\n * ── 視覺 ──\n * 與 Dialog 對齊:bg-surface-raised / rounded-lg / border-border / elevation-200。\n * layout-space 鎖 md(輕量浮層 header py-tight 保持精簡);ui-size 繼承 page(內部控件對齊觸發點,2026-06-15 改,原 data-density master switch)。\n *\n * ── 結構 ──\n * PopoverContent:外殼(bg / border / radius / shadow / density),無內距。\n * PopoverHeader / PopoverBody / PopoverFooter:消費 overlay-surface pattern\n * 共用的 SurfaceHeader / SurfaceBody / SurfaceFooter primitives(padding SSOT)。\n *\n * ── Header dismiss X(2026-04-20 決策) ──\n * 所有 PopoverHeader 一律附右上 X 按鈕(對齊 Dialog 的 canonical)。Popover 雖是\n * non-modal + click-outside-to-close,但有 header 的 Popover 通常結構化程度高\n * (title / 多區塊),明確的「關閉」入口讓使用者更易退出。無 header 的簡單 Popover\n * 不加 X(click-outside / Esc 即可)。\n */\n\nconst Popover = PopoverPrimitive.Root\nconst PopoverTrigger = PopoverPrimitive.Trigger\nconst PopoverAnchor = PopoverPrimitive.Anchor\nconst PopoverClose = PopoverPrimitive.Close\n\n// AutoFocus canonical(對齊 Dialog / Sheet / Material / Polaris)—\n// 開啟時 focus 落在 body 第一個有意義互動元素,避免 focus 到 close X 觸發 tooltip leak。\n// 兩段查詢對齊 dialog.tsx handleOpenAutoFocus(2026-07-05 修):舊版單一混合清單按 DOM 序取\n// 第一 match,header 內互動元素(非 data-dismiss)會搶走 body 首元素焦點,且 bare 段漏 select。\n// 改為:先 body-scoped 查詢,查無才退 bare 全域清單(保留 naked popover — consumer 不用\n// PopoverBody 自管結構 — 的 fallback 意圖),最後 footer → content 容器。\nconst handlePopoverOpenAutoFocus = (e: Event) => {\n e.preventDefault()\n const content = e.currentTarget as HTMLElement\n const firstBodyTarget = content.querySelector<HTMLElement>(\n '[data-popover-body] input:not([disabled]),[data-popover-body] textarea:not([disabled]),[data-popover-body] select:not([disabled]),[data-popover-body] button:not([disabled]):not([data-dismiss])'\n )\n const firstBareTarget = content.querySelector<HTMLElement>(\n 'input:not([disabled]),textarea:not([disabled]),select:not([disabled]),button:not([disabled]):not([data-dismiss])'\n )\n const firstFooterButton = content.querySelector<HTMLElement>(\n '[data-popover-footer] button:not([disabled]):not([data-dismiss])'\n )\n ;(firstBodyTarget ?? firstBareTarget ?? firstFooterButton ?? content).focus({ preventScroll: true })\n}\n\n// ── PopoverTitle → aria-labelledby 自動接線(2026-07-05,照 Radix Dialog Title 機制補 DS 層)──\n// Radix Popover 的 content 有 role=\"dialog\",但沒有 Radix Dialog Title 式的自動 accessible name\n// 接線 → 一般 Popover 預設無 accessible name。DS 層以 context 補同機制:PopoverContent 以 useId\n// 建 titleId 供給 children;PopoverTitle 掛載時註冊,PopoverContent 偵測到有 title 註冊才設\n// aria-labelledby。Consumer 自傳 aria-label / aria-labelledby 一律優先。PopoverTitle 亦被\n// DataTable sort / filter panel 等非 PopoverContent 場景裸用(raw SurfaceHeader chrome)—\n// context 為 null 時全部 no-op,行為與舊版一致。\ninterface PopoverTitleContextValue {\n titleId: string\n onTitleMount: () => void\n onTitleUnmount: () => void\n}\n\nconst PopoverTitleContext = React.createContext<PopoverTitleContextValue | null>(null)\n\nconst PopoverContent = React.forwardRef<\n React.ElementRef<typeof PopoverPrimitive.Content>,\n React.ComponentPropsWithoutRef<typeof PopoverPrimitive.Content>\n>(({ className, align = \"center\", sideOffset = OVERLAY_SIDE_OFFSET, collisionPadding = OVERLAY_COLLISION_PADDING, onOpenAutoFocus, \"aria-label\": ariaLabel, \"aria-labelledby\": ariaLabelledBy, ...props }, ref) => {\n const titleId = React.useId()\n const [hasTitle, setHasTitle] = React.useState(false)\n const titleContext = React.useMemo<PopoverTitleContextValue>(\n () => ({ titleId, onTitleMount: () => setHasTitle(true), onTitleUnmount: () => setHasTitle(false) }),\n [titleId],\n )\n return (\n <PopoverPrimitive.Portal>\n <PopoverTitleContext.Provider value={titleContext}>\n <PopoverPrimitive.Content\n ref={ref}\n align={align}\n sideOffset={sideOffset}\n collisionPadding={collisionPadding}\n // Layout-space lock(2026-06-15 canonical,原 data-density=\"md\" 改為只鎖 layout-space):Popover 是\n // 輕量浮層,header/footer 用 py-tight(layout-space)→ 鎖 layout-space=md 保持精簡 padding;但\n // **ui-size 不鎖、繼承 page** → 內部 field 控件 / dismiss 按鈕隨 page 放大,跟觸發點一致(decouple)。\n // [data-layout-space=\"md\"] 有 reset selector(layoutSpace.css L31)→ lg page 上正確 reset 回 md。\n data-layout-space=\"md\"\n onOpenAutoFocus={onOpenAutoFocus ?? handlePopoverOpenAutoFocus}\n aria-label={ariaLabel}\n // accessible name 自動接線:consumer 自傳優先;無 aria-label 且有 PopoverTitle 註冊才掛 titleId\n aria-labelledby={ariaLabelledBy ?? (ariaLabel == null && hasTitle ? titleId : undefined)}\n className={cn(\n \"z-50 w-72 rounded-lg border border-border bg-surface-raised text-foreground shadow-[var(--elevation-200)] outline-none\",\n // 2026-05-04 viewport-aware max-h SSOT(從 ProfileCard 升 DS-wide):header/footer 永遠 in-viewport,body 壓縮 scroll\n // 2026-05-05 audit dim 35 補:加 `min-h-0` 完成 M25 chain invariant(flex item default min-h: auto 阻 shrink)\n \"max-h-[var(--radix-popover-content-available-height,100vh)] flex flex-col overflow-hidden min-h-0\",\n \"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 motion-reduce:animate-none\",\n \"data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2\",\n \"origin-[var(--radix-popover-content-transform-origin)]\",\n className\n )}\n {...props}\n />\n </PopoverTitleContext.Provider>\n </PopoverPrimitive.Portal>\n )\n})\nPopoverContent.displayName = PopoverPrimitive.Content.displayName\n\n// PopoverHeader: SurfaceHeader + Close X(對齊 Dialog 的 canonical,見 docblock)\n// justify-between 讓 children 與 Close 分左右。\ninterface PopoverHeaderProps extends React.HTMLAttributes<HTMLDivElement> {\n /**\n * 隱藏右上 close X(預設 false,顯示)。\n * Coachmark / Tour 類 composition 用 Skip / Done 自管 close,不需 X。\n */\n hideClose?: boolean\n}\n\nconst PopoverHeader = React.forwardRef<HTMLDivElement, PopoverHeaderProps>(\n ({ className, children, hideClose = false, ...props }, ref) => (\n // Popover lightweight chrome canonical(2026-05-04 重思 v2;2026-06-16 slot 改衍生 + 收斂 SSOT):\n // 走 `COMPACT_HEADER_SLOT`(= calc(--font-body-size×1.5) = 14×1.5 = 21 = PopoverTitle text-body line-box,\n // 衍生非寫死,取代舊各自寫死的 1.25rem / 20 巧合值)→ slot = title 行高,title 字級改 slot 自動跟。\n // Header 維持 padding-based:max(21 title, 21 slot) + py-tight(12*2) = 45 → 自然比 Dialog/Sheet 48 輕一級。\n // Q10 穩定:title-only / title+close 都 = title + py 主導(slot=title)。無 min-h / 無 py override。\n <SurfaceHeader\n ref={ref}\n className={cn(\"justify-between\", COMPACT_HEADER_SLOT, className)}\n {...props}\n >\n <div className=\"flex-1 min-w-0\">{children}</div>\n {!hideClose && (\n // Dismiss X = native sm,SurfaceHeader 負 my trick 讓 layout 佔位縮到 slot(21 = title line-box)\n <PopoverPrimitive.Close asChild>\n <Button data-dismiss iconOnly dismiss size=\"sm\" startIcon={XIcon} aria-label=\"關閉\" />\n </PopoverPrimitive.Close>\n )}\n </SurfaceHeader>\n ),\n)\nPopoverHeader.displayName = \"PopoverHeader\"\n\n// PopoverBody / PopoverFooter: wrap SurfaceBody / SurfaceFooter with data-popover-*\n// attributes so handlePopoverOpenAutoFocus 可正確定位 body 內第一個 interactive 元素\n//\n// ── List-as-region 場景(menu / Cmd+K / nav)──\n// 不再提供 `flush` variant(2026-05-01 移除,對齊 DialogBody / SheetBody canonical)。\n// consumer 用 SurfaceBody className override 撤掉 chrome padding + 自管 list outer wrapper:\n// `<PopoverBody className=\"!px-0 !py-0\"><div className=\"py-2\">{items}</div></PopoverBody>`\n// 或乾脆不用 PopoverBody,直接 PopoverContent > 自管 list 結構(naked popover)。\n// 詳 DialogBody comment + `tokens/layoutSpace/layoutSpace.spec.md`「List-as-region in overlay body」\nconst PopoverBody = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(\n ({ ...props }, ref) => <SurfaceBody ref={ref} data-popover-body {...props} />,\n)\nPopoverBody.displayName = \"PopoverBody\"\n\nconst PopoverFooter = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(\n ({ ...props }, ref) => <SurfaceFooter ref={ref} data-popover-footer {...props} />\n)\nPopoverFooter.displayName = \"PopoverFooter\"\n\n// PopoverTitle(2026-04-22 v4 non-modal canonical):`text-body font-medium`(14px)\n// Rationale:Popover / Coachmark 屬 **non-modal 輕量浮層**,跟 density 鎖 md 同源的視覺語言 —\n// chrome 全體輕量:\n// - layout-space 鎖 md(ui-size 繼承 page,2026-06-15)\n// - dismiss X 走 COMPACT_HEADER_SLOT 佔位 21(= title text-body line-box;見 PopoverHeader docblock)\n// - **title `text-body`(14px)跟 Dialog / Sheet modal 的 body-lg(16px)形成重量級 vs 輕量級視覺區分**\n// 世界級對照:Figma / Notion / Linear / Material 的 popover / filter panel / inline settings\n// 的 header title 多半比 modal dialog title 小一級(16→14 或同比),視覺宣告「此浮層可忽略」\n//\n// Coachmark 同樣消費 PopoverTitle 作 header 小標籤(如 \"新功能介紹\" / \"Tip 1 of 3\"),\n// CoachmarkBody 的主 title 另走 text-body-lg(見 coachmark.tsx CoachmarkBody 主 title <h3>)。\nconst PopoverTitle = React.forwardRef<\n HTMLHeadingElement,\n React.HTMLAttributes<HTMLHeadingElement>\n>(({ className, id, ...props }, ref) => {\n const titleCtx = React.useContext(PopoverTitleContext)\n // 掛載時向 PopoverContent 註冊 → content 自動設 aria-labelledby(見 PopoverTitleContext docblock)。\n // consumer 自傳 id 時不註冊:auto 接線指向 context titleId,自訂 id 下引用會斷 → aria 由 consumer 自管。\n const shouldRegister = id == null && titleCtx != null\n const onTitleMount = titleCtx?.onTitleMount\n const onTitleUnmount = titleCtx?.onTitleUnmount\n React.useLayoutEffect(() => {\n if (!shouldRegister) return\n onTitleMount?.()\n return onTitleUnmount\n }, [shouldRegister, onTitleMount, onTitleUnmount])\n return (\n <h2\n ref={ref}\n id={id ?? titleCtx?.titleId}\n className={cn(\"text-body font-medium truncate\", className)}\n {...props}\n />\n )\n})\nPopoverTitle.displayName = \"PopoverTitle\"\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 popoverMeta = {\n component: 'Popover',\n family: null, // non-family composite / overlay / layout\n variants: {\n\n },\n sizes: {\n\n },\n states: ['default'], // 浮層容器 primitive,無自己的 variant/size/disabled 互動 state(spec「為何無 Inspector」段;對齊 separator.tsx meta canonical)\n tokens: {\n bg: ['bg-surface-raised'],\n fg: ['text-foreground'],\n ring: [],\n },\n} as const\n\nexport {\n Popover,\n PopoverTrigger,\n PopoverAnchor,\n PopoverContent,\n PopoverClose,\n PopoverHeader,\n PopoverBody,\n PopoverFooter,\n PopoverTitle,\n}\n"],"names":["XIcon"],"mappings":"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"}
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
2
|
+
* Public 元件(2026-07-04 Q8 拍板升 public,原 @internal 標記移除)。
|
|
3
|
+
* 理由:avatar.spec.md「hover 必出 ProfileCard」DS-wide canonical 要求 consumer 直接
|
|
4
|
+
* import ProfileCard + ProfileCardDefaultActions 組 HoverCard — 按 ui-development.md
|
|
5
|
+
* 唯一判準(DS 想讓 consumer 直接用 = public)internal 標記是 governance bug;
|
|
6
|
+
* 世界級對照:Atlassian ProfileCard 同為 public standalone。
|
|
4
7
|
*/
|
|
5
8
|
import * as React from 'react';
|
|
6
9
|
import { type AvatarData } from '../../components/Avatar/avatar';
|
|
@@ -23,7 +26,12 @@ import { type AvatarData } from '../../components/Avatar/avatar';
|
|
|
23
26
|
*
|
|
24
27
|
* Chat + Audio call 是 **default**,不是 **only**——consumer 可覆寫,但需有明確理由。
|
|
25
28
|
*/
|
|
26
|
-
export declare const ProfileCardDefaultActions: (
|
|
29
|
+
export declare const ProfileCardDefaultActions: ({ onChat, onCall, }: {
|
|
30
|
+
/** Chat 按鈕 handler(2026-07-06 user 拍板:預設鈕必可接線,未傳 = dev-warn;PersonData.onChat 自動 forward) */
|
|
31
|
+
onChat?: () => void;
|
|
32
|
+
/** Audio call 按鈕 handler(ChevronDown 下拉行為留 v2,先接主 callback) */
|
|
33
|
+
onCall?: () => void;
|
|
34
|
+
}) => import("react/jsx-runtime").JSX.Element;
|
|
27
35
|
type StatusType = 'online' | 'away' | 'busy' | 'offline';
|
|
28
36
|
/**
|
|
29
37
|
* ProfileCard SSOT — 預設 field keys(現行 v15.7:default 只 `id` + `employeeNumber`;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"profile-card.d.ts","sourceRoot":"","sources":["../../../src/components/ProfileCard/profile-card.tsx"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"profile-card.d.ts","sourceRoot":"","sources":["../../../src/components/ProfileCard/profile-card.tsx"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,KAAK,KAAK,MAAM,OAAO,CAAA;AAG9B,OAAO,EAAU,KAAK,UAAU,EAAE,MAAM,0CAA0C,CAAA;AAMlF;;;;;;;;;;;;;;;;;;GAkBG;AACH,eAAO,MAAM,yBAAyB,GAAI,qBAGvC;IACD,6FAA6F;IAC7F,MAAM,CAAC,EAAE,MAAM,IAAI,CAAA;IACnB,+DAA+D;IAC/D,MAAM,CAAC,EAAE,MAAM,IAAI,CAAA;CACpB,4CAeA,CAAA;AAqBD,KAAK,UAAU,GAAG,QAAQ,GAAG,MAAM,GAAG,MAAM,GAAG,SAAS,CAAA;AAiBxD;;;;;;;;;;;;;;;;;;;;;GAqBG;AAIH,eAAO,MAAM,2BAA2B,mCAAoC,CAAA;AAC5E,KAAK,0BAA0B,GAAG,OAAO,2BAA2B,CAAC,MAAM,CAAC,CAAA;AAS5E,MAAM,WAAW,gBAAiB,SAAQ,KAAK,CAAC,cAAc,CAAC,cAAc,CAAC;IAC5E,IAAI,EAAE,MAAM,CAAA;IACZ,MAAM,CAAC,EAAE,UAAU,CAAA;IACnB,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,MAAM,CAAC,EAAE,UAAU,CAAA;IACnB,aAAa,CAAC,EAAE,KAAK,CAAC,SAAS,CAAA;IAC/B,OAAO,CAAC,EAAE,KAAK,CAAC,SAAS,CAAA;IACzB;;;;;OAKG;IACH,MAAM,CAAC,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,KAAK,CAAC,SAAS,CAAA;KAAE,EAAE,CAAA;IACpD;;;OAGG;IACH,kBAAkB,CAAC,EAAE,OAAO,CAAC,MAAM,CAAC,0BAA0B,EAAE,KAAK,CAAC,SAAS,CAAC,CAAC,CAAA;IACjF,UAAU,CAAC,EAAE,MAAM,IAAI,CAAA;IACvB,aAAa,CAAC,EAAE,MAAM,CAAA;CACvB;AAGD,QAAA,MAAM,WAAW,yFAuKhB,CAAA;AAKD,eAAO,MAAM,YAAY;;;;;;;;;;;CAef,CAAA;AAEV,OAAO,EAAE,WAAW,EAAE,CAAA"}
|
|
@@ -7,10 +7,20 @@ import { Button } from "../Button/button.js";
|
|
|
7
7
|
import { DescriptionList, DescriptionItem } from "../DescriptionList/description-list.js";
|
|
8
8
|
import { ScrollArea } from "../ScrollArea/scroll-area.js";
|
|
9
9
|
import { ItemContent } from "../../patterns/element-anatomy/item-anatomy.js";
|
|
10
|
-
const ProfileCardDefaultActions = (
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
10
|
+
const ProfileCardDefaultActions = ({
|
|
11
|
+
onChat,
|
|
12
|
+
onCall
|
|
13
|
+
}) => {
|
|
14
|
+
if (process.env.NODE_ENV !== "production" && !onChat && !onCall) {
|
|
15
|
+
console.warn(
|
|
16
|
+
"[DS ProfileCard] ProfileCardDefaultActions 未接 onChat/onCall——按鈕點擊無反應。請傳入 callback(PersonData.onChat/onCall 會自動 forward)或自組 actions。"
|
|
17
|
+
);
|
|
18
|
+
}
|
|
19
|
+
return /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
20
|
+
/* @__PURE__ */ jsx(Button, { variant: "tertiary", size: "sm", startIcon: MessageCircle, onClick: onChat, children: "Chat" }),
|
|
21
|
+
/* @__PURE__ */ jsx(Button, { variant: "tertiary", size: "sm", startIcon: Phone, endIcon: ChevronDown, onClick: onCall, children: "Audio call" })
|
|
22
|
+
] });
|
|
23
|
+
};
|
|
14
24
|
const AVATAR_SIZE = 64;
|
|
15
25
|
const STATUS_DOT_COLOR = {
|
|
16
26
|
online: "var(--status-online)",
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"profile-card.js","sources":["../../../src/components/ProfileCard/profile-card.tsx"],"sourcesContent":["/**\n * @internal — DS-internal 單元(per `.claude/rules/ui-development.md` Public vs Internal canonical;spec frontmatter `isInternal`)。\n * 不進 root barrel front-door;由 PeoplePicker(person-display)wrap 消費,end-user app 請用 wrapper 元件。\n */\n// @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 { MessageCircle, Phone, ChevronDown } from 'lucide-react'\nimport { cn } from '@/lib/utils'\nimport { Avatar, type AvatarData } from '@/design-system/components/Avatar/avatar'\nimport { Button } from '@/design-system/components/Button/button'\nimport { DescriptionList, DescriptionItem } from '@/design-system/components/DescriptionList/description-list'\nimport { ScrollArea } from '@/design-system/components/ScrollArea/scroll-area'\nimport { ItemContent } from '@/design-system/patterns/element-anatomy/item-anatomy'\n\n/**\n * ProfileCardDefaultActions — canonical 預設 action 組合\n *\n * ── 為什麼需要 export ──\n * ProfileCard 的 action 列在世界級 chat/contact app 是「關係型快速動作」的 canonical\n * (Slack / iMessage / LinkedIn / Figma 皆如此):**Chat / Message + Audio call**。\n * 跨 consumer(avatar.principles / profile-card.stories / future product code)應該用\n * 同一組預設,避免每個範例各自發明 action 組合,讓 reader 誤以為 action 會隨情境自動變。\n *\n * ── 使用方式(hover context 必含 onViewMore,見 profile-card.spec.md 「View more」節)──\n * <ProfileCard name=\"...\" actions={<ProfileCardDefaultActions />} onViewMore={...} />\n *\n * ── 何時要換成自訂 action ──\n * - Single-action 情境(只要「傳訊息」)→ consumer 傳 `<Button>傳訊息</Button>`\n * - 特定情境的 action(管理員「撤銷邀請」/ HR「離職管理」)→ consumer 自訂\n * - 非人員關係動作(「訂購此商品」)→ 根本不應該用 ProfileCard\n *\n * Chat + Audio call 是 **default**,不是 **only**——consumer 可覆寫,但需有明確理由。\n */\nexport const ProfileCardDefaultActions = () => (\n <>\n <Button variant=\"tertiary\" size=\"sm\" startIcon={MessageCircle}>Chat</Button>\n <Button variant=\"tertiary\" size=\"sm\" startIcon={Phone} endIcon={ChevronDown}>Audio call</Button>\n </>\n)\n\n/**\n * ProfileCard — 人員 HoverCard 的內容元件\n *\n * ── Status 對齊 Avatar presence canonical(2026-04-20) ──\n * status type = `online | away | busy | offline`,跟 Avatar 的 presence prop 對齊\n * (世界級 Slack / Teams / Discord term);dot 色走 `--status-*` semantic token\n * (獨立於 success/error/warning,避免語義衝突)。\n *\n * ── Avatar 對齊 ──\n * 跟 FileItem rich 統一:右側 text column 用 justify-center + minHeight=avatar。\n * 短文字置中於 avatar,長文字(多行名字)自然撐高。\n *\n * ── Info fields ──\n * Status message / ID / Employee number 等唯讀屬性全部用 DescriptionList 家族\n * 承載(不手刻 dt/dd),canonical 由 DS primitive own。\n */\n\nconst AVATAR_SIZE = 64\n\ntype StatusType = 'online' | 'away' | 'busy' | 'offline'\n\n// Presence semantic tokens(見 color/semantic.css)——跟 Avatar status dot 共用\nconst STATUS_DOT_COLOR: Record<StatusType, string> = {\n online: 'var(--status-online)',\n away: 'var(--status-away)',\n busy: 'var(--status-busy)',\n offline: 'var(--status-offline)',\n}\n\nconst STATUS_LABEL: Record<StatusType, string> = {\n online: 'Online',\n away: 'Away',\n busy: 'Busy',\n offline: 'Offline',\n}\n\n/**\n * ProfileCard SSOT — 預設 field keys(現行 v15.7:default 只 `id` + `employeeNumber`;\n * v11 always-render 為歷史脈絡,已退役 — 見下方 2026-05-07 註解 + render 處 v12 註解)\n *\n * ── 為什麼 SSOT ──\n * User explicit rule:「所有 namecard 預設顯示的資訊都是要一樣完整的」。前 v10 props\n * 全 optional + body conditional render → consumer 漏傳 fields 視覺缺 section,每個範例\n * 各自不一致(同 person 在 DataTable / PeoplePicker / avatar.principles 看起來不同)。\n *\n * v11 canonical(歷史):ProfileCard 當時 always renders 4 default sections — 缺 data → 顯\n * `EMPTY_PLACEHOLDER`(\"—\")。現行:default fields 縮為 2(v15.7)+ status 條件 render(v12),\n * 「視覺結構 SSOT 在此元件,不依賴 consumer」原則不變。\n *\n * ── 對齊 world-class ──\n * Slack profile card / Linear member card / Notion person card / GitHub user card / Figma user card\n * 都是 fixed schema(role / email / location / department / department / pronouns 等),\n * 不會因為某 user 沒填 phone 整個 phone field 不見 — 缺 = 顯 `Not set` 或留白。\n *\n * ── 為什麼 placeholder 不 hide ──\n * Hide → consumer 不知道少傳 → 視覺漂移;Placeholder → 永遠看到「該欄該有」+ dev-warn 提示\n * consumer 補資料,自動防漂移(M19 ensure-canonical 對齊)。\n */\n// **2026-05-07 v15.7 user directive**:default render 只 `id` + `employeeNumber` 兩個。\n// Email / Phone / Department / Location 等其他 description 一律 opt-in by consumer 透過\n// `fields` array prop。對齊 user 明確「應該確保所有都只有這兩個,因為我並沒有要求你要選其他的」。\nexport const NAMECARD_DEFAULT_FIELD_KEYS = ['id', 'employeeNumber'] as const\ntype ProfileCardDefaultFieldKey = typeof NAMECARD_DEFAULT_FIELD_KEYS[number]\n\nconst DEFAULT_FIELD_LABEL: Record<ProfileCardDefaultFieldKey, string> = {\n id: 'ID',\n employeeNumber: 'Employee number',\n}\n\nconst FIELD_PLACEHOLDER = '—'\n\nexport interface ProfileCardProps extends React.HTMLAttributes<HTMLDivElement> {\n name: string\n avatar?: AvatarData\n subtitle?: string\n status?: StatusType\n statusMessage?: React.ReactNode\n actions?: React.ReactNode\n /**\n * Consumer 傳的 field 資料(partial)。預設 keys 走 `NAMECARD_DEFAULT_FIELD_KEYS` —\n * 只有 id / employeeNumber 兩個 default field 永遠 render(缺資料顯 `—`);email / phone /\n * department / location 等其他欄位一律 opt-in by consumer 透過本 prop 傳入。Consumer 想新增\n * 自訂 field 直接傳入(在 default 之後 append),想 override default key value 也直接傳。\n */\n fields?: { label: string; value: React.ReactNode }[]\n /**\n * Default field 的真實值。Object key = NAMECARD_DEFAULT_FIELD_KEYS 之一。\n * 缺 key → render placeholder。Dev mode 會 console.warn 提醒消費者補資料。\n */\n defaultFieldValues?: Partial<Record<ProfileCardDefaultFieldKey, React.ReactNode>>\n onViewMore?: () => void\n viewMoreLabel?: string\n}\n\n// code-quality-allow: long-function — foundational composite main body — 拆 sub-fn 會複雜化 local state / ref / context binding\nconst ProfileCard = React.forwardRef<HTMLDivElement, ProfileCardProps>(\n (\n {\n name,\n avatar,\n subtitle,\n status,\n statusMessage,\n actions,\n fields,\n defaultFieldValues,\n onViewMore,\n viewMoreLabel = 'View more',\n className,\n ...props\n },\n ref,\n ) => {\n // v12 canonical:default fields 永遠 render(缺資料顯 placeholder),consumer 自訂\n // fields 在 default 之後 append。Status section 為條件 render(status undefined →\n // 整 block 隱藏,見下方 render 處)— v11「永遠 render + placeholder」已退役。\n //\n // **Dedup canonical(2026-05-07 v15.8 fix Bug E)**:consumer 的 `fields` array 若含\n // label 撞 default(eg. 「ID」「Employee number」),consumer 值 win — defaults 那一行\n // 跳過(否則 same label 連 render 兩次,如 default placeholder `—` + consumer 真值)。\n // 這是遷移期 forgiving 行為:DEV warn 提示應改用 `defaultFieldValues`,但 production\n // 不破壞既有 consumer。對齊 React `key` 唯一性 + Linear / Slack profile card 一 label\n // 一 row idiom。\n const allFields = React.useMemo(() => {\n const consumerLabels = new Set((fields ?? []).map((f) => f.label))\n const defaults = NAMECARD_DEFAULT_FIELD_KEYS\n .map((key) => ({\n label: DEFAULT_FIELD_LABEL[key],\n value: defaultFieldValues?.[key] ?? FIELD_PLACEHOLDER,\n }))\n .filter((d) => !consumerLabels.has(d.label))\n return fields && fields.length > 0 ? [...defaults, ...fields] : defaults\n }, [defaultFieldValues, fields])\n\n // Dev warn:consumer 透過 `fields` 傳 default key label(legacy pattern)→ 應改 `defaultFieldValues`\n if (process.env.NODE_ENV !== 'production' && fields) {\n const legacyEntry = fields.find((f) =>\n Object.values(DEFAULT_FIELD_LABEL).includes(f.label as string),\n )\n if (legacyEntry) {\n // eslint-disable-next-line no-console\n console.warn(\n `[ProfileCard] \"${name}\":legacy pattern — fields[].label=\"${legacyEntry.label}\" ` +\n `is a default field. Migrate to defaultFieldValues={{ id, employeeNumber }} prop ` +\n `to align with NAMECARD_DEFAULT_FIELD_KEYS canonical.`,\n )\n }\n }\n\n // Dev mode warn:consumer 沒傳 default field 任何 key → 提示補完(避免漂移成 placeholder-only)\n if (process.env.NODE_ENV !== 'production' && !defaultFieldValues) {\n // eslint-disable-next-line no-console\n console.warn(\n `[ProfileCard] \"${name}\":no defaultFieldValues passed — sections will render placeholders. ` +\n `Pass at least { id, employeeNumber } via defaultFieldValues prop. ` +\n `For other description items (email/phone/department/location etc),use \\`fields\\` prop array.`,\n )\n }\n\n // Layout canonical(2026-04-23):Header + Actions 固定上,Body(status + fields)可捲動,\n // View more 固定下。**ProfileCard 自己約束高度**,不依賴 consumer HoverCardContent 設 flex:\n // - `max-h-[var(--radix-hover-card-content-available-height,...)]`:HoverCard / Popover\n // context 自動繼承 Radix viewport-aware 變數;standalone 落到 100vh fallback\n // - 內部 `flex flex-col + overflow-hidden`:Header(shrink-0)+ Body(flex-1 min-h-0 ScrollArea)\n // + Footer(shrink-0)三層 chrome\n // 世界級對照:Slack / Linear / GitHub / Notion hover-profile popover 皆此 chrome pattern。\n return (\n <div\n ref={ref}\n className={cn(\n 'w-[320px] flex flex-col overflow-hidden',\n 'max-h-[var(--radix-hover-card-content-available-height,var(--radix-popover-content-available-height,100vh))]',\n className,\n )}\n {...props}\n >\n {/* ── HEADER(固定): profile + actions ── */}\n <div className=\"shrink-0 flex flex-col\">\n <div className=\"flex items-start gap-3 px-4 py-3\">\n <Avatar\n src={avatar?.src}\n alt={avatar?.alt ?? name}\n color={avatar?.color}\n size={AVATAR_SIZE}\n status={status}\n className=\"shrink-0\"\n />\n {/* ProfileCard typography:label body-lg(16/1.5) + desc body(14/1.5) = reading mode + size=\"lg\"。\n labelClassName escape hatch 加 font-medium(card context 語意)+ labelTruncate=false 允許 wrap。 */}\n <ItemContent\n label={name}\n description={subtitle}\n mode=\"reading\"\n size=\"lg\"\n labelTruncate={false}\n labelClassName=\"text-body-lg font-medium text-foreground\"\n className=\"justify-center\"\n style={{ minHeight: AVATAR_SIZE }}\n />\n </div>\n\n {/* Action buttons — 均分空間 + 填滿格子(canonical):多個 action 等寬瓜分容器,\n 單一 action 也撐滿容器。`grid grid-flow-col auto-cols-fr` + `[&>*]:w-full`。\n 世界級對照:iOS contact card / macOS contact / LinkedIn profile card 的 action row。 */}\n {actions && (\n <div className=\"grid grid-flow-col auto-cols-fr gap-2 px-4 pb-3 [&>*]:w-full\">\n {actions}\n </div>\n )}\n </div>\n\n {/* ── BODY(可捲動,v12 status-conditional 2026-05-14):status + fields ──\n **v12 rule**(per user 拍板「不應該顯示『狀態沒有被設定』,production 每 user 一定有\n presence state,undefined 頂多是 loading transient 還沒讀到」):status undefined →\n 隱藏整 status badge + status message block(loading 期間 skip),禁 render「Status not\n set」這種 placeholder(語義錯,user presence 不會「沒設定」)。**ProfileCard-specific 不外推\n 至 DS 其他元件**(FileItem / DescriptionList / DataTable cell 各自 placeholder 邏輯\n unrelated)。Fields section 仍 always-render(info schema 性質)。 */}\n <ScrollArea className=\"flex-1 min-h-0 border-t border-divider\">\n {/* Status section:`status` defined 才 render(v12 conditional canonical) */}\n {status && (\n <div className=\"px-4 py-3 flex flex-col gap-3\">\n <div className=\"flex items-center gap-2 px-3 py-2 bg-muted rounded-md\">\n <span\n className=\"w-2.5 h-2.5 rounded-full shrink-0\"\n style={{ backgroundColor: STATUS_DOT_COLOR[status] }}\n />\n <span className=\"text-body\">{STATUS_LABEL[status]}</span>\n </div>\n {/* Status message — 只在 status defined 才 render(語意配對:status badge + status\n message 是一組;沒 status 就沒 status message)。缺 statusMessage 顯 placeholder。 */}\n <DescriptionList>\n <DescriptionItem label=\"Status message\">\n {statusMessage ?? <span className=\"text-fg-muted\">{FIELD_PLACEHOLDER}</span>}\n </DescriptionItem>\n </DescriptionList>\n </div>\n )}\n\n {/* Fields section:status defined 才有 border-t separator;無 status section 時去除 border-t(因 ScrollArea 起點已有上方 border-t,不重疊) */}\n <div className={cn('px-4 py-3', status && 'border-t border-divider')}>\n <DescriptionList cols={2}>\n {allFields.map((f) => (\n <DescriptionItem key={f.label} label={f.label}>\n {f.value === FIELD_PLACEHOLDER\n ? <span className=\"text-fg-muted\">{FIELD_PLACEHOLDER}</span>\n : f.value}\n </DescriptionItem>\n ))}\n </DescriptionList>\n </div>\n </ScrollArea>\n\n {/* ── FOOTER(固定): View more,py-3 canonical(12px,比一般 link 按鈕多呼吸) ── */}\n {onViewMore && (\n <div className=\"shrink-0 border-t border-divider px-4 py-3\">\n <Button variant=\"link\" size=\"sm\" onClick={onViewMore} className=\"w-full\">{viewMoreLabel}</Button>\n </div>\n )}\n </div>\n )\n },\n)\nProfileCard.displayName = 'ProfileCard'\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 nameCardMeta = {\n component: 'ProfileCard',\n family: null, // non-family composite / overlay / layout\n variants: {\n\n },\n sizes: {\n\n },\n states: [], // 唯讀 person-info template,無自身互動 state(hover/active/focus/disabled 屬子 Button/Avatar)\n tokens: {\n bg: ['bg-muted'],\n fg: ['text-foreground'],\n ring: [],\n },\n} as const\n\nexport { ProfileCard }\n"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"profile-card.js","sources":["../../../src/components/ProfileCard/profile-card.tsx"],"sourcesContent":["/**\n * Public 元件(2026-07-04 Q8 拍板升 public,原 @internal 標記移除)。\n * 理由:avatar.spec.md「hover 必出 ProfileCard」DS-wide canonical 要求 consumer 直接\n * import ProfileCard + ProfileCardDefaultActions 組 HoverCard — 按 ui-development.md\n * 唯一判準(DS 想讓 consumer 直接用 = public)internal 標記是 governance bug;\n * 世界級對照:Atlassian ProfileCard 同為 public standalone。\n */\n// @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 { MessageCircle, Phone, ChevronDown } from 'lucide-react'\nimport { cn } from '@/lib/utils'\nimport { Avatar, type AvatarData } from '@/design-system/components/Avatar/avatar'\nimport { Button } from '@/design-system/components/Button/button'\nimport { DescriptionList, DescriptionItem } from '@/design-system/components/DescriptionList/description-list'\nimport { ScrollArea } from '@/design-system/components/ScrollArea/scroll-area'\nimport { ItemContent } from '@/design-system/patterns/element-anatomy/item-anatomy'\n\n/**\n * ProfileCardDefaultActions — canonical 預設 action 組合\n *\n * ── 為什麼需要 export ──\n * ProfileCard 的 action 列在世界級 chat/contact app 是「關係型快速動作」的 canonical\n * (Slack / iMessage / LinkedIn / Figma 皆如此):**Chat / Message + Audio call**。\n * 跨 consumer(avatar.principles / profile-card.stories / future product code)應該用\n * 同一組預設,避免每個範例各自發明 action 組合,讓 reader 誤以為 action 會隨情境自動變。\n *\n * ── 使用方式(hover context 必含 onViewMore,見 profile-card.spec.md 「View more」節)──\n * <ProfileCard name=\"...\" actions={<ProfileCardDefaultActions />} onViewMore={...} />\n *\n * ── 何時要換成自訂 action ──\n * - Single-action 情境(只要「傳訊息」)→ consumer 傳 `<Button>傳訊息</Button>`\n * - 特定情境的 action(管理員「撤銷邀請」/ HR「離職管理」)→ consumer 自訂\n * - 非人員關係動作(「訂購此商品」)→ 根本不應該用 ProfileCard\n *\n * Chat + Audio call 是 **default**,不是 **only**——consumer 可覆寫,但需有明確理由。\n */\nexport const ProfileCardDefaultActions = ({\n onChat,\n onCall,\n}: {\n /** Chat 按鈕 handler(2026-07-06 user 拍板:預設鈕必可接線,未傳 = dev-warn;PersonData.onChat 自動 forward) */\n onChat?: () => void\n /** Audio call 按鈕 handler(ChevronDown 下拉行為留 v2,先接主 callback) */\n onCall?: () => void\n}) => {\n // Dev-mode warning(對齊本檔 defaultFieldValues dev-warn 先例):預設 actions 沒接 handler\n // = 按鈕點了永遠沒反應(2026-07-06 D4 finding);production 必傳 callback 或自組 actions。\n if (process.env.NODE_ENV !== 'production' && !onChat && !onCall) {\n // eslint-disable-next-line no-console\n console.warn(\n '[DS ProfileCard] ProfileCardDefaultActions 未接 onChat/onCall——按鈕點擊無反應。請傳入 callback(PersonData.onChat/onCall 會自動 forward)或自組 actions。'\n )\n }\n return (\n <>\n <Button variant=\"tertiary\" size=\"sm\" startIcon={MessageCircle} onClick={onChat}>Chat</Button>\n <Button variant=\"tertiary\" size=\"sm\" startIcon={Phone} endIcon={ChevronDown} onClick={onCall}>Audio call</Button>\n </>\n )\n}\n\n/**\n * ProfileCard — 人員 HoverCard 的內容元件\n *\n * ── Status 對齊 Avatar presence canonical(2026-04-20) ──\n * status type = `online | away | busy | offline`,跟 Avatar 的 presence prop 對齊\n * (世界級 Slack / Teams / Discord term);dot 色走 `--status-*` semantic token\n * (獨立於 success/error/warning,避免語義衝突)。\n *\n * ── Avatar 對齊 ──\n * 跟 FileItem rich 統一:右側 text column 用 justify-center + minHeight=avatar。\n * 短文字置中於 avatar,長文字(多行名字)自然撐高。\n *\n * ── Info fields ──\n * Status message / ID / Employee number 等唯讀屬性全部用 DescriptionList 家族\n * 承載(不手刻 dt/dd),canonical 由 DS primitive own。\n */\n\nconst AVATAR_SIZE = 64\n\ntype StatusType = 'online' | 'away' | 'busy' | 'offline'\n\n// Presence semantic tokens(見 color/semantic.css)——跟 Avatar status dot 共用\nconst STATUS_DOT_COLOR: Record<StatusType, string> = {\n online: 'var(--status-online)',\n away: 'var(--status-away)',\n busy: 'var(--status-busy)',\n offline: 'var(--status-offline)',\n}\n\nconst STATUS_LABEL: Record<StatusType, string> = {\n online: 'Online',\n away: 'Away',\n busy: 'Busy',\n offline: 'Offline',\n}\n\n/**\n * ProfileCard SSOT — 預設 field keys(現行 v15.7:default 只 `id` + `employeeNumber`;\n * v11 always-render 為歷史脈絡,已退役 — 見下方 2026-05-07 註解 + render 處 v12 註解)\n *\n * ── 為什麼 SSOT ──\n * User explicit rule:「所有 namecard 預設顯示的資訊都是要一樣完整的」。前 v10 props\n * 全 optional + body conditional render → consumer 漏傳 fields 視覺缺 section,每個範例\n * 各自不一致(同 person 在 DataTable / PeoplePicker / avatar.principles 看起來不同)。\n *\n * v11 canonical(歷史):ProfileCard 當時 always renders 4 default sections — 缺 data → 顯\n * `EMPTY_PLACEHOLDER`(\"—\")。現行:default fields 縮為 2(v15.7)+ status 條件 render(v12),\n * 「視覺結構 SSOT 在此元件,不依賴 consumer」原則不變。\n *\n * ── 對齊 world-class ──\n * Slack profile card / Linear member card / Notion person card / GitHub user card / Figma user card\n * 都是 fixed schema(role / email / location / department / department / pronouns 等),\n * 不會因為某 user 沒填 phone 整個 phone field 不見 — 缺 = 顯 `Not set` 或留白。\n *\n * ── 為什麼 placeholder 不 hide ──\n * Hide → consumer 不知道少傳 → 視覺漂移;Placeholder → 永遠看到「該欄該有」+ dev-warn 提示\n * consumer 補資料,自動防漂移(M19 ensure-canonical 對齊)。\n */\n// **2026-05-07 v15.7 user directive**:default render 只 `id` + `employeeNumber` 兩個。\n// Email / Phone / Department / Location 等其他 description 一律 opt-in by consumer 透過\n// `fields` array prop。對齊 user 明確「應該確保所有都只有這兩個,因為我並沒有要求你要選其他的」。\nexport const NAMECARD_DEFAULT_FIELD_KEYS = ['id', 'employeeNumber'] as const\ntype ProfileCardDefaultFieldKey = typeof NAMECARD_DEFAULT_FIELD_KEYS[number]\n\nconst DEFAULT_FIELD_LABEL: Record<ProfileCardDefaultFieldKey, string> = {\n id: 'ID',\n employeeNumber: 'Employee number',\n}\n\nconst FIELD_PLACEHOLDER = '—'\n\nexport interface ProfileCardProps extends React.HTMLAttributes<HTMLDivElement> {\n name: string\n avatar?: AvatarData\n subtitle?: string\n status?: StatusType\n statusMessage?: React.ReactNode\n actions?: React.ReactNode\n /**\n * Consumer 傳的 field 資料(partial)。預設 keys 走 `NAMECARD_DEFAULT_FIELD_KEYS` —\n * 只有 id / employeeNumber 兩個 default field 永遠 render(缺資料顯 `—`);email / phone /\n * department / location 等其他欄位一律 opt-in by consumer 透過本 prop 傳入。Consumer 想新增\n * 自訂 field 直接傳入(在 default 之後 append),想 override default key value 也直接傳。\n */\n fields?: { label: string; value: React.ReactNode }[]\n /**\n * Default field 的真實值。Object key = NAMECARD_DEFAULT_FIELD_KEYS 之一。\n * 缺 key → render placeholder。Dev mode 會 console.warn 提醒消費者補資料。\n */\n defaultFieldValues?: Partial<Record<ProfileCardDefaultFieldKey, React.ReactNode>>\n onViewMore?: () => void\n viewMoreLabel?: string\n}\n\n// code-quality-allow: long-function — foundational composite main body — 拆 sub-fn 會複雜化 local state / ref / context binding\nconst ProfileCard = React.forwardRef<HTMLDivElement, ProfileCardProps>(\n (\n {\n name,\n avatar,\n subtitle,\n status,\n statusMessage,\n actions,\n fields,\n defaultFieldValues,\n onViewMore,\n viewMoreLabel = 'View more',\n className,\n ...props\n },\n ref,\n ) => {\n // v12 canonical:default fields 永遠 render(缺資料顯 placeholder),consumer 自訂\n // fields 在 default 之後 append。Status section 為條件 render(status undefined →\n // 整 block 隱藏,見下方 render 處)— v11「永遠 render + placeholder」已退役。\n //\n // **Dedup canonical(2026-05-07 v15.8 fix Bug E)**:consumer 的 `fields` array 若含\n // label 撞 default(eg. 「ID」「Employee number」),consumer 值 win — defaults 那一行\n // 跳過(否則 same label 連 render 兩次,如 default placeholder `—` + consumer 真值)。\n // 這是遷移期 forgiving 行為:DEV warn 提示應改用 `defaultFieldValues`,但 production\n // 不破壞既有 consumer。對齊 React `key` 唯一性 + Linear / Slack profile card 一 label\n // 一 row idiom。\n const allFields = React.useMemo(() => {\n const consumerLabels = new Set((fields ?? []).map((f) => f.label))\n const defaults = NAMECARD_DEFAULT_FIELD_KEYS\n .map((key) => ({\n label: DEFAULT_FIELD_LABEL[key],\n value: defaultFieldValues?.[key] ?? FIELD_PLACEHOLDER,\n }))\n .filter((d) => !consumerLabels.has(d.label))\n return fields && fields.length > 0 ? [...defaults, ...fields] : defaults\n }, [defaultFieldValues, fields])\n\n // Dev warn:consumer 透過 `fields` 傳 default key label(legacy pattern)→ 應改 `defaultFieldValues`\n if (process.env.NODE_ENV !== 'production' && fields) {\n const legacyEntry = fields.find((f) =>\n Object.values(DEFAULT_FIELD_LABEL).includes(f.label as string),\n )\n if (legacyEntry) {\n // eslint-disable-next-line no-console\n console.warn(\n `[ProfileCard] \"${name}\":legacy pattern — fields[].label=\"${legacyEntry.label}\" ` +\n `is a default field. Migrate to defaultFieldValues={{ id, employeeNumber }} prop ` +\n `to align with NAMECARD_DEFAULT_FIELD_KEYS canonical.`,\n )\n }\n }\n\n // Dev mode warn:consumer 沒傳 default field 任何 key → 提示補完(避免漂移成 placeholder-only)\n if (process.env.NODE_ENV !== 'production' && !defaultFieldValues) {\n // eslint-disable-next-line no-console\n console.warn(\n `[ProfileCard] \"${name}\":no defaultFieldValues passed — sections will render placeholders. ` +\n `Pass at least { id, employeeNumber } via defaultFieldValues prop. ` +\n `For other description items (email/phone/department/location etc),use \\`fields\\` prop array.`,\n )\n }\n\n // Layout canonical(2026-04-23):Header + Actions 固定上,Body(status + fields)可捲動,\n // View more 固定下。**ProfileCard 自己約束高度**,不依賴 consumer HoverCardContent 設 flex:\n // - `max-h-[var(--radix-hover-card-content-available-height,...)]`:HoverCard / Popover\n // context 自動繼承 Radix viewport-aware 變數;standalone 落到 100vh fallback\n // - 內部 `flex flex-col + overflow-hidden`:Header(shrink-0)+ Body(flex-1 min-h-0 ScrollArea)\n // + Footer(shrink-0)三層 chrome\n // 世界級對照:Slack / Linear / GitHub / Notion hover-profile popover 皆此 chrome pattern。\n return (\n <div\n ref={ref}\n className={cn(\n 'w-[320px] flex flex-col overflow-hidden',\n 'max-h-[var(--radix-hover-card-content-available-height,var(--radix-popover-content-available-height,100vh))]',\n className,\n )}\n {...props}\n >\n {/* ── HEADER(固定): profile + actions ── */}\n <div className=\"shrink-0 flex flex-col\">\n <div className=\"flex items-start gap-3 px-4 py-3\">\n <Avatar\n src={avatar?.src}\n alt={avatar?.alt ?? name}\n color={avatar?.color}\n size={AVATAR_SIZE}\n status={status}\n className=\"shrink-0\"\n />\n {/* ProfileCard typography:label body-lg(16/1.5) + desc body(14/1.5) = reading mode + size=\"lg\"。\n labelClassName escape hatch 加 font-medium(card context 語意)+ labelTruncate=false 允許 wrap。 */}\n <ItemContent\n label={name}\n description={subtitle}\n mode=\"reading\"\n size=\"lg\"\n labelTruncate={false}\n labelClassName=\"text-body-lg font-medium text-foreground\"\n className=\"justify-center\"\n style={{ minHeight: AVATAR_SIZE }}\n />\n </div>\n\n {/* Action buttons — 均分空間 + 填滿格子(canonical):多個 action 等寬瓜分容器,\n 單一 action 也撐滿容器。`grid grid-flow-col auto-cols-fr` + `[&>*]:w-full`。\n 世界級對照:iOS contact card / macOS contact / LinkedIn profile card 的 action row。 */}\n {actions && (\n <div className=\"grid grid-flow-col auto-cols-fr gap-2 px-4 pb-3 [&>*]:w-full\">\n {actions}\n </div>\n )}\n </div>\n\n {/* ── BODY(可捲動,v12 status-conditional 2026-05-14):status + fields ──\n **v12 rule**(per user 拍板「不應該顯示『狀態沒有被設定』,production 每 user 一定有\n presence state,undefined 頂多是 loading transient 還沒讀到」):status undefined →\n 隱藏整 status badge + status message block(loading 期間 skip),禁 render「Status not\n set」這種 placeholder(語義錯,user presence 不會「沒設定」)。**ProfileCard-specific 不外推\n 至 DS 其他元件**(FileItem / DescriptionList / DataTable cell 各自 placeholder 邏輯\n unrelated)。Fields section 仍 always-render(info schema 性質)。 */}\n <ScrollArea className=\"flex-1 min-h-0 border-t border-divider\">\n {/* Status section:`status` defined 才 render(v12 conditional canonical) */}\n {status && (\n <div className=\"px-4 py-3 flex flex-col gap-3\">\n <div className=\"flex items-center gap-2 px-3 py-2 bg-muted rounded-md\">\n <span\n className=\"w-2.5 h-2.5 rounded-full shrink-0\"\n style={{ backgroundColor: STATUS_DOT_COLOR[status] }}\n />\n <span className=\"text-body\">{STATUS_LABEL[status]}</span>\n </div>\n {/* Status message — 只在 status defined 才 render(語意配對:status badge + status\n message 是一組;沒 status 就沒 status message)。缺 statusMessage 顯 placeholder。 */}\n <DescriptionList>\n <DescriptionItem label=\"Status message\">\n {statusMessage ?? <span className=\"text-fg-muted\">{FIELD_PLACEHOLDER}</span>}\n </DescriptionItem>\n </DescriptionList>\n </div>\n )}\n\n {/* Fields section:status defined 才有 border-t separator;無 status section 時去除 border-t(因 ScrollArea 起點已有上方 border-t,不重疊) */}\n <div className={cn('px-4 py-3', status && 'border-t border-divider')}>\n <DescriptionList cols={2}>\n {allFields.map((f) => (\n <DescriptionItem key={f.label} label={f.label}>\n {f.value === FIELD_PLACEHOLDER\n ? <span className=\"text-fg-muted\">{FIELD_PLACEHOLDER}</span>\n : f.value}\n </DescriptionItem>\n ))}\n </DescriptionList>\n </div>\n </ScrollArea>\n\n {/* ── FOOTER(固定): View more,py-3 canonical(12px,比一般 link 按鈕多呼吸) ── */}\n {onViewMore && (\n <div className=\"shrink-0 border-t border-divider px-4 py-3\">\n <Button variant=\"link\" size=\"sm\" onClick={onViewMore} className=\"w-full\">{viewMoreLabel}</Button>\n </div>\n )}\n </div>\n )\n },\n)\nProfileCard.displayName = 'ProfileCard'\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 nameCardMeta = {\n component: 'ProfileCard',\n family: null, // non-family composite / overlay / layout\n variants: {\n\n },\n sizes: {\n\n },\n states: [], // 唯讀 person-info template,無自身互動 state(hover/active/focus/disabled 屬子 Button/Avatar)\n tokens: {\n bg: ['bg-muted'],\n fg: ['text-foreground'],\n ring: [],\n },\n} as const\n\nexport { ProfileCard }\n"],"names":[],"mappings":"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"}
|
|
@@ -72,7 +72,7 @@ export declare const radioGroupMeta: {
|
|
|
72
72
|
readonly typography: "body-lg";
|
|
73
73
|
};
|
|
74
74
|
};
|
|
75
|
-
readonly states: readonly ["default", "hover", "
|
|
75
|
+
readonly states: readonly ["default", "hover", "checked", "focus-visible", "disabled"];
|
|
76
76
|
readonly tokens: {
|
|
77
77
|
readonly bg: readonly ["bg-disabled", "bg-surface"];
|
|
78
78
|
readonly fg: readonly ["text-fg-disabled", "text-fg-secondary", "text-foreground", "text-primary"];
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"radio-group.d.ts","sourceRoot":"","sources":["../../../src/components/RadioGroup/radio-group.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAA;AAC9B,OAAO,KAAK,mBAAmB,MAAM,6BAA6B,CAAA;AAElE,OAAO,EAAO,KAAK,YAAY,EAAE,MAAM,0BAA0B,CAAA;AAGjE,OAAO,KAAK,EAAE,SAAS,EAAE,YAAY,EAAE,MAAM,8CAA8C,CAAA;AAG3F,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,cAAc,CAAA;AAC9C,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,0CAA0C,CAAA;AAY1E,MAAM,WAAW,eACf,SAAQ,KAAK,CAAC,wBAAwB,CAAC,OAAO,mBAAmB,CAAC,IAAI,CAAC;IACvE;;;;;;;;OAQG;IACH,IAAI,CAAC,EAAE,SAAS,CAAA;IAChB;;;OAGG;IACH,OAAO,CAAC,EAAE,YAAY,CAAA;CACvB;AA6BD,QAAA,MAAM,UAAU,
|
|
1
|
+
{"version":3,"file":"radio-group.d.ts","sourceRoot":"","sources":["../../../src/components/RadioGroup/radio-group.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAA;AAC9B,OAAO,KAAK,mBAAmB,MAAM,6BAA6B,CAAA;AAElE,OAAO,EAAO,KAAK,YAAY,EAAE,MAAM,0BAA0B,CAAA;AAGjE,OAAO,KAAK,EAAE,SAAS,EAAE,YAAY,EAAE,MAAM,8CAA8C,CAAA;AAG3F,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,cAAc,CAAA;AAC9C,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,0CAA0C,CAAA;AAY1E,MAAM,WAAW,eACf,SAAQ,KAAK,CAAC,wBAAwB,CAAC,OAAO,mBAAmB,CAAC,IAAI,CAAC;IACvE;;;;;;;;OAQG;IACH,IAAI,CAAC,EAAE,SAAS,CAAA;IAChB;;;OAGG;IACH,OAAO,CAAC,EAAE,YAAY,CAAA;CACvB;AA6BD,QAAA,MAAM,UAAU,wFAwFd,CAAA;AAUF,QAAA,MAAM,iBAAiB;;8EA2BtB,CAAA;AAOD,KAAK,uBAAuB,GAAG,KAAK,CAAC,wBAAwB,CAAC,OAAO,mBAAmB,CAAC,IAAI,CAAC,CAAA;AAE9F,MAAM,WAAW,mBACf,SAAQ,uBAAuB,EAC7B,YAAY,CAAC,OAAO,iBAAiB,CAAC;IACxC;;;;;OAKG;IACH,KAAK,CAAC,EAAE,KAAK,CAAC,SAAS,CAAA;IACvB;;;OAGG;IACH,WAAW,CAAC,EAAE,KAAK,CAAC,SAAS,CAAA;IAC7B,qHAAqH;IACrH,IAAI,CAAC,EAAE,UAAU,CAAA;IACjB,+BAA+B;IAC/B,MAAM,CAAC,EAAE,UAAU,CAAA;IACnB;;;;OAIG;IACH,QAAQ,CAAC,EAAE,OAAO,CAAA;CACnB;AAID,QAAA,MAAM,cAAc,+FAwFnB,CAAA;AAKD,eAAO,MAAM,cAAc;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAwBjB,CAAA;AAEV,OAAO,EAAE,UAAU,EAAE,cAAc,EAAE,iBAAiB,EAAE,CAAA"}
|
|
@@ -31,13 +31,25 @@ const RadioGroup = React.forwardRef(({ className, mode, variant: _chrome, value,
|
|
|
31
31
|
const resolvedMode = useResolvedFieldMode({ mode, disabled: props.disabled });
|
|
32
32
|
const fieldCtx = useFieldContext();
|
|
33
33
|
const resolvedBoxSize = useResolvedFieldSize(void 0, "md");
|
|
34
|
+
const {
|
|
35
|
+
onValueChange: _onValueChange,
|
|
36
|
+
disabled: _disabled,
|
|
37
|
+
required: _required,
|
|
38
|
+
name: _name,
|
|
39
|
+
orientation: _orientation,
|
|
40
|
+
dir: _dir,
|
|
41
|
+
loop: _loop,
|
|
42
|
+
asChild: _asChild,
|
|
43
|
+
children: _children,
|
|
44
|
+
...restDomProps
|
|
45
|
+
} = props;
|
|
34
46
|
if (resolvedMode === "display") {
|
|
35
47
|
const selectedValue = value ?? defaultValue;
|
|
36
48
|
if (!selectedValue) {
|
|
37
|
-
return /* @__PURE__ */ jsx("div", { role: "group", className: cn("grid", className), children: /* @__PURE__ */ jsx("span", { className: "text-fg-muted", children: EMPTY_DISPLAY }) });
|
|
49
|
+
return /* @__PURE__ */ jsx("div", { ...restDomProps, ref, role: "group", className: cn("grid", className), children: /* @__PURE__ */ jsx("span", { className: "text-fg-muted", children: EMPTY_DISPLAY }) });
|
|
38
50
|
}
|
|
39
51
|
const selectedLabel = findSelectedRadioLabel(props.children, selectedValue);
|
|
40
|
-
return /* @__PURE__ */ jsx("div", { role: "group", className: cn("grid", className), children: /* @__PURE__ */ jsx("span", { className: "text-foreground", children: selectedLabel ?? selectedValue }) });
|
|
52
|
+
return /* @__PURE__ */ jsx("div", { ...restDomProps, ref, role: "group", className: cn("grid", className), children: /* @__PURE__ */ jsx("span", { className: "text-foreground", children: selectedLabel ?? selectedValue }) });
|
|
41
53
|
}
|
|
42
54
|
if (resolvedMode === "readonly" && (fieldCtx == null ? void 0 : fieldCtx.hasFieldWrapper) === true) {
|
|
43
55
|
const selectedValue = value ?? defaultValue;
|
|
@@ -46,6 +58,8 @@ const RadioGroup = React.forwardRef(({ className, mode, variant: _chrome, value,
|
|
|
46
58
|
return /* @__PURE__ */ jsx(
|
|
47
59
|
"div",
|
|
48
60
|
{
|
|
61
|
+
...restDomProps,
|
|
62
|
+
ref,
|
|
49
63
|
role: "radiogroup",
|
|
50
64
|
"aria-readonly": "true",
|
|
51
65
|
"aria-labelledby": fieldCtx == null ? void 0 : fieldCtx.labelId,
|
|
@@ -135,6 +149,14 @@ const RadioGroupItem = React.forwardRef(
|
|
|
135
149
|
tabIndex: effectiveReadonly ? -1 : void 0,
|
|
136
150
|
className: cn(radioItemVariants({ size }), className),
|
|
137
151
|
...props,
|
|
152
|
+
onClick: (event) => {
|
|
153
|
+
var _a;
|
|
154
|
+
if (effectiveReadonly) {
|
|
155
|
+
event.preventDefault();
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
(_a = props.onClick) == null ? void 0 : _a.call(props, event);
|
|
159
|
+
},
|
|
138
160
|
children: /* @__PURE__ */ jsx(RadioGroupPrimitive.Indicator, { className: "grid place-content-center", children: /* @__PURE__ */ jsx(
|
|
139
161
|
Circle,
|
|
140
162
|
{
|
|
@@ -174,7 +196,10 @@ const radioGroupMeta = {
|
|
|
174
196
|
md: { fieldHeight: 32, iconSize: 8, typography: "body" },
|
|
175
197
|
lg: { fieldHeight: 36, iconSize: 10, typography: "body-lg" }
|
|
176
198
|
},
|
|
177
|
-
|
|
199
|
+
// 2026-07-04 修 stale meta:移除 'active' — radioItemVariants cva 無 active-state 樣式,
|
|
200
|
+
// Radix radio 也無 data-state=active(對照 checkboxMeta 正確不含 active)
|
|
201
|
+
// 'checked' = 選定持續態(card 式 = border+text primary;2026-07-07 meta 詞彙統一補列)
|
|
202
|
+
states: ["default", "hover", "checked", "focus-visible", "disabled"],
|
|
178
203
|
tokens: {
|
|
179
204
|
bg: ["bg-disabled", "bg-surface"],
|
|
180
205
|
fg: ["text-fg-disabled", "text-fg-secondary", "text-foreground", "text-primary"],
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"radio-group.js","sources":["../../../src/components/RadioGroup/radio-group.tsx"],"sourcesContent":["// @benchmark-unverified-blanket: file-level retraction per M22 (d) — claims herein not individually URL-cited; treat as unverified visual/usage rumor unless retrofit per-claim. Hook escape preserved.\nimport * as React from \"react\"\nimport * as RadioGroupPrimitive from \"@radix-ui/react-radio-group\"\nimport { Circle } from \"lucide-react\"\nimport { cva, type VariantProps } from \"class-variance-authority\"\n\nimport { cn } from \"@/lib/utils\"\nimport type { FieldMode, FieldVariant } from \"@/design-system/components/Field/field-types\"\nimport { useFieldContext, useResolvedFieldMode, useResolvedFieldDisabled, useResolvedFieldSize } from \"@/design-system/components/Field/field-context\"\nimport { SelectionItem } from \"@/design-system/components/SelectionControl/selection-item\"\nimport type { LucideIcon } from \"lucide-react\"\nimport type { AvatarData } from \"@/design-system/components/Avatar/avatar\"\nimport { EMPTY_DISPLAY, fieldWrapperStyles } from \"@/design-system/components/Field/field-wrapper\"\n\n// ── RadioGroup display mode ─────────────────────────────────────────────────\n// RadioGroup mode='display' 時:Group 不渲染 Radix primitive(無 radio 視覺),\n// 改由 RadioGroup 本體 walk props.children,找 control.value === selectedValue 的\n// SelectionItem,把它的 label 渲染為單一純文字 span(其他選項不顯示)。\n// 對齊 Carbon read-only single-select(只顯示 selected 內容)+ Airtable / Notion read-only。\n// 實作在 RadioGroup forwardRef 內(見下方 mode === 'display' 分支)。\n\n// ── RadioGroup ──────────────────────────────────────────────────────────────\n\nexport interface RadioGroupProps\n extends React.ComponentPropsWithoutRef<typeof RadioGroupPrimitive.Root> {\n /**\n * Field mode(2026-05-05 Phase B3 align):\n * edit — 一般可互動 RadioGroup(預設)\n * display — **純展示**:不渲染 Radix Root / 任何 radio 視覺;RadioGroup 本體 walk\n * children,僅 control.value === group.value 那筆把 label 渲染為純文字 span。\n * 對齊 Carbon read-only / DataTable single-select cell read mode。\n * readonly — standalone:ReadonlyContext 傳 items 鎖互動保留視覺;Field 內:灰框 + 選中項 label(不渲染 radio 群組)\n * disabled — 同 RadioGroupPrimitive.Root disabled 屬性\n */\n mode?: FieldMode\n /**\n * Visual chrome — RadioGroup 本體無 input wrapper variant,本 prop 對主體無視覺影響;\n * 為對齊 Field 4-mode + chrome 透傳契約而保留(M19 一致性)。\n */\n variant?: FieldVariant\n}\n\n// RadioGroup mode='readonly' → 透過 context 把 readOnly 傳給所有 child RadioGroupItem\n// (item 已支援 readOnly prop + data-[readonly] 樣式;Radix Root 無 readOnly,故用 context)。\nconst RadioGroupReadonlyContext = React.createContext(false)\n\n// walk children 找 control.value === selectedValue 的 SelectionItem label(display / readonly-in-Field 共用)\nfunction findSelectedRadioLabel(children: React.ReactNode, selectedValue: string | undefined): React.ReactNode {\n let selectedLabel: React.ReactNode = null\n React.Children.forEach(children, (child) => {\n if (!React.isValidElement(child)) return\n const cProps = child.props as { control?: unknown; label?: React.ReactNode; value?: unknown }\n // 形狀 1:<RadioGroupItem value label>(主用法)— value/label 直掛 props\n if (cProps.value === selectedValue && cProps.value !== undefined) {\n selectedLabel = cProps.label ?? selectedValue\n return\n }\n // 形狀 2:<SelectionItem control={<RadioGroupItem value/>} label>(組合用法)\n const control = cProps.control\n if (React.isValidElement(control)) {\n const controlValue = (control.props as { value?: unknown }).value\n if (controlValue === selectedValue) {\n selectedLabel = cProps.label ?? selectedValue\n }\n }\n })\n return selectedLabel\n}\n\nconst RadioGroup = React.forwardRef<\n React.ElementRef<typeof RadioGroupPrimitive.Root>,\n RadioGroupProps\n>(({ className, mode, variant: _chrome, value, defaultValue, ...props }, ref) => {\n // 2026-06-08 SSOT cascade:resolvedMode 經 resolver hook 讀 fieldCtx(原 root 完全不讀 → <Field disabled>/<Field mode> 失效)\n const resolvedMode = useResolvedFieldMode({ mode, disabled: (props as { disabled?: boolean }).disabled })\n const fieldCtx = useFieldContext()\n // readonly 灰框 size:走 SSOT resolver(RadioGroup 無 size prop → ctx > 'md')\n const resolvedBoxSize = useResolvedFieldSize(undefined, 'md') as 'sm' | 'md' | 'lg'\n // mode='display' — 純展示 selected option 的 label,不渲染任何 radio control 視覺。\n // 對齊 Carbon read-only single-select(只顯示 selected 內容)+ Airtable / Notion read-only。\n // 實作:walk children 找 control.value === selectedValue 的 SelectionItem,render label plain text。\n // (不用 context dispatch 給 RadioGroupItem — SelectionItem layout wrapper 仍會渲染所有 item label)\n if (resolvedMode === 'display') {\n const selectedValue = (value ?? defaultValue) as string | undefined\n if (!selectedValue) {\n return <div role=\"group\" className={cn('grid', className)}><span className=\"text-fg-muted\">{EMPTY_DISPLAY}</span></div>\n }\n const selectedLabel = findSelectedRadioLabel(props.children, selectedValue)\n return (\n <div role=\"group\" className={cn('grid', className)}>\n <span className=\"text-foreground\">{selectedLabel ?? selectedValue}</span>\n </div>\n )\n }\n\n // ── mode='readonly' in Field(2026-06-12 user 拍板,與 Checkbox/Switch 灰框模型一致)──\n // Field 內 readonly 單選 = fieldWrapperStyles readonly 灰框 + 選中項 label 文字\n // (= Select readonly 同款呈現:同為「單選資料」,鎖定時呈現一致)。\n // standalone readonly(無 Field)維持原樣鎖互動(ReadonlyContext 路徑)。\n if (resolvedMode === 'readonly' && fieldCtx?.hasFieldWrapper === true) {\n const selectedValue = (value ?? defaultValue) as string | undefined\n const selectedLabel = selectedValue ? findSelectedRadioLabel(props.children, selectedValue) : null\n const boxSize = resolvedBoxSize\n return (\n <div\n role=\"radiogroup\"\n aria-readonly=\"true\"\n aria-labelledby={fieldCtx?.labelId}\n aria-invalid={fieldCtx?.invalid || undefined}\n data-readonly=\"true\"\n tabIndex={0}\n className={cn(\n fieldWrapperStyles({ size: boxSize, mode: 'readonly', variant: 'default' }),\n 'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring',\n className,\n )}\n >\n {selectedValue\n ? <span className=\"text-foreground\">{selectedLabel ?? selectedValue}</span>\n : <span className=\"text-fg-muted\">{EMPTY_DISPLAY}</span>}\n </div>\n )\n }\n\n // mode='disabled' → Radix Root disabled(原生 propagate 給所有 item);\n // mode='readonly' → context 傳 readOnly 給 items(item 渲染為 data-[readonly] 鎖互動 + aria-readonly)。\n return (\n <RadioGroupReadonlyContext.Provider value={resolvedMode === 'readonly'}>\n <RadioGroupPrimitive.Root\n className={cn(\"grid\", className)}\n value={value}\n defaultValue={defaultValue}\n {...props}\n disabled={resolvedMode === 'disabled'}\n ref={ref}\n />\n </RadioGroupReadonlyContext.Provider>\n )\n})\nRadioGroup.displayName = 'RadioGroup'\n// Field layout 宣告:RadioGroup 是 block primitive(多項堆疊),\n// 進入 <Field> 時 control area 自動切 items-start + padding-top 公式對齊。\n// Convention 詳見 components/Field/field.spec.md「Control area:Inline vs Block」段落。\n;(RadioGroup as unknown as { fieldLayout: 'block' }).fieldLayout = 'block'\n\n// ── RadioGroupItem Variants ─────────────────────────────────────────────────\n// 與 Checkbox 完全對齊:sm/md=16px, lg=20px。差異只有形狀(rounded-full)和指示器(filled dot)。\n\nconst radioItemVariants = cva(\n [\n 'grid place-content-center shrink-0 rounded-full',\n 'border border-border bg-surface',\n 'transition-colors duration-150',\n 'hover:border-border-hover',\n 'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-1',\n 'data-[state=checked]:border-primary data-[state=checked]:text-primary',\n 'data-[state=checked]:hover:border-primary-hover data-[state=checked]:hover:text-primary-hover',\n 'disabled:cursor-not-allowed disabled:bg-disabled disabled:border-transparent disabled:hover:border-transparent',\n 'disabled:data-[state=checked]:bg-disabled disabled:data-[state=checked]:border-transparent disabled:data-[state=checked]:text-fg-disabled',\n // readOnly:鎖定互動但維持 checked/unchecked 視覺\n 'data-[readonly=true]:pointer-events-none data-[readonly=true]:cursor-default',\n 'data-[readonly=true]:hover:border-border',\n ],\n {\n variants: {\n size: {\n sm: 'h-4 w-4',\n md: 'h-4 w-4',\n lg: 'h-5 w-5',\n },\n },\n defaultVariants: {\n size: 'md',\n },\n }\n)\n\n// ── Dot Size ────────────────────────────────────────────────────────────────\nconst dotSize: Record<string, number> = { sm: 8, md: 8, lg: 10 }\n\n// ── Types ───────────────────────────────────────────────────────────────────\n\ntype RadioItemPrimitiveProps = React.ComponentPropsWithoutRef<typeof RadioGroupPrimitive.Item>\n\nexport interface RadioGroupItemProps\n extends RadioItemPrimitiveProps,\n VariantProps<typeof radioItemVariants> {\n /**\n * Inline label。提供時 RadioGroupItem 自動透過 SelectionItem 包裝,\n * 套用 codified 樣式(text-body / text-foreground / disabled 色)。\n * 在 <Field> context 內時此 prop 仍然生效(Radio 的 label 是每個 item\n * 各自的,不是整組 Field 的;FieldLabel 是 RadioGroup 整體的 label)。\n */\n label?: React.ReactNode\n /**\n * Inline description(secondary 文字)。須與 label 搭配使用。\n * 套用 text-body / text-fg-secondary 樣式。\n */\n description?: React.ReactNode\n /** 可選左側 icon(label 前)— 2026-06-12 M30 修:轉發 SelectionItem 既有 canonical 槽(selection-item.tsx jsDoc SSOT;與 avatar 互斥)*/\n icon?: LucideIcon\n /** 可選左側 avatar(label 前)— 同上 */\n avatar?: AvatarData\n /**\n * readonly 模式:鎖定互動但維持 checked/unchecked 視覺正確。\n * 通常整個 RadioGroup 一起設 readonly(由 parent RadioGroup 的 disabled\n * 或 readonly 行為決定),個別 item 也可設。\n */\n readOnly?: boolean\n}\n\n// ── RadioGroupItem ──────────────────────────────────────────────────────────\n\nconst RadioGroupItem = React.forwardRef<\n React.ElementRef<typeof RadioGroupPrimitive.Item>,\n RadioGroupItemProps\n>(\n (\n {\n className,\n size,\n label,\n icon,\n avatar,\n description,\n readOnly = false,\n disabled,\n id: idProp,\n ...props\n },\n ref\n ) => {\n const sizeKey = size ?? 'md'\n const dotPx = dotSize[sizeKey]\n\n // 注意:RadioGroup mode='display' 的純文字渲染由 RadioGroup 本體 walk-children 處理\n // (見上方 RadioGroup forwardRef 的 mode === 'display' 分支),RadioGroupItem 在 display\n // mode 下不會被獨立 render,故此處無 display 分支。\n\n // 注意:Radio 的 label 語意與 Checkbox/Switch 不同——\n // Checkbox/Switch 的 label 就是該 control 的唯一 label(被 Field context 接管),\n // RadioGroupItem 的 label 是「該選項」的 label(每 item 各自擁有),\n // FieldLabel 則是整個 RadioGroup 的 label。\n // 因此 RadioGroupItem 的 label 不因 Field context 被忽略。\n const resolvedDisabled = useResolvedFieldDisabled(disabled)\n // group-level readonly(RadioGroup mode='readonly')或 item-level readOnly,任一 true 即鎖互動。\n const groupReadonly = React.useContext(RadioGroupReadonlyContext)\n const effectiveReadonly = readOnly || groupReadonly\n\n const generatedId = React.useId()\n const inputId = idProp ?? generatedId\n\n const rootEl = (\n <RadioGroupPrimitive.Item\n id={inputId}\n ref={ref}\n disabled={resolvedDisabled}\n aria-readonly={effectiveReadonly || undefined}\n data-readonly={effectiveReadonly || undefined}\n tabIndex={effectiveReadonly ? -1 : undefined}\n className={cn(radioItemVariants({ size }), className)}\n {...props}\n >\n <RadioGroupPrimitive.Indicator className=\"grid place-content-center\">\n <Circle\n style={{ width: dotPx, height: dotPx }}\n className=\"fill-current text-current\"\n />\n </RadioGroupPrimitive.Indicator>\n </RadioGroupPrimitive.Item>\n )\n\n // 無 label → 只渲染 radio 本體\n if (label == null) return rootEl\n\n // 有 label → 透過 SelectionItem 包裝,與 Checkbox 一致(disabled 已於上方 useResolvedFieldDisabled 解析)\n return (\n <SelectionItem\n control={rootEl}\n label={label}\n description={description}\n icon={icon}\n avatar={avatar}\n htmlFor={inputId}\n disabled={resolvedDisabled}\n size={sizeKey}\n />\n )\n }\n)\nRadioGroupItem.displayName = 'RadioGroupItem'\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 radioGroupMeta = {\n component: 'RadioGroup',\n family: null, // self-contained primitive(對齊 spec frontmatter self-contained + body L31;非 Family 4 — field-controls.spec.md 成員名單不含 RadioGroup)\n variants: {\n\n },\n sizes: {\n // iconSize = 渲染指示器尺寸(對齊 checkbox/switch meta 慣例:checkbox iconSize=真 Check glyph 12/12/16)。\n // Radio 指示器是 filled dot 非 glyph,真值 = dotSize 8/8/10(radio-group.tsx:179);\n // 控件框 16/16/20 與 Checkbox 對齊但那是 box 非「指示器」,不入此鍵(避免 metadata 語意 drift)。\n sm: { fieldHeight: 28, iconSize: 8, typography: 'body' },\n md: { fieldHeight: 32, iconSize: 8, typography: 'body' },\n lg: { fieldHeight: 36, iconSize: 10, typography: 'body-lg' },\n },\n states: ['default', 'hover', 'active', 'focus-visible', 'disabled'],\n tokens: {\n bg: ['bg-disabled', 'bg-surface'],\n fg: ['text-fg-disabled', 'text-fg-secondary', 'text-foreground', 'text-primary'],\n ring: ['ring-ring'],\n },\n defaultSize: 'md',\n} as const\n\nexport { RadioGroup, RadioGroupItem, radioItemVariants }\n"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"radio-group.js","sources":["../../../src/components/RadioGroup/radio-group.tsx"],"sourcesContent":["// @benchmark-unverified-blanket: file-level retraction per M22 (d) — claims herein not individually URL-cited; treat as unverified visual/usage rumor unless retrofit per-claim. Hook escape preserved.\nimport * as React from \"react\"\nimport * as RadioGroupPrimitive from \"@radix-ui/react-radio-group\"\nimport { Circle } from \"lucide-react\"\nimport { cva, type VariantProps } from \"class-variance-authority\"\n\nimport { cn } from \"@/lib/utils\"\nimport type { FieldMode, FieldVariant } from \"@/design-system/components/Field/field-types\"\nimport { useFieldContext, useResolvedFieldMode, useResolvedFieldDisabled, useResolvedFieldSize } from \"@/design-system/components/Field/field-context\"\nimport { SelectionItem } from \"@/design-system/components/SelectionControl/selection-item\"\nimport type { LucideIcon } from \"lucide-react\"\nimport type { AvatarData } from \"@/design-system/components/Avatar/avatar\"\nimport { EMPTY_DISPLAY, fieldWrapperStyles } from \"@/design-system/components/Field/field-wrapper\"\n\n// ── RadioGroup display mode ─────────────────────────────────────────────────\n// RadioGroup mode='display' 時:Group 不渲染 Radix primitive(無 radio 視覺),\n// 改由 RadioGroup 本體 walk props.children,找 control.value === selectedValue 的\n// SelectionItem,把它的 label 渲染為單一純文字 span(其他選項不顯示)。\n// 對齊 Carbon read-only single-select(只顯示 selected 內容)+ Airtable / Notion read-only。\n// 實作在 RadioGroup forwardRef 內(見下方 mode === 'display' 分支)。\n\n// ── RadioGroup ──────────────────────────────────────────────────────────────\n\nexport interface RadioGroupProps\n extends React.ComponentPropsWithoutRef<typeof RadioGroupPrimitive.Root> {\n /**\n * Field mode(2026-05-05 Phase B3 align):\n * edit — 一般可互動 RadioGroup(預設)\n * display — **純展示**:不渲染 Radix Root / 任何 radio 視覺;RadioGroup 本體 walk\n * children,僅 control.value === group.value 那筆把 label 渲染為純文字 span。\n * 對齊 Carbon read-only / DataTable single-select cell read mode。\n * readonly — standalone:ReadonlyContext 傳 items 鎖互動保留視覺;Field 內:灰框 + 選中項 label(不渲染 radio 群組)\n * disabled — 同 RadioGroupPrimitive.Root disabled 屬性\n */\n mode?: FieldMode\n /**\n * Visual chrome — RadioGroup 本體無 input wrapper variant,本 prop 對主體無視覺影響;\n * 為對齊 Field 4-mode + chrome 透傳契約而保留(M19 一致性)。\n */\n variant?: FieldVariant\n}\n\n// RadioGroup mode='readonly' → 透過 context 把 readOnly 傳給所有 child RadioGroupItem\n// (item 已支援 readOnly prop + data-[readonly] 樣式;Radix Root 無 readOnly,故用 context)。\nconst RadioGroupReadonlyContext = React.createContext(false)\n\n// walk children 找 control.value === selectedValue 的 SelectionItem label(display / readonly-in-Field 共用)\nfunction findSelectedRadioLabel(children: React.ReactNode, selectedValue: string | undefined): React.ReactNode {\n let selectedLabel: React.ReactNode = null\n React.Children.forEach(children, (child) => {\n if (!React.isValidElement(child)) return\n const cProps = child.props as { control?: unknown; label?: React.ReactNode; value?: unknown }\n // 形狀 1:<RadioGroupItem value label>(主用法)— value/label 直掛 props\n if (cProps.value === selectedValue && cProps.value !== undefined) {\n selectedLabel = cProps.label ?? selectedValue\n return\n }\n // 形狀 2:<SelectionItem control={<RadioGroupItem value/>} label>(組合用法)\n const control = cProps.control\n if (React.isValidElement(control)) {\n const controlValue = (control.props as { value?: unknown }).value\n if (controlValue === selectedValue) {\n selectedLabel = cProps.label ?? selectedValue\n }\n }\n })\n return selectedLabel\n}\n\nconst RadioGroup = React.forwardRef<\n React.ElementRef<typeof RadioGroupPrimitive.Root>,\n RadioGroupProps\n>(({ className, mode, variant: _chrome, value, defaultValue, ...props }, ref) => {\n // 2026-06-08 SSOT cascade:resolvedMode 經 resolver hook 讀 fieldCtx(原 root 完全不讀 → <Field disabled>/<Field mode> 失效)\n const resolvedMode = useResolvedFieldMode({ mode, disabled: (props as { disabled?: boolean }).disabled })\n const fieldCtx = useFieldContext()\n // readonly 灰框 size:走 SSOT resolver(RadioGroup 無 size prop → ctx > 'md')\n const resolvedBoxSize = useResolvedFieldSize(undefined, 'md') as 'sm' | 'md' | 'lg'\n // mode='display' — 純展示 selected option 的 label,不渲染任何 radio control 視覺。\n // 對齊 Carbon read-only single-select(只顯示 selected 內容)+ Airtable / Notion read-only。\n // 實作:walk children 找 control.value === selectedValue 的 SelectionItem,render label plain text。\n // (不用 context dispatch 給 RadioGroupItem — SelectionItem layout wrapper 仍會渲染所有 item label)\n // 2026-07-04 修:display / readonly-in-Field 分支的 <div> 原丟棄剩餘 props(id/data-*/aria-*)\n // 且不轉發 forwardRef ref → 自家 radio-group.stories.tsx display demo 傳的 aria-label 都靜默失效。\n // 抽出 Radix 專屬 non-DOM props,其餘 DOM props 於兩分支 spread 轉發(與 Checkbox/Switch 同修);\n // edit 主路徑不受影響(Root 仍 spread 完整 props)。\n const {\n onValueChange: _onValueChange,\n disabled: _disabled,\n required: _required,\n name: _name,\n orientation: _orientation,\n dir: _dir,\n loop: _loop,\n asChild: _asChild,\n children: _children,\n ...restDomProps\n } = props as RadioGroupProps & { asChild?: boolean }\n\n if (resolvedMode === 'display') {\n const selectedValue = (value ?? defaultValue) as string | undefined\n if (!selectedValue) {\n return <div {...restDomProps} ref={ref as React.Ref<HTMLDivElement>} role=\"group\" className={cn('grid', className)}><span className=\"text-fg-muted\">{EMPTY_DISPLAY}</span></div>\n }\n const selectedLabel = findSelectedRadioLabel(props.children, selectedValue)\n return (\n <div {...restDomProps} ref={ref as React.Ref<HTMLDivElement>} role=\"group\" className={cn('grid', className)}>\n <span className=\"text-foreground\">{selectedLabel ?? selectedValue}</span>\n </div>\n )\n }\n\n // ── mode='readonly' in Field(2026-06-12 user 拍板,與 Checkbox/Switch 灰框模型一致)──\n // Field 內 readonly 單選 = fieldWrapperStyles readonly 灰框 + 選中項 label 文字\n // (= Select readonly 同款呈現:同為「單選資料」,鎖定時呈現一致)。\n // standalone readonly(無 Field)維持原樣鎖互動(ReadonlyContext 路徑)。\n if (resolvedMode === 'readonly' && fieldCtx?.hasFieldWrapper === true) {\n const selectedValue = (value ?? defaultValue) as string | undefined\n const selectedLabel = selectedValue ? findSelectedRadioLabel(props.children, selectedValue) : null\n const boxSize = resolvedBoxSize\n return (\n <div\n {...restDomProps}\n ref={ref as React.Ref<HTMLDivElement>}\n role=\"radiogroup\"\n aria-readonly=\"true\"\n aria-labelledby={fieldCtx?.labelId}\n aria-invalid={fieldCtx?.invalid || undefined}\n data-readonly=\"true\"\n tabIndex={0}\n className={cn(\n fieldWrapperStyles({ size: boxSize, mode: 'readonly', variant: 'default' }),\n 'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring',\n className,\n )}\n >\n {selectedValue\n ? <span className=\"text-foreground\">{selectedLabel ?? selectedValue}</span>\n : <span className=\"text-fg-muted\">{EMPTY_DISPLAY}</span>}\n </div>\n )\n }\n\n // mode='disabled' → Radix Root disabled(原生 propagate 給所有 item);\n // mode='readonly' → context 傳 readOnly 給 items(item 渲染為 data-[readonly] 鎖互動 + aria-readonly)。\n return (\n <RadioGroupReadonlyContext.Provider value={resolvedMode === 'readonly'}>\n <RadioGroupPrimitive.Root\n className={cn(\"grid\", className)}\n value={value}\n defaultValue={defaultValue}\n {...props}\n disabled={resolvedMode === 'disabled'}\n ref={ref}\n />\n </RadioGroupReadonlyContext.Provider>\n )\n})\nRadioGroup.displayName = 'RadioGroup'\n// Field layout 宣告:RadioGroup 是 block primitive(多項堆疊),\n// 進入 <Field> 時 control area 自動切 items-start + padding-top 公式對齊。\n// Convention 詳見 components/Field/field.spec.md「Control area:Inline vs Block」段落。\n;(RadioGroup as unknown as { fieldLayout: 'block' }).fieldLayout = 'block'\n\n// ── RadioGroupItem Variants ─────────────────────────────────────────────────\n// 與 Checkbox 完全對齊:sm/md=16px, lg=20px。差異只有形狀(rounded-full)和指示器(filled dot)。\n\nconst radioItemVariants = cva(\n [\n 'grid place-content-center shrink-0 rounded-full',\n 'border border-border bg-surface',\n 'transition-colors duration-150',\n 'hover:border-border-hover',\n 'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-1',\n 'data-[state=checked]:border-primary data-[state=checked]:text-primary',\n 'data-[state=checked]:hover:border-primary-hover data-[state=checked]:hover:text-primary-hover',\n 'disabled:cursor-not-allowed disabled:bg-disabled disabled:border-transparent disabled:hover:border-transparent',\n 'disabled:data-[state=checked]:bg-disabled disabled:data-[state=checked]:border-transparent disabled:data-[state=checked]:text-fg-disabled',\n // readOnly:鎖定互動但維持 checked/unchecked 視覺\n 'data-[readonly=true]:pointer-events-none data-[readonly=true]:cursor-default',\n 'data-[readonly=true]:hover:border-border',\n ],\n {\n variants: {\n size: {\n sm: 'h-4 w-4',\n md: 'h-4 w-4',\n lg: 'h-5 w-5',\n },\n },\n defaultVariants: {\n size: 'md',\n },\n }\n)\n\n// ── Dot Size ────────────────────────────────────────────────────────────────\nconst dotSize: Record<string, number> = { sm: 8, md: 8, lg: 10 }\n\n// ── Types ───────────────────────────────────────────────────────────────────\n\ntype RadioItemPrimitiveProps = React.ComponentPropsWithoutRef<typeof RadioGroupPrimitive.Item>\n\nexport interface RadioGroupItemProps\n extends RadioItemPrimitiveProps,\n VariantProps<typeof radioItemVariants> {\n /**\n * Inline label。提供時 RadioGroupItem 自動透過 SelectionItem 包裝,\n * 套用 codified 樣式(text-body / text-foreground / disabled 色)。\n * 在 <Field> context 內時此 prop 仍然生效(Radio 的 label 是每個 item\n * 各自的,不是整組 Field 的;FieldLabel 是 RadioGroup 整體的 label)。\n */\n label?: React.ReactNode\n /**\n * Inline description(secondary 文字)。須與 label 搭配使用。\n * 套用 text-body / text-fg-secondary 樣式。\n */\n description?: React.ReactNode\n /** 可選左側 icon(label 前)— 2026-06-12 M30 修:轉發 SelectionItem 既有 canonical 槽(selection-item.tsx jsDoc SSOT;與 avatar 互斥)*/\n icon?: LucideIcon\n /** 可選左側 avatar(label 前)— 同上 */\n avatar?: AvatarData\n /**\n * readonly 模式:鎖定互動但維持 checked/unchecked 視覺正確。\n * 通常整個 RadioGroup 一起設 readonly(由 parent RadioGroup 的 disabled\n * 或 readonly 行為決定),個別 item 也可設。\n */\n readOnly?: boolean\n}\n\n// ── RadioGroupItem ──────────────────────────────────────────────────────────\n\nconst RadioGroupItem = React.forwardRef<\n React.ElementRef<typeof RadioGroupPrimitive.Item>,\n RadioGroupItemProps\n>(\n (\n {\n className,\n size,\n label,\n icon,\n avatar,\n description,\n readOnly = false,\n disabled,\n id: idProp,\n ...props\n },\n ref\n ) => {\n const sizeKey = size ?? 'md'\n const dotPx = dotSize[sizeKey]\n\n // 注意:RadioGroup mode='display' 的純文字渲染由 RadioGroup 本體 walk-children 處理\n // (見上方 RadioGroup forwardRef 的 mode === 'display' 分支),RadioGroupItem 在 display\n // mode 下不會被獨立 render,故此處無 display 分支。\n\n // 注意:Radio 的 label 語意與 Checkbox/Switch 不同——\n // Checkbox/Switch 的 label 就是該 control 的唯一 label(被 Field context 接管),\n // RadioGroupItem 的 label 是「該選項」的 label(每 item 各自擁有),\n // FieldLabel 則是整個 RadioGroup 的 label。\n // 因此 RadioGroupItem 的 label 不因 Field context 被忽略。\n const resolvedDisabled = useResolvedFieldDisabled(disabled)\n // group-level readonly(RadioGroup mode='readonly')或 item-level readOnly,任一 true 即鎖互動。\n const groupReadonly = React.useContext(RadioGroupReadonlyContext)\n const effectiveReadonly = readOnly || groupReadonly\n\n const generatedId = React.useId()\n const inputId = idProp ?? generatedId\n\n const rootEl = (\n <RadioGroupPrimitive.Item\n id={inputId}\n ref={ref}\n disabled={resolvedDisabled}\n aria-readonly={effectiveReadonly || undefined}\n data-readonly={effectiveReadonly || undefined}\n tabIndex={effectiveReadonly ? -1 : undefined}\n className={cn(radioItemVariants({ size }), className)}\n {...props}\n // 2026-07-05 D4 修:readOnly 鎖互動補 label 繞道 — <label htmlFor> 的 synthetic click\n // 不是 pointer event,cva pointer-events-none 攔不到,點 label 文字仍選中。\n // preventDefault 讓 Radix composeEventHandlers(checkForDefaultPrevented)跳過內部 onCheck;\n // 必列在 {...props} 之後才不被 consumer onClick 覆蓋(與 Checkbox/Switch 同修)。\n // 同時攔 Radix roving-focus 的 arrow-key onFocus → click() 路徑(同為 click event)。\n onClick={(event) => {\n if (effectiveReadonly) {\n event.preventDefault()\n return\n }\n props.onClick?.(event)\n }}\n >\n <RadioGroupPrimitive.Indicator className=\"grid place-content-center\">\n <Circle\n style={{ width: dotPx, height: dotPx }}\n className=\"fill-current text-current\"\n />\n </RadioGroupPrimitive.Indicator>\n </RadioGroupPrimitive.Item>\n )\n\n // 無 label → 只渲染 radio 本體\n if (label == null) return rootEl\n\n // 有 label → 透過 SelectionItem 包裝,與 Checkbox 一致(disabled 已於上方 useResolvedFieldDisabled 解析)\n return (\n <SelectionItem\n control={rootEl}\n label={label}\n description={description}\n icon={icon}\n avatar={avatar}\n htmlFor={inputId}\n disabled={resolvedDisabled}\n size={sizeKey}\n />\n )\n }\n)\nRadioGroupItem.displayName = 'RadioGroupItem'\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 radioGroupMeta = {\n component: 'RadioGroup',\n family: null, // self-contained primitive(對齊 spec frontmatter self-contained + body L31;非 Family 4 — field-controls.spec.md 成員名單不含 RadioGroup)\n variants: {\n\n },\n sizes: {\n // iconSize = 渲染指示器尺寸(對齊 checkbox/switch meta 慣例:checkbox iconSize=真 Check glyph 12/12/16)。\n // Radio 指示器是 filled dot 非 glyph,真值 = dotSize 8/8/10(radio-group.tsx:179);\n // 控件框 16/16/20 與 Checkbox 對齊但那是 box 非「指示器」,不入此鍵(避免 metadata 語意 drift)。\n sm: { fieldHeight: 28, iconSize: 8, typography: 'body' },\n md: { fieldHeight: 32, iconSize: 8, typography: 'body' },\n lg: { fieldHeight: 36, iconSize: 10, typography: 'body-lg' },\n },\n // 2026-07-04 修 stale meta:移除 'active' — radioItemVariants cva 無 active-state 樣式,\n // Radix radio 也無 data-state=active(對照 checkboxMeta 正確不含 active)\n // 'checked' = 選定持續態(card 式 = border+text primary;2026-07-07 meta 詞彙統一補列)\n states: ['default', 'hover', 'checked', 'focus-visible', 'disabled'],\n tokens: {\n bg: ['bg-disabled', 'bg-surface'],\n fg: ['text-fg-disabled', 'text-fg-secondary', 'text-foreground', 'text-primary'],\n ring: ['ring-ring'],\n },\n defaultSize: 'md',\n} as const\n\nexport { RadioGroup, RadioGroupItem, radioItemVariants }\n"],"names":[],"mappings":"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"}
|
|
@@ -135,9 +135,9 @@ function StarIcon({ Icon, sizePx, fillRatio, isHalf, interactive, onHover, onCli
|
|
|
135
135
|
}
|
|
136
136
|
);
|
|
137
137
|
}
|
|
138
|
-
return /* @__PURE__ */ jsxs("span", { className: "relative inline-flex", style: { width: sizePx, height: sizePx }, children: [
|
|
139
|
-
/* @__PURE__ */ jsx(Icon, { size: sizePx, fill: FILL_EMPTY, stroke: "none", className: "absolute inset-0", style: { color: FILL_EMPTY } }),
|
|
140
|
-
/* @__PURE__ */ jsx("span", { className: "absolute inset-0 overflow-hidden", style: { width: sizePx * fillRatio }, children: /* @__PURE__ */ jsx(Icon, { size: sizePx, fill: FILL_FILLED, stroke: "none", style: { color: FILL_FILLED } }) }),
|
|
138
|
+
return /* @__PURE__ */ jsxs("span", { "aria-hidden": true, className: "relative inline-flex", style: { width: sizePx, height: sizePx }, children: [
|
|
139
|
+
/* @__PURE__ */ jsx(Icon, { size: sizePx, fill: FILL_EMPTY, stroke: "none", "aria-hidden": true, className: "absolute inset-0", style: { color: FILL_EMPTY } }),
|
|
140
|
+
/* @__PURE__ */ jsx("span", { className: "absolute inset-0 overflow-hidden", style: { width: sizePx * fillRatio }, children: /* @__PURE__ */ jsx(Icon, { size: sizePx, fill: FILL_FILLED, stroke: "none", "aria-hidden": true, style: { color: FILL_FILLED } }) }),
|
|
141
141
|
interactive && /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
142
142
|
/* @__PURE__ */ jsx(
|
|
143
143
|
"span",
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"rating.js","sources":["../../../src/components/Rating/rating.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 { Star, type LucideIcon } from 'lucide-react'\nimport { cn } from '@/lib/utils'\nimport { useFieldContext, useResolvedFieldSize, useResolvedFieldDisabled, useResolvedFieldMode } from '@/design-system/components/Field/field-context'\n\n/**\n * Rating — 星星評分元件\n *\n * 世界級對照:Ant Design `<Rate>`、Material MUI `<Rating>`。\n * shadcn 核心沒有 Rating,本元件自建。\n *\n * ── 使用情境 ──\n * - review / feedback:商品評分 / 服務評分(可編輯 + 唯讀兩種)\n * - display:已提交評分的唯讀呈現(商品清單星等)\n *\n * ── 視覺 ──\n * 填色用 `var(--warning)`(yellow-6,世界級黃星 convention;與 warning 語意共用色相\n * 但語境不同,評分 = UX convention color 非 status)。\n * 空色用 `var(--divider)`(neutral-4 借 divider semantic alias;灰色;與 disabled/empty 同級)。\n *\n * ── 互動 ──\n * interactive(預設):hover 預覽、click 設值、keyboard Left/Right 改值\n * readOnly:純顯示,不響應 hover / click\n *\n * ── 精度 ──\n * precision=\"full\"(預設) — 整星(1, 2, 3, 4, 5)\n * precision=\"half\" — 半星(0.5, 1, 1.5, 2, 2.5, ..., 5)\n */\n\n// ── Icon size canonical(2026-04-21 AR48 修正)──\n//\n// Rating 的「一顆星」視覺重量接近 **Avatar / identity icon**,不是純 inline icon。\n// 理由:\n// - 星星是 filled shape(解析整個 icon 是重量感的一部分),不像純 outline icon 靠 stroke\n// - Field 內 Rating 跟 Avatar / Tag 並排時視覺份量要對齊,否則 row height 一致但 icon 看起來比重量不對\n// - 世界級對照:Ant Rate in Form = 20px、Material MUI Rating fontSize=inherit 預設約 24、Airbnb 評分星 24px\n//\n// 因此 Field 內 Rating icon size 對齊 **item-anatomy inline Avatar sizes**:sm=20 / md=24 / lg=24。\n// 非 icon tier(16/16/20)——star 不是次要 affordance icon,它是主要資料視覺。\n//\n// Container 高度仍對齊 `--field-height-*`(sm=28 / md=32 / lg=36),讓 Rating 可與其他\n// field-height family 元件(Input / Select)並排時 row height 對齊。\n//\n// ── 使用情境 ──\n// - **Standalone**(獨立展示評分,如商品卡 / 評論)→ 預設 `xs`(container 24,icon 20,\n// 對齊 Avatar sm 20px;iOS HIG / Airbnb 商品卡星星 20-24px)\n// - **Field 內**(表單評分欄位)→ 跟 Field 尺寸對齊(sm=20 / md=24 / lg=24,default md)\nconst SIZE_PX = { xs: 20, sm: 20, md: 24, lg: 24 } as const\nconst CONTAINER_HEIGHT: Record<'xs' | 'sm' | 'md' | 'lg', string> = {\n xs: 'h-field-xs',\n sm: 'h-field-sm',\n md: 'h-field-md',\n lg: 'h-field-lg',\n}\n\nexport interface RatingProps extends Omit<React.HTMLAttributes<HTMLDivElement>, 'onChange'> {\n /** 當前評分(0 ~ max) */\n value?: number\n /** 預設值(uncontrolled) */\n defaultValue?: number\n /** 評分改變 callback */\n onChange?: (value: number) => void\n /** 滿分(預設 5) */\n max?: number\n /** 尺寸。standalone 建議 xs(24px);Field 內跟隨 Field size 傳 sm/md/lg */\n size?: 'xs' | 'sm' | 'md' | 'lg'\n /** 精度:full = 整星,half = 半星 */\n precision?: 'full' | 'half'\n /** 唯讀(無 hover / click 響應) */\n readOnly?: boolean\n /** 完全停用 */\n disabled?: boolean\n /**\n * Loading 狀態 — 正在取得既有評分 / 正在儲存。\n * 視覺同 disabled(composite 整塊 opacity-disabled)但 semantic 不同:\n * loading = 暫時性等待(aria-busy),disabled = 永久業務規則(aria-disabled)。\n * 詳 rating.spec.md「Interactive vs ReadOnly」+「Loading canonical」\n */\n loading?: boolean\n /** 自訂 icon(預設 Star);傳 LucideIcon */\n icon?: LucideIcon\n /**\n * a11y label。readOnly(role=img)時必填。\n * interactive(role=slider)時:在 Field 內免填(自動 aria-labelledby 指向 FieldLabel);\n * standalone(無 Field)時必填——role=slider 依 WAI-ARIA APG 必有 accessible name。\n */\n 'aria-label'?: string\n}\n\n// code-quality-allow: long-function — foundational composite main body — 拆 sub-fn 會複雜化 local state / ref / context binding\nconst Rating = React.forwardRef<HTMLDivElement, RatingProps>(\n (\n {\n value,\n defaultValue = 0,\n onChange,\n max = 5,\n size: sizeProp,\n precision = 'full',\n readOnly: readOnlyProp = false,\n disabled: disabledProp,\n loading = false,\n icon: Icon = Star,\n className,\n ...props\n },\n ref,\n ) => {\n // Context-aware default size(AR31 canonical):\n // - Field 內(有 FieldContext.size) → 跟 Field size 對齊(sm / md / lg)\n // - Standalone(無 Field context) → default `xs`(24px,對齊 Avatar / Tag sm / iOS HIG standalone)\n // consumer 可傳 size 顯式 override。世界級對照:Material Rating standalone 24dp、\n // Ant Rate in Form 跟 Form.itemSize,standalone 24px。\n const fieldCtx = useFieldContext() // 保留:aria-labelledby 用 fieldCtx.labelId\n // 2026-06-08 SSOT:<Field disabled> cascade(原 isInteractive 只看 local disabled prop)\n const disabled = useResolvedFieldDisabled(disabledProp)\n // <Field mode=\"readonly\"> cascade(2026-06-12 補):Rating 的 readonly 呈現 = 星星本身\n // (星星即值語言,role=img,全業界 review-stars canonical)——不包灰框,只鎖互動。\n const resolvedMode = useResolvedFieldMode({ mode: undefined, disabled, readOnly: readOnlyProp })\n const readOnly = readOnlyProp || resolvedMode === 'readonly'\n const size = useResolvedFieldSize<'xs' | 'sm' | 'md' | 'lg'>(sizeProp, 'xs') // SSOT:統一 size resolution(Rating default 'xs')\n const [internalValue, setInternalValue] = React.useState(defaultValue)\n const [hoverValue, setHoverValue] = React.useState<number | null>(null)\n const isControlled = value !== undefined\n const currentValue = isControlled ? value : internalValue\n const displayValue = hoverValue ?? currentValue\n const iconPx = SIZE_PX[size]\n const isInteractive = !readOnly && !disabled && !loading\n\n const setValue = (v: number) => {\n if (!isControlled) setInternalValue(v)\n onChange?.(v)\n }\n\n const handleKeyDown = (e: React.KeyboardEvent<HTMLDivElement>) => {\n if (!isInteractive) return\n const step = precision === 'half' ? 0.5 : 1\n // Full ARIA slider pattern(WAI-ARIA):Arrow / Home / End 支援 — D4 UX audit 2026-04-22\n if (e.key === 'ArrowRight' || e.key === 'ArrowUp') {\n e.preventDefault()\n setValue(Math.min(max, currentValue + step))\n } else if (e.key === 'ArrowLeft' || e.key === 'ArrowDown') {\n e.preventDefault()\n setValue(Math.max(0, currentValue - step))\n } else if (e.key === 'Home') {\n e.preventDefault()\n setValue(0)\n } else if (e.key === 'End') {\n e.preventDefault()\n setValue(max)\n }\n }\n\n return (\n <div\n ref={ref}\n role={isInteractive ? 'slider' : 'img'}\n // a11y(#30):role=slider 必有 accessible name(WAI-ARIA APG slider pattern)。\n // Field 內 → 自動 aria-labelledby 指向 FieldLabel 的 id(fieldCtx.labelId,免填);\n // Standalone → 仍需 consumer 傳 aria-label。對齊 TimePicker / DatePicker 同 canonical\n // (time-picker.tsx:313 / date-picker.tsx:514:aria-labelledby={fieldCtx?.labelId})。\n // 置於 {...props} 前,consumer 顯式傳的 aria-labelledby 仍可覆寫。\n aria-labelledby={fieldCtx?.labelId} // 2026-06-12 修:readonly/disabled(role=img)也需 accessible name,labelledby 對 img 合法\n aria-valuenow={isInteractive ? currentValue : undefined}\n aria-valuemin={isInteractive ? 0 : undefined}\n aria-valuemax={isInteractive ? max : undefined}\n aria-valuetext={isInteractive ? `${currentValue} of ${max} stars` : undefined}\n aria-disabled={disabled || undefined}\n // a11y: 刻意不設 aria-readonly — readOnly 時 role=img(axe aria-allowed-attr 禁 img 用 aria-readonly,2026-04-25);\n // interactive 時 role=slider 但必非 readOnly(isInteractive = !readOnly)。兩 state 皆不該有此屬性,故省略。\n aria-busy={loading || undefined}\n tabIndex={isInteractive ? 0 : undefined}\n onKeyDown={handleKeyDown}\n onMouseLeave={() => setHoverValue(null)}\n className={cn(\n 'inline-flex items-center gap-1',\n // Container 對齊 field-height family,讓 Rating 可與 Input/Select/Button 並排 row-align\n CONTAINER_HEIGHT[size],\n 'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 rounded-md',\n // disabled 跟 loading 視覺相同(composite uniform dim),semantic 由 aria-disabled / aria-busy 區分\n (disabled || loading) && 'opacity-disabled pointer-events-none',\n className,\n )}\n {...props}\n >\n {Array.from({ length: max }, (_, i) => {\n const starValue = i + 1\n const fillRatio = Math.max(0, Math.min(1, displayValue - i)) // 0..1\n const isHalf = precision === 'half' && fillRatio > 0 && fillRatio < 1\n\n return (\n <StarIcon\n key={i}\n Icon={Icon}\n sizePx={iconPx}\n fillRatio={fillRatio}\n isHalf={isHalf}\n interactive={isInteractive}\n onHover={(halfFirst) => {\n if (!isInteractive) return\n const v = precision === 'half' && halfFirst ? starValue - 0.5 : starValue\n setHoverValue(v)\n }}\n onClick={(halfFirst) => {\n if (!isInteractive) return\n const v = precision === 'half' && halfFirst ? starValue - 0.5 : starValue\n setValue(v)\n }}\n />\n )\n })}\n </div>\n )\n },\n)\nRating.displayName = 'Rating'\n\n// ── StarIcon: 單顆星 + half-precision overlay ─────────────────────────────\n\ninterface StarIconProps {\n Icon: LucideIcon\n sizePx: number\n fillRatio: number // 0..1\n isHalf: boolean\n interactive: boolean\n onHover: (halfFirst: boolean) => void\n onClick: (halfFirst: boolean) => void\n}\n\nconst FILL_FILLED = 'var(--warning)' // yellow-6 — 黃星 convention\nconst FILL_EMPTY = 'var(--divider)' // 灰色空星(neutral-4 借 divider semantic alias,user 2026-05-09 拍板;對齊 Material rgba(0,0,0,0.26) muted-fill canonical)\n\nfunction StarIcon({ Icon, sizePx, fillRatio, isHalf, interactive, onHover, onClick }: StarIconProps) {\n // a11y(2026-04-25 axe nested-interactive fix):inner 點擊目標改 <span>(非 interactive\n // element),不會跟外層 role='slider' 形成 nested-interactive 違規。鍵盤控制統一在外層\n // slider 的 arrow keys,inner 只處理 mouse click 定位。Ant Rate / Material MUI 同模式。\n if (!isHalf) {\n // Full: 一整顆 fill(filled 或 empty)\n const fill = fillRatio >= 1 ? FILL_FILLED : FILL_EMPTY\n return (\n <span\n role=\"presentation\"\n onMouseEnter={interactive ? () => onHover(false) : undefined}\n onClick={interactive ? () => onClick(false) : undefined}\n className={cn(\n 'inline-flex',\n interactive ? 'cursor-pointer' : 'cursor-default',\n )}\n style={{ color: fill }}\n aria-hidden\n >\n {/* stroke=\"none\" 移除 Lucide Star 預設的 outline stroke(lucide defaultAttributes\n strokeWidth=2 + stroke=currentColor 會畫輪廓),讓星星是純 fill-only 的 shape——\n fill 與 outline 同色視覺上仍有亮度差。\n 世界級對照:Ant Rate / Material MUI Rating 皆純 fill,無 outline stroke。*/}\n <Icon size={sizePx} fill={fill} stroke=\"none\" className=\"shrink-0\" />\n </span>\n )\n }\n\n // Half: 兩個重疊 icon,左半 filled / 右半 empty + 兩個 hover zone 切半星\n return (\n <span className=\"relative inline-flex\" style={{ width: sizePx, height: sizePx }}>\n <Icon size={sizePx} fill={FILL_EMPTY} stroke=\"none\" className=\"absolute inset-0\" style={{ color: FILL_EMPTY }} />\n <span className=\"absolute inset-0 overflow-hidden\" style={{ width: sizePx * fillRatio }}>\n <Icon size={sizePx} fill={FILL_FILLED} stroke=\"none\" style={{ color: FILL_FILLED }} />\n </span>\n {interactive && (\n <>\n <span\n role=\"presentation\"\n onMouseEnter={() => onHover(true)}\n onClick={() => onClick(true)}\n className=\"absolute inset-y-0 left-0 w-1/2 cursor-pointer\"\n aria-hidden\n />\n <span\n role=\"presentation\"\n onMouseEnter={() => onHover(false)}\n onClick={() => onClick(false)}\n className=\"absolute inset-y-0 right-0 w-1/2 cursor-pointer\"\n aria-hidden\n />\n </>\n )}\n </span>\n )\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 ratingMeta = {\n component: 'Rating',\n family: null, // self-contained primitive(對齊 spec frontmatter self-contained + body L24;非 Family 4)\n variants: {\n\n },\n sizes: {\n\n },\n states: ['default', 'hover', 'active', 'focus-visible', 'disabled'],\n tokens: {\n bg: ['bg-transparent'],\n fg: [],\n ring: ['ring-ring'],\n },\n} as const\n\nexport { Rating }\n"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"rating.js","sources":["../../../src/components/Rating/rating.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 { Star, type LucideIcon } from 'lucide-react'\nimport { cn } from '@/lib/utils'\nimport { useFieldContext, useResolvedFieldSize, useResolvedFieldDisabled, useResolvedFieldMode } from '@/design-system/components/Field/field-context'\n\n/**\n * Rating — 星星評分元件\n *\n * 世界級對照:Ant Design `<Rate>`、Material MUI `<Rating>`。\n * shadcn 核心沒有 Rating,本元件自建。\n *\n * ── 使用情境 ──\n * - review / feedback:商品評分 / 服務評分(可編輯 + 唯讀兩種)\n * - display:已提交評分的唯讀呈現(商品清單星等)\n *\n * ── 視覺 ──\n * 填色用 `var(--warning)`(yellow-6,世界級黃星 convention;與 warning 語意共用色相\n * 但語境不同,評分 = UX convention color 非 status)。\n * 空色用 `var(--divider)`(neutral-4 借 divider semantic alias;灰色;與 disabled/empty 同級)。\n *\n * ── 互動 ──\n * interactive(預設):hover 預覽、click 設值、keyboard Left/Right 改值\n * readOnly:純顯示,不響應 hover / click\n *\n * ── 精度 ──\n * precision=\"full\"(預設) — 整星(1, 2, 3, 4, 5)\n * precision=\"half\" — 半星(0.5, 1, 1.5, 2, 2.5, ..., 5)\n */\n\n// ── Icon size canonical(2026-04-21 AR48 修正)──\n//\n// Rating 的「一顆星」視覺重量接近 **Avatar / identity icon**,不是純 inline icon。\n// 理由:\n// - 星星是 filled shape(解析整個 icon 是重量感的一部分),不像純 outline icon 靠 stroke\n// - Field 內 Rating 跟 Avatar / Tag 並排時視覺份量要對齊,否則 row height 一致但 icon 看起來比重量不對\n// - 世界級對照:Ant Rate in Form = 20px、Material MUI Rating fontSize=inherit 預設約 24、Airbnb 評分星 24px\n//\n// 因此 Field 內 Rating icon size 對齊 **item-anatomy inline Avatar sizes**:sm=20 / md=24 / lg=24。\n// 非 icon tier(16/16/20)——star 不是次要 affordance icon,它是主要資料視覺。\n//\n// Container 高度仍對齊 `--field-height-*`(sm=28 / md=32 / lg=36),讓 Rating 可與其他\n// field-height family 元件(Input / Select)並排時 row height 對齊。\n//\n// ── 使用情境 ──\n// - **Standalone**(獨立展示評分,如商品卡 / 評論)→ 預設 `xs`(container 24,icon 20,\n// 對齊 Avatar sm 20px;iOS HIG / Airbnb 商品卡星星 20-24px)\n// - **Field 內**(表單評分欄位)→ 跟 Field 尺寸對齊(sm=20 / md=24 / lg=24,default md)\nconst SIZE_PX = { xs: 20, sm: 20, md: 24, lg: 24 } as const\nconst CONTAINER_HEIGHT: Record<'xs' | 'sm' | 'md' | 'lg', string> = {\n xs: 'h-field-xs',\n sm: 'h-field-sm',\n md: 'h-field-md',\n lg: 'h-field-lg',\n}\n\nexport interface RatingProps extends Omit<React.HTMLAttributes<HTMLDivElement>, 'onChange'> {\n /** 當前評分(0 ~ max) */\n value?: number\n /** 預設值(uncontrolled) */\n defaultValue?: number\n /** 評分改變 callback */\n onChange?: (value: number) => void\n /** 滿分(預設 5) */\n max?: number\n /** 尺寸。standalone 建議 xs(24px);Field 內跟隨 Field size 傳 sm/md/lg */\n size?: 'xs' | 'sm' | 'md' | 'lg'\n /** 精度:full = 整星,half = 半星 */\n precision?: 'full' | 'half'\n /** 唯讀(無 hover / click 響應) */\n readOnly?: boolean\n /** 完全停用 */\n disabled?: boolean\n /**\n * Loading 狀態 — 正在取得既有評分 / 正在儲存。\n * 視覺同 disabled(composite 整塊 opacity-disabled)但 semantic 不同:\n * loading = 暫時性等待(aria-busy),disabled = 永久業務規則(aria-disabled)。\n * 詳 rating.spec.md「Interactive vs ReadOnly」+「Loading canonical」\n */\n loading?: boolean\n /** 自訂 icon(預設 Star);傳 LucideIcon */\n icon?: LucideIcon\n /**\n * a11y label。readOnly(role=img)時必填。\n * interactive(role=slider)時:在 Field 內免填(自動 aria-labelledby 指向 FieldLabel);\n * standalone(無 Field)時必填——role=slider 依 WAI-ARIA APG 必有 accessible name。\n */\n 'aria-label'?: string\n}\n\n// code-quality-allow: long-function — foundational composite main body — 拆 sub-fn 會複雜化 local state / ref / context binding\nconst Rating = React.forwardRef<HTMLDivElement, RatingProps>(\n (\n {\n value,\n defaultValue = 0,\n onChange,\n max = 5,\n size: sizeProp,\n precision = 'full',\n readOnly: readOnlyProp = false,\n disabled: disabledProp,\n loading = false,\n icon: Icon = Star,\n className,\n ...props\n },\n ref,\n ) => {\n // Context-aware default size(AR31 canonical):\n // - Field 內(有 FieldContext.size) → 跟 Field size 對齊(sm / md / lg)\n // - Standalone(無 Field context) → default `xs`(24px,對齊 Avatar / Tag sm / iOS HIG standalone)\n // consumer 可傳 size 顯式 override。世界級對照:Material Rating standalone 24dp、\n // Ant Rate in Form 跟 Form.itemSize,standalone 24px。\n const fieldCtx = useFieldContext() // 保留:aria-labelledby 用 fieldCtx.labelId\n // 2026-06-08 SSOT:<Field disabled> cascade(原 isInteractive 只看 local disabled prop)\n const disabled = useResolvedFieldDisabled(disabledProp)\n // <Field mode=\"readonly\"> cascade(2026-06-12 補):Rating 的 readonly 呈現 = 星星本身\n // (星星即值語言,role=img,全業界 review-stars canonical)——不包灰框,只鎖互動。\n const resolvedMode = useResolvedFieldMode({ mode: undefined, disabled, readOnly: readOnlyProp })\n const readOnly = readOnlyProp || resolvedMode === 'readonly'\n const size = useResolvedFieldSize<'xs' | 'sm' | 'md' | 'lg'>(sizeProp, 'xs') // SSOT:統一 size resolution(Rating default 'xs')\n const [internalValue, setInternalValue] = React.useState(defaultValue)\n const [hoverValue, setHoverValue] = React.useState<number | null>(null)\n const isControlled = value !== undefined\n const currentValue = isControlled ? value : internalValue\n const displayValue = hoverValue ?? currentValue\n const iconPx = SIZE_PX[size]\n const isInteractive = !readOnly && !disabled && !loading\n\n const setValue = (v: number) => {\n if (!isControlled) setInternalValue(v)\n onChange?.(v)\n }\n\n const handleKeyDown = (e: React.KeyboardEvent<HTMLDivElement>) => {\n if (!isInteractive) return\n const step = precision === 'half' ? 0.5 : 1\n // Full ARIA slider pattern(WAI-ARIA):Arrow / Home / End 支援 — D4 UX audit 2026-04-22\n if (e.key === 'ArrowRight' || e.key === 'ArrowUp') {\n e.preventDefault()\n setValue(Math.min(max, currentValue + step))\n } else if (e.key === 'ArrowLeft' || e.key === 'ArrowDown') {\n e.preventDefault()\n setValue(Math.max(0, currentValue - step))\n } else if (e.key === 'Home') {\n e.preventDefault()\n setValue(0)\n } else if (e.key === 'End') {\n e.preventDefault()\n setValue(max)\n }\n }\n\n return (\n <div\n ref={ref}\n role={isInteractive ? 'slider' : 'img'}\n // a11y(#30):role=slider 必有 accessible name(WAI-ARIA APG slider pattern)。\n // Field 內 → 自動 aria-labelledby 指向 FieldLabel 的 id(fieldCtx.labelId,免填);\n // Standalone → 仍需 consumer 傳 aria-label。對齊 TimePicker / DatePicker 同 canonical\n // (time-picker.tsx:313 / date-picker.tsx:514:aria-labelledby={fieldCtx?.labelId})。\n // 置於 {...props} 前,consumer 顯式傳的 aria-labelledby 仍可覆寫。\n aria-labelledby={fieldCtx?.labelId} // 2026-06-12 修:readonly/disabled(role=img)也需 accessible name,labelledby 對 img 合法\n aria-valuenow={isInteractive ? currentValue : undefined}\n aria-valuemin={isInteractive ? 0 : undefined}\n aria-valuemax={isInteractive ? max : undefined}\n aria-valuetext={isInteractive ? `${currentValue} of ${max} stars` : undefined}\n aria-disabled={disabled || undefined}\n // a11y: 刻意不設 aria-readonly — readOnly 時 role=img(axe aria-allowed-attr 禁 img 用 aria-readonly,2026-04-25);\n // interactive 時 role=slider 但必非 readOnly(isInteractive = !readOnly)。兩 state 皆不該有此屬性,故省略。\n aria-busy={loading || undefined}\n tabIndex={isInteractive ? 0 : undefined}\n onKeyDown={handleKeyDown}\n onMouseLeave={() => setHoverValue(null)}\n className={cn(\n 'inline-flex items-center gap-1',\n // Container 對齊 field-height family,讓 Rating 可與 Input/Select/Button 並排 row-align\n CONTAINER_HEIGHT[size],\n 'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 rounded-md',\n // disabled 跟 loading 視覺相同(composite uniform dim),semantic 由 aria-disabled / aria-busy 區分\n (disabled || loading) && 'opacity-disabled pointer-events-none',\n className,\n )}\n {...props}\n >\n {Array.from({ length: max }, (_, i) => {\n const starValue = i + 1\n const fillRatio = Math.max(0, Math.min(1, displayValue - i)) // 0..1\n const isHalf = precision === 'half' && fillRatio > 0 && fillRatio < 1\n\n return (\n <StarIcon\n key={i}\n Icon={Icon}\n sizePx={iconPx}\n fillRatio={fillRatio}\n isHalf={isHalf}\n interactive={isInteractive}\n onHover={(halfFirst) => {\n if (!isInteractive) return\n const v = precision === 'half' && halfFirst ? starValue - 0.5 : starValue\n setHoverValue(v)\n }}\n onClick={(halfFirst) => {\n if (!isInteractive) return\n const v = precision === 'half' && halfFirst ? starValue - 0.5 : starValue\n setValue(v)\n }}\n />\n )\n })}\n </div>\n )\n },\n)\nRating.displayName = 'Rating'\n\n// ── StarIcon: 單顆星 + half-precision overlay ─────────────────────────────\n\ninterface StarIconProps {\n Icon: LucideIcon\n sizePx: number\n fillRatio: number // 0..1\n isHalf: boolean\n interactive: boolean\n onHover: (halfFirst: boolean) => void\n onClick: (halfFirst: boolean) => void\n}\n\nconst FILL_FILLED = 'var(--warning)' // yellow-6 — 黃星 convention\nconst FILL_EMPTY = 'var(--divider)' // 灰色空星(neutral-4 借 divider semantic alias,user 2026-05-09 拍板;對齊 Material rgba(0,0,0,0.26) muted-fill canonical)\n\nfunction StarIcon({ Icon, sizePx, fillRatio, isHalf, interactive, onHover, onClick }: StarIconProps) {\n // a11y(2026-04-25 axe nested-interactive fix):inner 點擊目標改 <span>(非 interactive\n // element),不會跟外層 role='slider' 形成 nested-interactive 違規。鍵盤控制統一在外層\n // slider 的 arrow keys,inner 只處理 mouse click 定位。Ant Rate / Material MUI 同模式。\n if (!isHalf) {\n // Full: 一整顆 fill(filled 或 empty)\n const fill = fillRatio >= 1 ? FILL_FILLED : FILL_EMPTY\n return (\n <span\n role=\"presentation\"\n onMouseEnter={interactive ? () => onHover(false) : undefined}\n onClick={interactive ? () => onClick(false) : undefined}\n className={cn(\n 'inline-flex',\n interactive ? 'cursor-pointer' : 'cursor-default',\n )}\n style={{ color: fill }}\n aria-hidden\n >\n {/* stroke=\"none\" 移除 Lucide Star 預設的 outline stroke(lucide defaultAttributes\n strokeWidth=2 + stroke=currentColor 會畫輪廓),讓星星是純 fill-only 的 shape——\n fill 與 outline 同色視覺上仍有亮度差。\n 世界級對照:Ant Rate / Material MUI Rating 皆純 fill,無 outline stroke。*/}\n <Icon size={sizePx} fill={fill} stroke=\"none\" className=\"shrink-0\" />\n </span>\n )\n }\n\n // Half: 兩個重疊 icon,左半 filled / 右半 empty + 兩個 hover zone 切半星\n return (\n <span aria-hidden className=\"relative inline-flex\" style={{ width: sizePx, height: sizePx }}>\n <Icon size={sizePx} fill={FILL_EMPTY} stroke=\"none\" aria-hidden className=\"absolute inset-0\" style={{ color: FILL_EMPTY }} />\n <span className=\"absolute inset-0 overflow-hidden\" style={{ width: sizePx * fillRatio }}>\n <Icon size={sizePx} fill={FILL_FILLED} stroke=\"none\" aria-hidden style={{ color: FILL_FILLED }} />\n </span>\n {interactive && (\n <>\n <span\n role=\"presentation\"\n onMouseEnter={() => onHover(true)}\n onClick={() => onClick(true)}\n className=\"absolute inset-y-0 left-0 w-1/2 cursor-pointer\"\n aria-hidden\n />\n <span\n role=\"presentation\"\n onMouseEnter={() => onHover(false)}\n onClick={() => onClick(false)}\n className=\"absolute inset-y-0 right-0 w-1/2 cursor-pointer\"\n aria-hidden\n />\n </>\n )}\n </span>\n )\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 ratingMeta = {\n component: 'Rating',\n family: null, // self-contained primitive(對齊 spec frontmatter self-contained + body L24;非 Family 4)\n variants: {\n\n },\n sizes: {\n\n },\n states: ['default', 'hover', 'active', 'focus-visible', 'disabled'],\n tokens: {\n bg: ['bg-transparent'],\n fg: [],\n ring: ['ring-ring'],\n },\n} as const\n\nexport { Rating }\n"],"names":[],"mappings":"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"}
|
|
@@ -31,7 +31,7 @@ declare const ScrollArea: React.ForwardRefExoticComponent<Omit<ScrollAreaPrimiti
|
|
|
31
31
|
declare const ScrollBar: React.ForwardRefExoticComponent<Omit<ScrollAreaPrimitive.ScrollAreaScrollbarProps & React.RefAttributes<HTMLDivElement>, "ref"> & React.RefAttributes<HTMLDivElement>>;
|
|
32
32
|
export declare const scrollAreaMeta: {
|
|
33
33
|
readonly component: "ScrollArea";
|
|
34
|
-
readonly family:
|
|
34
|
+
readonly family: "self-contained";
|
|
35
35
|
readonly variants: {};
|
|
36
36
|
readonly sizes: {};
|
|
37
37
|
readonly states: readonly ["default"];
|
|
@@ -46,8 +46,8 @@ const ScrollBar = React.forwardRef(({ className, orientation = "vertical", ...pr
|
|
|
46
46
|
ScrollBar.displayName = ScrollAreaPrimitive.ScrollAreaScrollbar.displayName;
|
|
47
47
|
const scrollAreaMeta = {
|
|
48
48
|
component: "ScrollArea",
|
|
49
|
-
family:
|
|
50
|
-
//
|
|
49
|
+
family: "self-contained",
|
|
50
|
+
// 對齊 scroll-area.spec.md frontmatter(SSOT;同 Separator/OverflowIndicator meta 寫法)
|
|
51
51
|
variants: {},
|
|
52
52
|
sizes: {},
|
|
53
53
|
states: ["default"],
|