@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":"field.js","sources":["../../../src/components/Field/field.tsx"],"sourcesContent":["// code-quality-allow: file-size — foundational composite(Field + FieldLabel + FieldDescription + FieldError + context + 8 layout variants),拆檔會讓 Field 家族互相 import 循環\nimport * as React from 'react'\nimport { Info as InfoIcon } from 'lucide-react'\nimport { cn } from '@/lib/utils'\nimport { Tooltip, TooltipTrigger, TooltipContent } from '@/design-system/components/Tooltip/tooltip'\n\n/**\n * Field — 表單欄位佈局容器(shadcn Field 風格)\n *\n * ── 定位 ────────────────────────────────────────────────────────────────\n * Field 只負責 **佈局 + 狀態 context**,不擁有任何資料型別邏輯。\n * 每個資料型別對應的 Control(Input、NumberInput、Checkbox、Switch 等)\n * 維持自己的 edit / readonly / disabled 三態,Field 透過 context 把\n * mode / disabled / required / invalid / id 傳給子元件,由子元件決定\n * 如何反映。\n *\n * ── 結構 ────────────────────────────────────────────────────────────────\n * <Field orientation=\"vertical | horizontal\" labelWidth=\"120px\">\n * <FieldLabel>姓名</FieldLabel>\n * <Input value={...} onChange={...} /> ← Control(任何非 label/desc/error 的 child)\n * <FieldDescription>...</FieldDescription>\n * <FieldError>{errors.name}</FieldError>\n * </Field>\n *\n * Control 會自動包在 control area slot(min-h-field-* + items-center),\n * 確保 Checkbox / Switch / Radio 等高度 < field-height 的 primitive\n * 垂直對齊 Input 中線;Input 等自身為 field-height 的 primitive 填滿。\n *\n * ── Horizontal mode 的 label 垂直對齊 ───────────────────────────────────\n * FieldLabel 在 horizontal 模式下使用公式:\n * padding-top: calc((var(--field-height-{size}) - 1lh) / 2)\n *\n * 單行 label → 文字第一行與 input 中線對齊(視覺置中)\n * 多行 label → 第一行仍與 input 中線對齊,其餘行往下流(label 高度超過\n * input 時視覺上仍保持與 input 內容同一基準線)\n *\n * 此公式 tracks field-height 和 line-height 的變動,size 切換或字體\n * 調整時自動連動,不需 JS 測量。\n *\n * ── Horizontal mode 的 label 寬度 ───────────────────────────────────────\n * 透過 labelWidth prop → --field-label-width CSS variable,可以是任何\n * CSS length(\"120px\"、\"10rem\"、\"30%\" 等)。預設 \"auto\" 由 label 內容撐開。\n *\n * ── Required 星號 ──────────────────────────────────────────────────────\n * Field 的 required prop 會透過 context 傳給 FieldLabel 自動渲染 *,\n * 星號為 neutral-7(fg-muted),貼齊 label 文字(無 gap),disabled\n * 時降為 fg-disabled。也可在個別 FieldLabel 覆寫。\n */\n\n// ── Types & Context ──\n// Context 定義在 field-context.ts(打斷 circular import)。\n// field.tsx 只 import 不 re-export——consumer 直接從 field-context.ts import useFieldContext。\n\nimport type { FieldMode, FieldVariant, FieldOrientation, FieldSize, FieldControlLayout, FieldContextValue } from './field-context'\nimport { FieldContext, useFieldContext } from './field-context'\n\n// ── Internal helpers ────────────────────────────────────────────────────────\n\nconst MIN_H_CLASS: Record<FieldSize, string> = {\n sm: 'min-h-field-sm',\n md: 'min-h-field-md',\n lg: 'min-h-field-lg',\n}\n\nconst FIELD_HEIGHT_VAR: Record<FieldSize, string> = {\n sm: 'var(--field-height-sm)',\n md: 'var(--field-height-md)',\n lg: 'var(--field-height-lg)',\n}\n\n// Label / Description / Error 的字體固定 text-body (14px),不隨 field size 變。\n// 世界級共識:field size 只影響 input 高度,不影響表單佈局元素的 typography。\nconst FIELD_TEXT_CLASS = 'text-body'\n\ntype SlotKind = 'label' | 'description' | 'error' | 'control'\n\nfunction resolveSlotKind(node: React.ReactNode): SlotKind {\n if (!React.isValidElement(node)) return 'control'\n const displayName = (node.type as { displayName?: string } | null | undefined)?.displayName\n if (displayName === 'FieldLabel') return 'label'\n if (displayName === 'FieldDescription') return 'description'\n if (displayName === 'FieldError') return 'error'\n return 'control'\n}\n\n/**\n * 偵測 control children 的 fieldLayout——任一 control 宣告為 'block' 即整個 area 切 block 模式。\n *\n * Convention:block primitive 在自己的元件檔案掛 static `fieldLayout = 'block'` 屬性,\n * Field 在 render 時讀 `child.type.fieldLayout`。預設 'inline'。\n *\n * 為什麼是「任一」而非「全部」:實務上 Field 一個 control area 通常只有一個 control,\n * 但若 consumer 同時放多個 child(例如 RadioGroup + 一段補充文字節點),只要其中有 block\n * primitive,整個 area 就應該以 block 模式佈局,確保第一行對齊正確。\n */\nfunction detectControlLayout(controlNodes: React.ReactNode[]): FieldControlLayout {\n for (const node of controlNodes) {\n if (!React.isValidElement(node)) continue\n const layout = (node.type as { fieldLayout?: FieldControlLayout } | null | undefined)?.fieldLayout\n if (layout === 'block') return 'block'\n }\n return 'inline'\n}\n\n// ── Field ───────────────────────────────────────────────────────────────────\n\nexport interface FieldProps extends Omit<React.HTMLAttributes<HTMLDivElement>, 'id'> {\n id?: string\n mode?: FieldMode\n /**\n * 視覺外殼(2026-05-05)。\n * - `default`(預設)— 含 border + bg(一般 form input)\n * - `bare` — 透明 variant,hover/focus reveal(cell-as-input substrate;VS Code/Figma toolbar idiom)\n *\n * 透傳機制:Field 一次宣告,所有 child Field control 自動繼承(per-control prop override 可覆寫)。\n */\n variant?: FieldVariant\n orientation?: FieldOrientation\n size?: FieldSize\n required?: boolean\n disabled?: boolean\n invalid?: boolean\n /**\n * Horizontal mode 的 label 欄寬度。支援任何 CSS length 值(\"120px\"、\"10rem\"、\"30%\"...)。\n * 預設 'auto' 由 label 內容撐開。\n */\n labelWidth?: string\n /**\n * Control area 佈局模型(逃生艙)。\n *\n * 預設由 Field 自動偵測——迭代全部 control child 的 `type.fieldLayout` static 屬性,\n * 任一宣告為 `'block'` 即整個 area 切 block(first-block-wins);全部缺宣告時視為 `'inline'`。\n *\n * 只有兩種情況需要手動指定:\n * 1. consumer 把自己手寫的 JSX(`<div>` / 函式元件)當 control,系統無法偵測——強制 `'block'`\n * 2. 想覆寫 primitive 的預設(如把 RadioGroup 強制 inline 呈現,罕見)\n */\n controlLayout?: FieldControlLayout\n}\n\n// ── FieldGroup Context(cascade horizontal labelWidth)──\n// 同一畫面多個 horizontal Field,label 寬度應統一對齊 → FieldGroup 提供 SSOT。\n// 下面 Field 組件自動 consume,consumer 可用 Field 的 labelWidth prop 覆寫單行。\ninterface FieldGroupContextValue {\n horizontalLabelWidth?: string\n}\n// code-quality-allow: long-function — foundational composite main body — 拆 sub-fn 會複雜化 local state / ref / context binding\nconst FieldGroupContext = React.createContext<FieldGroupContextValue>({})\n\nconst Field = React.forwardRef<HTMLDivElement, FieldProps>(\n (\n {\n id: idProp,\n mode = 'edit',\n variant = 'default',\n orientation = 'vertical',\n size = 'md',\n required = false,\n disabled: disabledProp = false,\n invalid = false,\n labelWidth,\n controlLayout: controlLayoutProp,\n className,\n style,\n children,\n ...props\n },\n ref\n ) => {\n const generatedId = React.useId()\n const id = idProp ?? generatedId\n const labelId = `${id}-label`\n const descriptionId = `${id}-description`\n const errorId = `${id}-error`\n\n // FieldGroup cascade:group 的 horizontalLabelWidth 是 fallback,單行 labelWidth 覆寫\n const groupCtx = React.useContext(FieldGroupContext)\n const effectiveLabelWidth = labelWidth ?? groupCtx.horizontalLabelWidth\n\n // mode=disabled 與 disabled prop 任一為 true 即視為 disabled\n const disabled = disabledProp || mode === 'disabled'\n\n // 把 children 依 slot 類型分組\n const labelNodes: React.ReactNode[] = []\n const controlNodes: React.ReactNode[] = []\n const descriptionNodes: React.ReactNode[] = []\n const errorNodes: React.ReactNode[] = []\n\n React.Children.forEach(children, (child) => {\n const slot = resolveSlotKind(child)\n if (slot === 'label') labelNodes.push(child)\n else if (slot === 'description') descriptionNodes.push(child)\n else if (slot === 'error') errorNodes.push(child)\n else controlNodes.push(child)\n })\n\n // 解析 control layout:consumer 顯式指定 > primitive 自我宣告 > 預設 inline\n const controlLayout: FieldControlLayout =\n controlLayoutProp ?? detectControlLayout(controlNodes)\n\n const contextValue = React.useMemo<FieldContextValue>(\n () => ({\n id,\n labelId,\n descriptionId,\n errorId,\n mode,\n variant,\n disabled,\n required,\n invalid,\n size,\n orientation,\n controlLayout,\n hasFieldWrapper: true,\n }),\n [id, labelId, descriptionId, errorId, mode, variant, disabled, required, invalid, size, orientation, controlLayout]\n )\n\n // Control area:兩種佈局模型,「第一行內容中線」都錨在 field-height/2,\n // 跟 FieldLabel 在 horizontal 模式下的 padding-top 公式自然對齊。\n //\n // - inline: min-h-field-{size} + items-center\n // 單行 control(Input、Button 等)中線置中於 min-h box。\n //\n // - block: flex-col + items-start(不設 min-h、不加 padding-top,內容自己決定高度)\n // 多行 control(RadioGroup 等),第一行中線由 block primitive 自帶 py 推到\n // field-height/2,後續 item 自然往下流。\n // Block control area 不加額外 paddingTop——block primitive(RadioGroup 等)\n // 的子元件(SelectionItem)已自帶 py = calc((field-height - 1lh) / 2),\n // 第一個 item 的文字自然落在 field-height/2。額外加 paddingTop 會 double padding。\n const controlArea =\n controlLayout === 'block' ? (\n <div\n className=\"flex flex-col items-start min-w-0\"\n data-field-slot=\"control\"\n data-field-control-layout=\"block\"\n >\n {controlNodes}\n </div>\n ) : (\n <div\n className={cn('flex items-center min-w-0', MIN_H_CLASS[size])}\n data-field-slot=\"control\"\n data-field-control-layout=\"inline\"\n >\n {controlNodes}\n </div>\n )\n\n // Horizontal:grid 兩欄,label 在左、content 欄堆疊(control → description → error)\n if (orientation === 'horizontal') {\n return (\n <FieldContext.Provider value={contextValue}>\n <div\n ref={ref}\n className={cn('grid gap-x-3 items-start', className)}\n style={{\n gridTemplateColumns: 'var(--field-label-width, auto) minmax(0, 1fr)',\n ...(effectiveLabelWidth !== undefined\n ? ({ ['--field-label-width' as string]: effectiveLabelWidth } as React.CSSProperties)\n : undefined),\n ...style,\n }}\n data-field-orientation=\"horizontal\"\n data-field-mode={mode}\n data-field-size={size}\n data-field-disabled={disabled ? '' : undefined}\n data-field-invalid={invalid ? '' : undefined}\n {...props}\n >\n {labelNodes}\n <div className=\"flex flex-col gap-1 min-w-0\">\n {controlArea}\n {descriptionNodes}\n {errorNodes}\n </div>\n </div>\n </FieldContext.Provider>\n )\n }\n\n // Vertical(預設):單欄 flex-col\n return (\n <FieldContext.Provider value={contextValue}>\n <div\n ref={ref}\n className={cn('flex flex-col gap-1 min-w-0', className)}\n style={style}\n data-field-orientation=\"vertical\"\n data-field-mode={mode}\n data-field-size={size}\n data-field-disabled={disabled ? '' : undefined}\n data-field-invalid={invalid ? '' : undefined}\n {...props}\n >\n {labelNodes}\n {controlArea}\n {descriptionNodes}\n {errorNodes}\n </div>\n </FieldContext.Provider>\n )\n }\n)\nField.displayName = 'Field'\n\n// ── FieldLabel ──────────────────────────────────────────────────────────────\n\nexport interface FieldLabelProps extends React.LabelHTMLAttributes<HTMLLabelElement> {\n /**\n * 強制渲染 required 星號(覆寫 Field context 的 required)。\n * 若未設定,預設讀 context。\n */\n required?: boolean\n /**\n * 在 label 文字後方顯示 info icon (ℹ),hover 出現 tooltip 說明。\n * 傳 string → tooltip 內容。\n *\n * Info icon 用 inline action pattern(補充工具,視覺退後),\n * 因為 label 的 primary interaction 是 input,info 是補充說明。\n */\n info?: string\n}\n\n// code-quality-allow: long-function — foundational composite main body — 拆 sub-fn 會複雜化 local state / ref / context binding\nconst FieldLabel = React.forwardRef<HTMLLabelElement, FieldLabelProps>(\n ({ className, required: requiredProp, info, htmlFor: htmlForProp, style, children, ...props }, ref) => {\n const ctx = useFieldContext()\n const required = requiredProp ?? ctx?.required ?? false\n const disabled = ctx?.disabled ?? false\n const htmlFor = htmlForProp ?? ctx?.id\n const isHorizontal = ctx?.orientation === 'horizontal'\n const controlLayout = ctx?.controlLayout ?? 'inline'\n const size: FieldSize = ctx?.size ?? 'md'\n\n // Horizontal 模式對齊策略 — 依 controlLayout 分兩套 (CSS-only, 不需 JS 測量)\n //\n // ── Inline control (Input / Button / Switch / SegmentedControl) ──\n // Control 有固定單行高度 = field-height,可以對齊中線。\n // 策略: min-h-field-{size} + flex flex-col + justify-content: center\n //\n // 1) 短 label (總高 ≤ field-height):\n // min-h 生效 → 容器 = field-height → justify-center 把 label 垂直置中\n // 第一行 top = (field-height - 1lh)/2 → 第一行中線對齊 control 中線 ✓\n // 2) 長 label (總高 > field-height):\n // min-h 被內容撐大 → 容器 = label 總高 → justify-center 無作用(內容已填滿)\n // 第一行 top = 0 → label top 對齊 control top ✓\n //\n // ── Block control (RadioGroup / CheckboxGroup) ──\n // Control 是多行群組,沒有「整體中線」可以對齊;錨點是「第一個 item 的第一行\n // 中線永遠在 field-height/2」,由 SelectionItem 的 py 維持。\n // 策略: padding-top = (field-height - 1lh)/2 — 把 label 第一行推到同樣位置。\n //\n // 這個策略對任何 label 長度都正確:label 第一行永遠與第一個 item 第一行對齊,\n // label 超出 control 時往下流(因為 block control 通常本來就很高,不會有\n // inline 模式那種「label 比 control 高」的視覺問題)。\n //\n // 內層 <span>: 只有 inline 策略需要(flex-col 會把 * 星號和 label 文字縱向堆疊,\n // 必須包一層讓兩者 inline 同行)。block 策略可以不包,但為了 DOM 一致性一律包。\n const horizontalInlineClass =\n isHorizontal && controlLayout === 'inline'\n ? cn('flex flex-col justify-center', MIN_H_CLASS[size])\n : undefined\n\n const horizontalBlockStyle: React.CSSProperties | undefined =\n isHorizontal && controlLayout === 'block'\n ? { paddingTop: `calc((${FIELD_HEIGHT_VAR[size]} - 1lh) / 2)` }\n : undefined\n\n return (\n <label\n ref={ref}\n id={ctx?.labelId}\n htmlFor={htmlFor}\n className={cn(\n FIELD_TEXT_CLASS,\n 'font-normal select-none',\n disabled ? 'text-fg-disabled' : 'text-foreground',\n horizontalInlineClass,\n className\n )}\n style={{ ...horizontalBlockStyle, ...style }}\n data-field-slot=\"label\"\n data-field-disabled={disabled ? '' : undefined}\n // 2026-06-10 a11y:styled-disabled label 必明告 inactive(WCAG 1.4.3 inactive-UI 豁免需可機判;\n // axe 對無 aria-disabled 的 fg-disabled 文字誤報 color-contrast — deep-audit 抓 8 筆)\n aria-disabled={disabled || undefined}\n {...props}\n >\n <span className=\"inline-flex items-center gap-1\">\n <span>\n {required && (\n <span\n aria-hidden=\"true\"\n className={disabled ? 'text-fg-disabled' : 'text-fg-muted'}\n >\n *\n </span>\n )}\n {children}\n </span>\n {info && !disabled && (\n <Tooltip>\n <TooltipTrigger asChild>\n <button\n type=\"button\"\n aria-label={info}\n className=\"inline-flex items-center text-fg-muted hover:text-fg-secondary bg-transparent border-0 p-0 cursor-pointer\"\n >\n <InfoIcon size={16} aria-hidden />\n </button>\n </TooltipTrigger>\n <TooltipContent>{info}</TooltipContent>\n </Tooltip>\n )}\n </span>\n </label>\n )\n }\n)\nFieldLabel.displayName = 'FieldLabel'\n\n// ── FieldDescription ────────────────────────────────────────────────────────\n\nconst FieldDescription = React.forwardRef<\n HTMLParagraphElement,\n React.HTMLAttributes<HTMLParagraphElement>\n>(({ className, children, id: idProp, ...props }, ref) => {\n const ctx = useFieldContext()\n const disabled = ctx?.disabled ?? false\n\n return (\n <p\n ref={ref}\n id={idProp ?? ctx?.descriptionId}\n className={cn(\n FIELD_TEXT_CLASS,\n disabled ? 'text-fg-disabled' : 'text-fg-secondary',\n className\n )}\n data-field-slot=\"description\"\n {...props}\n >\n {children}\n </p>\n )\n})\nFieldDescription.displayName = 'FieldDescription'\n\n// ── FieldError ──────────────────────────────────────────────────────────────\n\nconst FieldError = React.forwardRef<\n HTMLParagraphElement,\n React.HTMLAttributes<HTMLParagraphElement>\n>(({ className, children, id: idProp, ...props }, ref) => {\n const ctx = useFieldContext()\n\n // 無內容不渲染,避免空殼佔位\n if (children == null || children === false || children === '') return null\n\n return (\n <p\n ref={ref}\n id={idProp ?? ctx?.errorId}\n className={cn(FIELD_TEXT_CLASS, 'text-error-text', className)}\n data-field-slot=\"error\"\n role=\"alert\"\n {...props}\n >\n {children}\n </p>\n )\n})\nFieldError.displayName = 'FieldError'\n\n// ── FieldGroup ──────────────────────────────────────────────────────────────\n// 垂直堆疊多個 Field,共用 gap 節奏。\n// 用於表單中多個欄位排列。\n\nexport interface FieldGroupProps extends React.HTMLAttributes<HTMLDivElement> {\n /** Field 之間的垂直間距,預設 'normal'(gap-4) */\n gap?: 'compact' | 'normal' | 'loose'\n /**\n * 同一 group 內所有 horizontal Field 共用的 label 欄寬度。\n *\n * 支援任何 CSS length(`\"140px\"` / `\"10rem\"` / `\"30%\"` 等)。預設不指定——\n * 每個 Field 自動以 label 內容撐開(容易歪七扭八)。\n *\n * 世界級 idiom:macOS System Settings / iOS Settings / GitHub Settings 的\n * setting list 一律 label 固定寬、control 右對齊,列與列整齊對齊。\n *\n * 單一 Field 可以用自己的 `labelWidth` prop 覆寫 cascade 值。\n */\n horizontalLabelWidth?: string\n}\n\nconst FieldGroup = React.forwardRef<HTMLDivElement, FieldGroupProps>(\n ({ className, gap = 'normal', horizontalLabelWidth, ...props }, ref) => {\n const gapClass = gap === 'compact' ? 'gap-3' : gap === 'loose' ? 'gap-6' : 'gap-4'\n const groupCtxValue = React.useMemo(\n () => ({ horizontalLabelWidth }),\n [horizontalLabelWidth],\n )\n return (\n <FieldGroupContext.Provider value={groupCtxValue}>\n <div\n ref={ref}\n className={cn('flex flex-col min-w-0', gapClass, className)}\n data-field-group=\"\"\n {...props}\n />\n </FieldGroupContext.Provider>\n )\n }\n)\nFieldGroup.displayName = 'FieldGroup'\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 fieldMeta = {\n component: 'Field',\n family: null, // non-family composite / overlay / layout\n variants: {\n\n },\n sizes: {\n\n },\n states: ['default', 'hover', 'active', 'focus-visible', 'disabled'],\n tokens: {\n bg: [],\n fg: ['text-error-text', 'text-fg-disabled', 'text-fg-muted', 'text-fg-secondary', 'text-foreground'],\n ring: [],\n },\n} as const\n\nexport { Field, FieldLabel, FieldDescription, FieldError, FieldGroup }\n\n// form-validation.spec.md 可執行層(per-component index gen 只 re-export 主檔,故經此公開)\nexport { useFormValidation } from './use-form-validation'\nexport type { UseFormValidationOptions, UseFormValidationReturn, FormFieldInputProps } from './use-form-validation'\n"],"names":["InfoIcon"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"field.js","sources":["../../../src/components/Field/field.tsx"],"sourcesContent":["// code-quality-allow: file-size — foundational composite(Field + FieldLabel + FieldDescription + FieldError + context + 8 layout variants),拆檔會讓 Field 家族互相 import 循環\nimport * as React from 'react'\nimport { Info as InfoIcon } from 'lucide-react'\nimport { cn } from '@/lib/utils'\nimport { Tooltip, TooltipTrigger, TooltipContent } from '@/design-system/components/Tooltip/tooltip'\n\n/**\n * Field — 表單欄位佈局容器(shadcn Field 風格)\n *\n * ── 定位 ────────────────────────────────────────────────────────────────\n * Field 只負責 **佈局 + 狀態 context**,不擁有任何資料型別邏輯。\n * 每個資料型別對應的 Control(Input、NumberInput、Checkbox、Switch 等)\n * 維持自己的 edit / readonly / disabled 三態,Field 透過 context 把\n * mode / disabled / required / invalid / id 傳給子元件,由子元件決定\n * 如何反映。\n *\n * ── 結構 ────────────────────────────────────────────────────────────────\n * <Field orientation=\"vertical | horizontal\" labelWidth=\"120px\">\n * <FieldLabel>姓名</FieldLabel>\n * <Input value={...} onChange={...} /> ← Control(任何非 label/desc/error 的 child)\n * <FieldDescription>...</FieldDescription>\n * <FieldError>{errors.name}</FieldError>\n * </Field>\n *\n * Control 會自動包在 control area slot(min-h-field-* + items-center),\n * 確保 Checkbox / Switch / Radio 等高度 < field-height 的 primitive\n * 垂直對齊 Input 中線;Input 等自身為 field-height 的 primitive 填滿。\n *\n * ── Horizontal mode 的 label 垂直對齊 ───────────────────────────────────\n * FieldLabel 在 horizontal 模式下使用公式:\n * padding-top: calc((var(--field-height-{size}) - 1lh) / 2)\n *\n * 單行 label → 文字第一行與 input 中線對齊(視覺置中)\n * 多行 label → 第一行仍與 input 中線對齊,其餘行往下流(label 高度超過\n * input 時視覺上仍保持與 input 內容同一基準線)\n *\n * 此公式 tracks field-height 和 line-height 的變動,size 切換或字體\n * 調整時自動連動,不需 JS 測量。\n *\n * ── Horizontal mode 的 label 寬度 ───────────────────────────────────────\n * 透過 labelWidth prop → --field-label-width CSS variable,可以是任何\n * CSS length(\"120px\"、\"10rem\"、\"30%\" 等)。預設 \"auto\" 由 label 內容撐開。\n *\n * ── Required 星號 ──────────────────────────────────────────────────────\n * Field 的 required prop 會透過 context 傳給 FieldLabel 自動渲染 *,\n * 星號為 neutral-7(fg-muted),貼齊 label 文字(無 gap),disabled\n * 時降為 fg-disabled。也可在個別 FieldLabel 覆寫。\n */\n\n// ── Types & Context ──\n// Context 定義在 field-context.ts(打斷 circular import)。\n// field.tsx 只 import 不 re-export——consumer 直接從 field-context.ts import useFieldContext。\n\nimport type { FieldMode, FieldVariant, FieldOrientation, FieldSize, FieldControlLayout, FieldContextValue } from './field-context'\nimport { FieldContext, useFieldContext } from './field-context'\n\n// ── Internal helpers ────────────────────────────────────────────────────────\n\nconst MIN_H_CLASS: Record<FieldSize, string> = {\n sm: 'min-h-field-sm',\n md: 'min-h-field-md',\n lg: 'min-h-field-lg',\n}\n\nconst FIELD_HEIGHT_VAR: Record<FieldSize, string> = {\n sm: 'var(--field-height-sm)',\n md: 'var(--field-height-md)',\n lg: 'var(--field-height-lg)',\n}\n\n// Label / Description / Error 的字體固定 text-body (14px),不隨 field size 變。\n// 世界級共識:field size 只影響 input 高度,不影響表單佈局元素的 typography。\nconst FIELD_TEXT_CLASS = 'text-body'\n\ntype SlotKind = 'label' | 'description' | 'error' | 'control'\n\nfunction resolveSlotKind(node: React.ReactNode): SlotKind {\n if (!React.isValidElement(node)) return 'control'\n const displayName = (node.type as { displayName?: string } | null | undefined)?.displayName\n if (displayName === 'FieldLabel') return 'label'\n if (displayName === 'FieldDescription') return 'description'\n if (displayName === 'FieldError') return 'error'\n return 'control'\n}\n\n/**\n * 偵測 control children 的 fieldLayout——任一 control 宣告為 'block' 即整個 area 切 block 模式。\n *\n * Convention:block primitive 在自己的元件檔案掛 static `fieldLayout = 'block'` 屬性,\n * Field 在 render 時讀 `child.type.fieldLayout`。預設 'inline'。\n *\n * 為什麼是「任一」而非「全部」:實務上 Field 一個 control area 通常只有一個 control,\n * 但若 consumer 同時放多個 child(例如 RadioGroup + 一段補充文字節點),只要其中有 block\n * primitive,整個 area 就應該以 block 模式佈局,確保第一行對齊正確。\n */\nfunction detectControlLayout(controlNodes: React.ReactNode[]): FieldControlLayout {\n for (const node of controlNodes) {\n if (!React.isValidElement(node)) continue\n const layout = (node.type as { fieldLayout?: FieldControlLayout } | null | undefined)?.fieldLayout\n if (layout === 'block') return 'block'\n }\n return 'inline'\n}\n\n// ── Field ───────────────────────────────────────────────────────────────────\n\nexport interface FieldProps extends Omit<React.HTMLAttributes<HTMLDivElement>, 'id'> {\n id?: string\n mode?: FieldMode\n /**\n * 視覺外殼(2026-05-05)。\n * - `default`(預設)— 含 border + bg(一般 form input)\n * - `bare` — 透明 variant,hover/focus reveal(cell-as-input substrate;VS Code/Figma toolbar idiom)\n *\n * 透傳機制:Field 一次宣告,所有 child Field control 自動繼承(per-control prop override 可覆寫)。\n */\n variant?: FieldVariant\n orientation?: FieldOrientation\n size?: FieldSize\n required?: boolean\n disabled?: boolean\n invalid?: boolean\n /**\n * Horizontal mode 的 label 欄寬度。支援任何 CSS length 值(\"120px\"、\"10rem\"、\"30%\"...)。\n * 預設 'auto' 由 label 內容撐開。\n */\n labelWidth?: string\n /**\n * Control area 佈局模型(逃生艙)。\n *\n * 預設由 Field 自動偵測——迭代全部 control child 的 `type.fieldLayout` static 屬性,\n * 任一宣告為 `'block'` 即整個 area 切 block(first-block-wins);全部缺宣告時視為 `'inline'`。\n *\n * 只有兩種情況需要手動指定:\n * 1. consumer 把自己手寫的 JSX(`<div>` / 函式元件)當 control,系統無法偵測——強制 `'block'`\n * 2. 想覆寫 primitive 的預設(如把 RadioGroup 強制 inline 呈現,罕見)\n */\n controlLayout?: FieldControlLayout\n}\n\n// ── FieldGroup Context(cascade horizontal labelWidth)──\n// 同一畫面多個 horizontal Field,label 寬度應統一對齊 → FieldGroup 提供 SSOT。\n// 下面 Field 組件自動 consume,consumer 可用 Field 的 labelWidth prop 覆寫單行。\ninterface FieldGroupContextValue {\n horizontalLabelWidth?: string\n}\n// code-quality-allow: long-function — foundational composite main body — 拆 sub-fn 會複雜化 local state / ref / context binding\nconst FieldGroupContext = React.createContext<FieldGroupContextValue>({})\n\nconst Field = React.forwardRef<HTMLDivElement, FieldProps>(\n (\n {\n id: idProp,\n mode = 'edit',\n variant = 'default',\n orientation = 'vertical',\n size = 'md',\n required = false,\n disabled: disabledProp = false,\n invalid = false,\n labelWidth,\n controlLayout: controlLayoutProp,\n className,\n style,\n children,\n ...props\n },\n ref\n ) => {\n const generatedId = React.useId()\n const id = idProp ?? generatedId\n const labelId = `${id}-label`\n const descriptionId = `${id}-description`\n const errorId = `${id}-error`\n\n // FieldGroup cascade:group 的 horizontalLabelWidth 是 fallback,單行 labelWidth 覆寫\n const groupCtx = React.useContext(FieldGroupContext)\n const effectiveLabelWidth = labelWidth ?? groupCtx.horizontalLabelWidth\n\n // mode=disabled 與 disabled prop 任一為 true 即視為 disabled\n const disabled = disabledProp || mode === 'disabled'\n\n // 把 children 依 slot 類型分組\n const labelNodes: React.ReactNode[] = []\n const controlNodes: React.ReactNode[] = []\n const descriptionNodes: React.ReactNode[] = []\n const errorNodes: React.ReactNode[] = []\n\n React.Children.forEach(children, (child) => {\n const slot = resolveSlotKind(child)\n if (slot === 'label') labelNodes.push(child)\n else if (slot === 'description') descriptionNodes.push(child)\n else if (slot === 'error') errorNodes.push(child)\n else controlNodes.push(child)\n })\n\n // 解析 control layout:consumer 顯式指定 > primitive 自我宣告 > 預設 inline\n const controlLayout: FieldControlLayout =\n controlLayoutProp ?? detectControlLayout(controlNodes)\n\n const contextValue = React.useMemo<FieldContextValue>(\n () => ({\n id,\n labelId,\n descriptionId,\n errorId,\n mode,\n variant,\n disabled,\n required,\n invalid,\n size,\n orientation,\n controlLayout,\n hasFieldWrapper: true,\n }),\n [id, labelId, descriptionId, errorId, mode, variant, disabled, required, invalid, size, orientation, controlLayout]\n )\n\n // Control area:兩種佈局模型,「第一行內容中線」都錨在 field-height/2,\n // 跟 FieldLabel 在 horizontal 模式下的 padding-top 公式自然對齊。\n //\n // - inline: min-h-field-{size} + items-center\n // 單行 control(Input、Button 等)中線置中於 min-h box。\n //\n // - block: flex-col + items-start(不設 min-h、不加 padding-top,內容自己決定高度)\n // 多行 control(RadioGroup 等),第一行中線由 block primitive 自帶 py 推到\n // field-height/2,後續 item 自然往下流。\n // Block control area 不加額外 paddingTop——block primitive(RadioGroup 等)\n // 的子元件(SelectionItem)已自帶 py = calc((field-height - 1lh) / 2),\n // 第一個 item 的文字自然落在 field-height/2。額外加 paddingTop 會 double padding。\n const controlArea =\n controlLayout === 'block' ? (\n <div\n className=\"flex flex-col items-start min-w-0\"\n data-field-slot=\"control\"\n data-field-control-layout=\"block\"\n >\n {controlNodes}\n </div>\n ) : (\n <div\n className={cn('flex items-center min-w-0', MIN_H_CLASS[size])}\n data-field-slot=\"control\"\n data-field-control-layout=\"inline\"\n >\n {controlNodes}\n </div>\n )\n\n // Horizontal:grid 兩欄,label 在左、content 欄堆疊(control → description → error)\n if (orientation === 'horizontal') {\n return (\n <FieldContext.Provider value={contextValue}>\n <div\n ref={ref}\n className={cn('grid gap-x-3 items-start', className)}\n style={{\n gridTemplateColumns: 'var(--field-label-width, auto) minmax(0, 1fr)',\n ...(effectiveLabelWidth !== undefined\n ? ({ ['--field-label-width' as string]: effectiveLabelWidth } as React.CSSProperties)\n : undefined),\n ...style,\n }}\n data-field-orientation=\"horizontal\"\n data-field-mode={mode}\n data-field-size={size}\n data-field-disabled={disabled ? '' : undefined}\n data-field-invalid={invalid ? '' : undefined}\n {...props}\n >\n {labelNodes}\n <div className=\"flex flex-col gap-1 min-w-0\">\n {controlArea}\n {descriptionNodes}\n {errorNodes}\n </div>\n </div>\n </FieldContext.Provider>\n )\n }\n\n // Vertical(預設):單欄 flex-col\n return (\n <FieldContext.Provider value={contextValue}>\n <div\n ref={ref}\n className={cn('flex flex-col gap-1 min-w-0', className)}\n style={style}\n data-field-orientation=\"vertical\"\n data-field-mode={mode}\n data-field-size={size}\n data-field-disabled={disabled ? '' : undefined}\n data-field-invalid={invalid ? '' : undefined}\n {...props}\n >\n {labelNodes}\n {controlArea}\n {descriptionNodes}\n {errorNodes}\n </div>\n </FieldContext.Provider>\n )\n }\n)\nField.displayName = 'Field'\n\n// ── FieldLabel ──────────────────────────────────────────────────────────────\n\nexport interface FieldLabelProps extends React.LabelHTMLAttributes<HTMLLabelElement> {\n /**\n * 強制渲染 required 星號(覆寫 Field context 的 required)。\n * 若未設定,預設讀 context。\n */\n required?: boolean\n /**\n * 在 label 文字後方顯示 info icon (ℹ),hover 出現 tooltip 說明。\n * 傳 string → tooltip 內容。\n *\n * Info icon 用 inline action pattern(補充工具,視覺退後),\n * 因為 label 的 primary interaction 是 input,info 是補充說明。\n */\n info?: string\n}\n\n// code-quality-allow: long-function — foundational composite main body — 拆 sub-fn 會複雜化 local state / ref / context binding\nconst FieldLabel = React.forwardRef<HTMLLabelElement, FieldLabelProps>(\n ({ className, required: requiredProp, info, htmlFor: htmlForProp, style, children, ...props }, ref) => {\n const ctx = useFieldContext()\n const required = requiredProp ?? ctx?.required ?? false\n const disabled = ctx?.disabled ?? false\n const htmlFor = htmlForProp ?? ctx?.id\n const isHorizontal = ctx?.orientation === 'horizontal'\n const controlLayout = ctx?.controlLayout ?? 'inline'\n const size: FieldSize = ctx?.size ?? 'md'\n\n // Horizontal 模式對齊策略 — 依 controlLayout 分兩套 (CSS-only, 不需 JS 測量)\n //\n // ── Inline control (Input / Button / Switch / SegmentedControl) ──\n // Control 有固定單行高度 = field-height,可以對齊中線。\n // 策略: min-h-field-{size} + flex flex-col + justify-content: center\n //\n // 1) 短 label (總高 ≤ field-height):\n // min-h 生效 → 容器 = field-height → justify-center 把 label 垂直置中\n // 第一行 top = (field-height - 1lh)/2 → 第一行中線對齊 control 中線 ✓\n // 2) 長 label (總高 > field-height):\n // min-h 被內容撐大 → 容器 = label 總高 → justify-center 無作用(內容已填滿)\n // 第一行 top = 0 → label top 對齊 control top ✓\n //\n // ── Block control (RadioGroup / CheckboxGroup) ──\n // Control 是多行群組,沒有「整體中線」可以對齊;錨點是「第一個 item 的第一行\n // 中線永遠在 field-height/2」,由 SelectionItem 的 py 維持。\n // 策略: padding-top = (field-height - 1lh)/2 — 把 label 第一行推到同樣位置。\n //\n // 這個策略對任何 label 長度都正確:label 第一行永遠與第一個 item 第一行對齊,\n // label 超出 control 時往下流(因為 block control 通常本來就很高,不會有\n // inline 模式那種「label 比 control 高」的視覺問題)。\n //\n // 內層 <span>: 只有 inline 策略需要(flex-col 會把 * 星號和 label 文字縱向堆疊,\n // 必須包一層讓兩者 inline 同行)。block 策略可以不包,但為了 DOM 一致性一律包。\n const horizontalInlineClass =\n isHorizontal && controlLayout === 'inline'\n ? cn('flex flex-col justify-center', MIN_H_CLASS[size])\n : undefined\n\n const horizontalBlockStyle: React.CSSProperties | undefined =\n isHorizontal && controlLayout === 'block'\n ? { paddingTop: `calc((${FIELD_HEIGHT_VAR[size]} - 1lh) / 2)` }\n : undefined\n\n return (\n <label\n ref={ref}\n id={ctx?.labelId}\n htmlFor={htmlFor}\n className={cn(\n FIELD_TEXT_CLASS,\n 'font-normal select-none',\n disabled ? 'text-fg-disabled' : 'text-foreground',\n horizontalInlineClass,\n className\n )}\n style={{ ...horizontalBlockStyle, ...style }}\n data-field-slot=\"label\"\n data-field-disabled={disabled ? '' : undefined}\n // 2026-06-10 a11y:styled-disabled label 必明告 inactive(WCAG 1.4.3 inactive-UI 豁免需可機判;\n // axe 對無 aria-disabled 的 fg-disabled 文字誤報 color-contrast — deep-audit 抓 8 筆)\n aria-disabled={disabled || undefined}\n {...props}\n >\n <span className=\"inline-flex items-center gap-1\">\n <span>\n {required && (\n <span\n aria-hidden=\"true\"\n className={disabled ? 'text-fg-disabled' : 'text-fg-muted'}\n >\n *\n </span>\n )}\n {children}\n </span>\n {info && !disabled && (\n <Tooltip>\n <TooltipTrigger asChild>\n <button\n type=\"button\"\n aria-label={info}\n className=\"inline-flex items-center text-fg-muted hover:text-fg-secondary bg-transparent border-0 p-0 cursor-pointer\"\n >\n <InfoIcon size={16} aria-hidden />\n </button>\n </TooltipTrigger>\n <TooltipContent>{info}</TooltipContent>\n </Tooltip>\n )}\n </span>\n </label>\n )\n }\n)\nFieldLabel.displayName = 'FieldLabel'\n\n// ── FieldDescription ────────────────────────────────────────────────────────\n\nconst FieldDescription = React.forwardRef<\n HTMLParagraphElement,\n React.HTMLAttributes<HTMLParagraphElement>\n>(({ className, children, id: idProp, ...props }, ref) => {\n const ctx = useFieldContext()\n const disabled = ctx?.disabled ?? false\n\n return (\n <p\n ref={ref}\n id={idProp ?? ctx?.descriptionId}\n className={cn(\n FIELD_TEXT_CLASS,\n disabled ? 'text-fg-disabled' : 'text-fg-secondary',\n className\n )}\n data-field-slot=\"description\"\n {...props}\n >\n {children}\n </p>\n )\n})\nFieldDescription.displayName = 'FieldDescription'\n\n// ── FieldError ──────────────────────────────────────────────────────────────\n\nconst FieldError = React.forwardRef<\n HTMLParagraphElement,\n React.HTMLAttributes<HTMLParagraphElement>\n>(({ className, children, id: idProp, ...props }, ref) => {\n const ctx = useFieldContext()\n\n // 無內容不渲染,避免空殼佔位\n if (children == null || children === false || children === '') return null\n\n return (\n <p\n ref={ref}\n id={idProp ?? ctx?.errorId}\n className={cn(FIELD_TEXT_CLASS, 'text-error-text', className)}\n data-field-slot=\"error\"\n role=\"alert\"\n {...props}\n >\n {children}\n </p>\n )\n})\nFieldError.displayName = 'FieldError'\n\n// ── FieldGroup ──────────────────────────────────────────────────────────────\n// 垂直堆疊多個 Field,共用 gap 節奏。\n// 用於表單中多個欄位排列。\n\nexport interface FieldGroupProps extends React.HTMLAttributes<HTMLDivElement> {\n /** Field 之間的垂直間距,預設 'normal'(gap-4) */\n gap?: 'compact' | 'normal' | 'loose'\n /**\n * 同一 group 內所有 horizontal Field 共用的 label 欄寬度。\n *\n * 支援任何 CSS length(`\"140px\"` / `\"10rem\"` / `\"30%\"` 等)。預設不指定——\n * 每個 Field 自動以 label 內容撐開(容易歪七扭八)。\n *\n * 世界級 idiom:macOS System Settings / iOS Settings / GitHub Settings 的\n * setting list 一律 label 固定寬、control 右對齊,列與列整齊對齊。\n *\n * 單一 Field 可以用自己的 `labelWidth` prop 覆寫 cascade 值。\n */\n horizontalLabelWidth?: string\n}\n\nconst FieldGroup = React.forwardRef<HTMLDivElement, FieldGroupProps>(\n ({ className, gap = 'normal', horizontalLabelWidth, ...props }, ref) => {\n const gapClass = gap === 'compact' ? 'gap-3' : gap === 'loose' ? 'gap-6' : 'gap-4'\n const groupCtxValue = React.useMemo(\n () => ({ horizontalLabelWidth }),\n [horizontalLabelWidth],\n )\n return (\n <FieldGroupContext.Provider value={groupCtxValue}>\n <div\n ref={ref}\n className={cn('flex flex-col min-w-0', gapClass, className)}\n data-field-group=\"\"\n {...props}\n />\n </FieldGroupContext.Provider>\n )\n }\n)\nFieldGroup.displayName = 'FieldGroup'\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 fieldMeta = {\n component: 'Field',\n family: null, // non-family composite / overlay / layout\n variants: {\n\n },\n sizes: {\n\n },\n // 'active' 移除 — field-wrapper compoundVariants 無按壓態(TimePicker 2026-07-04 audit 同結論)(2026-07-07 詞彙統一 DS-wide 按壓訊號盤點:檔內 0 active: utility / 0 *-active token)。\n states: ['default', 'hover', 'focus-visible', 'disabled'],\n tokens: {\n bg: [],\n fg: ['text-error-text', 'text-fg-disabled', 'text-fg-muted', 'text-fg-secondary', 'text-foreground'],\n ring: [],\n },\n} as const\n\nexport { Field, FieldLabel, FieldDescription, FieldError, FieldGroup }\n\n// form-validation.spec.md 可執行層(per-component index gen 只 re-export 主檔,故經此公開)\nexport { useFormValidation } from './use-form-validation'\nexport type { UseFormValidationOptions, UseFormValidationReturn, FormFieldInputProps } from './use-form-validation'\n"],"names":["InfoIcon"],"mappings":"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"}
|
|
@@ -27,6 +27,8 @@
|
|
|
27
27
|
* 8 Anchor 到第一個錯誤 → focus + scrollIntoView({block:'center'});每次 submit 重算
|
|
28
28
|
* 9 Async / 跨欄位 defer 到 submit → onSubmit 回傳 field-keyed errors → 同 8 anchor
|
|
29
29
|
* + Submit button:Create 永遠 enabled / Update disabled-until-dirty → `submitDisabled`
|
|
30
|
+
* + Double-submit 防護(2026-07-05 D4):await onSubmit 期間重入直接忽略;`isSubmitting`
|
|
31
|
+
* 暴露餵 Button loading / disabled;onSubmit reject 先復位再原樣上拋(不吞錯)
|
|
30
32
|
*
|
|
31
33
|
* ── v1 邊界(spec「可執行層」段 documented)──
|
|
32
34
|
* - getInputProps 支援 value/onChange 型控件(Input / Textarea / NumberInput / Select /
|
|
@@ -73,8 +75,10 @@ export interface UseFormValidationReturn<T extends FieldValues> {
|
|
|
73
75
|
errors: Partial<Record<keyof T, string>>;
|
|
74
76
|
/** 任一欄位偏離 initialValues(深比對,還原回原值 = false) */
|
|
75
77
|
isDirty: boolean;
|
|
76
|
-
/** Submit button disabled 狀態(intent 驅動,見 options.intent) */
|
|
78
|
+
/** Submit button disabled 狀態(intent 驅動,見 options.intent;submit 進行中一併 disabled) */
|
|
77
79
|
submitDisabled: boolean;
|
|
80
|
+
/** Submit 進行中(await onSubmit 期間 true)— 餵 Button loading;double-submit 防護的 state 面 */
|
|
81
|
+
isSubmitting: boolean;
|
|
78
82
|
/** Spread 到 value/onChange 型控件:`<Input {...form.getInputProps('name')} />` */
|
|
79
83
|
getInputProps: <K extends keyof T & string>(name: K) => FormFieldInputProps<T[K]>;
|
|
80
84
|
/** 接 `<form onSubmit={form.handleSubmit}>`(規則 7/8/9) */
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"use-form-validation.d.ts","sourceRoot":"","sources":["../../../src/components/Field/use-form-validation.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"use-form-validation.d.ts","sourceRoot":"","sources":["../../../src/components/Field/use-form-validation.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAsCG;AACH,OAAO,KAAK,KAAK,MAAM,OAAO,CAAA;AAE9B,OAAO,KAAK,EAAE,WAAW,EAAkC,MAAM,iBAAiB,CAAA;AAElF,MAAM,WAAW,wBAAwB,CAAC,CAAC,SAAS,WAAW;IAC7D,yCAAyC;IACzC,aAAa,EAAE,CAAC,CAAA;IAChB;;;;OAIG;IACH,MAAM,CAAC,EAAE,QAAQ,GAAG,QAAQ,CAAA;IAC5B;;;;OAIG;IACH,QAAQ,CAAC,EAAE,OAAO,CAAC,MAAM,CAAC,MAAM,CAAC,EAAE,CAAC,KAAK,EAAE,CAAC,CAAC,MAAM,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,KAAK,MAAM,GAAG,SAAS,CAAC,CAAC,CAAA;IACzF;;;;OAIG;IACH,QAAQ,EAAE,CAAC,MAAM,EAAE,CAAC,KAAK,IAAI,GAAG,OAAO,CAAC,MAAM,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC,CAAC,GAAG,OAAO,CAAC,IAAI,GAAG,OAAO,CAAC,MAAM,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC,CAAC,CAAC,CAAA;CACpH;AAED,MAAM,WAAW,mBAAmB,CAAC,CAAC,GAAG,OAAO;IAC9C,IAAI,EAAE,MAAM,CAAA;IACZ,KAAK,EAAE,CAAC,CAAA;IACR,QAAQ,EAAE,CAAC,YAAY,EAAE,OAAO,KAAK,IAAI,CAAA;IACzC,MAAM,EAAE,MAAM,IAAI,CAAA;IAClB,SAAS,EAAE,CAAC,CAAC,EAAE,KAAK,CAAC,aAAa,KAAK,IAAI,CAAA;CAC5C;AAED,MAAM,WAAW,uBAAuB,CAAC,CAAC,SAAS,WAAW;IAC5D,gBAAgB;IAChB,MAAM,EAAE,CAAC,CAAA;IACT,6DAA6D;IAC7D,MAAM,EAAE,OAAO,CAAC,MAAM,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC,CAAC,CAAA;IACxC,8CAA8C;IAC9C,OAAO,EAAE,OAAO,CAAA;IAChB,kFAAkF;IAClF,cAAc,EAAE,OAAO,CAAA;IACvB,qFAAqF;IACrF,YAAY,EAAE,OAAO,CAAA;IACrB,8EAA8E;IAC9E,aAAa,EAAE,CAAC,CAAC,SAAS,MAAM,CAAC,GAAG,MAAM,EAAE,IAAI,EAAE,CAAC,KAAK,mBAAmB,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAA;IACjF,wDAAwD;IACxD,YAAY,EAAE,CAAC,CAAC,CAAC,EAAE,KAAK,CAAC,SAAS,KAAK,OAAO,CAAC,IAAI,CAAC,CAAA;IACpD,4CAA4C;IAC5C,KAAK,EAAE,MAAM,IAAI,CAAA;IACjB,4DAA4D;IAC5D,aAAa,EAAE,CAAC,IAAI,EAAE,MAAM,CAAC,GAAG,MAAM,EAAE,KAAK,EAAE,OAAO,KAAK,IAAI,CAAA;CAChE;AAoCD,wBAAgB,iBAAiB,CAAC,CAAC,SAAS,WAAW,EACrD,OAAO,EAAE,wBAAwB,CAAC,CAAC,CAAC,GACnC,uBAAuB,CAAC,CAAC,CAAC,CA0I5B"}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import * as React from "react";
|
|
2
|
-
import { useForm } from "
|
|
2
|
+
import { useForm } from "react-hook-form";
|
|
3
3
|
function extractValue(eventOrValue) {
|
|
4
4
|
if (eventOrValue && typeof eventOrValue === "object" && "target" in eventOrValue && eventOrValue.target && typeof eventOrValue.target === "object" && "value" in eventOrValue.target) {
|
|
5
5
|
return eventOrValue.target.value;
|
|
@@ -7,13 +7,11 @@ function extractValue(eventOrValue) {
|
|
|
7
7
|
return eventOrValue;
|
|
8
8
|
}
|
|
9
9
|
function focusFirstError(errorNames) {
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
return;
|
|
16
|
-
}
|
|
10
|
+
const els = errorNames.map((name) => document.getElementsByName(name)[0]).filter((el2) => Boolean(el2)).sort((a, b) => a.compareDocumentPosition(b) & Node.DOCUMENT_POSITION_FOLLOWING ? -1 : 1);
|
|
11
|
+
const el = els[0];
|
|
12
|
+
if (el) {
|
|
13
|
+
el.focus();
|
|
14
|
+
el.scrollIntoView({ block: "center", behavior: "smooth" });
|
|
17
15
|
}
|
|
18
16
|
}
|
|
19
17
|
function useFormValidation(options) {
|
|
@@ -71,9 +69,12 @@ function useFormValidation(options) {
|
|
|
71
69
|
},
|
|
72
70
|
[form, validateField]
|
|
73
71
|
);
|
|
72
|
+
const isSubmittingRef = React.useRef(false);
|
|
73
|
+
const [isSubmitting, setIsSubmitting] = React.useState(false);
|
|
74
74
|
const handleSubmit = React.useCallback(
|
|
75
75
|
async (e) => {
|
|
76
76
|
e == null ? void 0 : e.preventDefault();
|
|
77
|
+
if (isSubmittingRef.current) return;
|
|
77
78
|
const current = form.getValues();
|
|
78
79
|
const formatErrors = [];
|
|
79
80
|
if (validate) {
|
|
@@ -93,18 +94,25 @@ function useFormValidation(options) {
|
|
|
93
94
|
focusFirstError(formatErrors);
|
|
94
95
|
return;
|
|
95
96
|
}
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
)
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
97
|
+
isSubmittingRef.current = true;
|
|
98
|
+
setIsSubmitting(true);
|
|
99
|
+
try {
|
|
100
|
+
const businessErrors = await onSubmit(current);
|
|
101
|
+
if (businessErrors && typeof businessErrors === "object") {
|
|
102
|
+
const names = Object.keys(businessErrors).filter(
|
|
103
|
+
(k) => businessErrors[k] != null
|
|
104
|
+
);
|
|
105
|
+
for (const name of names) {
|
|
106
|
+
form.setError(name, {
|
|
107
|
+
type: "business",
|
|
108
|
+
message: businessErrors[name]
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
if (names.length > 0) focusFirstError(names);
|
|
106
112
|
}
|
|
107
|
-
|
|
113
|
+
} finally {
|
|
114
|
+
isSubmittingRef.current = false;
|
|
115
|
+
setIsSubmitting(false);
|
|
108
116
|
}
|
|
109
117
|
},
|
|
110
118
|
[form, validate, onSubmit]
|
|
@@ -113,8 +121,10 @@ function useFormValidation(options) {
|
|
|
113
121
|
values,
|
|
114
122
|
errors,
|
|
115
123
|
isDirty,
|
|
116
|
-
// Submit Button 狀態 canonical:Create 永遠 enabled / Update disabled-until-dirty
|
|
117
|
-
|
|
124
|
+
// Submit Button 狀態 canonical:Create 永遠 enabled / Update disabled-until-dirty;
|
|
125
|
+
// submit 進行中一律 disabled(double-submit 防護的 UI 面,2026-07-05 D4)
|
|
126
|
+
submitDisabled: (intent === "update" ? !isDirty : false) || isSubmitting,
|
|
127
|
+
isSubmitting,
|
|
118
128
|
getInputProps,
|
|
119
129
|
handleSubmit,
|
|
120
130
|
reset: () => form.reset(),
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"use-form-validation.js","sources":["../../../src/components/Field/use-form-validation.ts"],"sourcesContent":["/**\n * useFormValidation — form-validation.spec.md 方法論的可執行層(SSOT executable arm)\n *\n * ── 定位 ──\n * 把 `form-validation.spec.md` 的 9 條驗證方法論編成**不可配置的預設**——consumer 拿到就是\n * canonical 行為,沒有 API 可以違反(M17「SSOT 必可傳播」:方法論從 prose 變 executable)。\n *\n * ── 實作基礎 ──\n * 基於 react-hook-form(direct dependency,完全 wrapped 不外露——對齊 DS「基於 X」引擎慣例:\n * DataTable 基於 TanStack / DatePicker 基於 react-day-picker / Toast 基於 sonner)。\n * Consumer 不 install、不 import、看不到 RHF API。RHF 提供 values state / dirty 深比對 /\n * errors store / resetField;驗證「時機」由本 hook own(RHF 的 mode/reValidateMode 不外露)。\n *\n * ── 與 Field 家族的關係(engine-agnostic 分層,field.spec.md「定位」段)──\n * Field 保持純 layout + context(MUI FormControl 派,可用於 cell / display / 無引擎場景);\n * 本 hook 住 form 層,錯誤經 consumer 一行 `<Field invalid={!!form.errors.x}>` 接入——\n * Field 層零耦合。這是「A 派的自由 + B 派的 DX」混合位置。\n *\n * ── 方法論對應(form-validation.spec.md 規則 1-9)──\n * 1 Focus 中不顯示錯誤 → 驗證只在 blur / submit 跑(無 onChange 驗證路徑)\n * 2 Blur 時驗證 → getInputProps().onBlur 跑 validate[name]\n * 3 Enter 等同 blur → form 內 Enter 觸發 submit(全驗,超集);單行控件原生行為\n * 4 Escape 取消回復原值 → getInputProps().onKeyDown Escape → resetField + 清 error\n * 5 開始編輯立即清除 error → onChange 先清 errors[name](不論新值合法與否)\n * 6 Blur 重新驗證 → 同 2(離開時重判)\n * 7 Submit 驗證全部 → handleSubmit 對所有 validate keys 全跑(不依賴 blur 狀態)\n * 8 Anchor 到第一個錯誤 → focus + scrollIntoView({block:'center'});每次 submit 重算\n * 9 Async / 跨欄位 defer 到 submit → onSubmit 回傳 field-keyed errors → 同 8 anchor\n * + Submit button:Create 永遠 enabled / Update disabled-until-dirty → `submitDisabled`\n *\n * ── v1 邊界(spec「可執行層」段 documented)──\n * - getInputProps 支援 value/onChange 型控件(Input / Textarea / NumberInput / Select /\n * Combobox / DatePicker / TimePicker;onChange 收 event 或裸值皆可)。Checkbox / Switch\n * (onCheckedChange)consumer 自接 setFieldValue。\n * - focus-first-error 以 DOM `name` 屬性定位(native input 生效;非 native 控件 fallback\n * scroll 略過,errors 視覺仍由 Field 紅框 + FieldError 呈現)。\n */\nimport * as React from 'react'\nimport { useForm } from 'react-hook-form'\nimport type { FieldValues, Path, PathValue, DefaultValues } from 'react-hook-form'\n\nexport interface UseFormValidationOptions<T extends FieldValues> {\n /** 表單初始值(Update 場景 = 現有資料;dirty 比對基準) */\n initialValues: T\n /**\n * 表單意圖,驅動 submit button 狀態(form-validation.spec.md「Submit Button 狀態」):\n * - 'create'(default):submitDisabled 永遠 false(不讓使用者猜「為什麼按不了」)\n * - 'update':submitDisabled = !isDirty(沒改就不用存;變更還原回 pristine 即再 disabled)\n */\n intent?: 'create' | 'update'\n /**\n * 格式驗證(blur 層,規則 2):single-field 純 syntax(email 格式 / 必填 / URL)。\n * 回傳 error 訊息字串 = 不合法;undefined = 合法。\n * 業務 / async / 跨欄位驗證**不要**放這裡——放 onSubmit 回傳(規則 9)。\n */\n validate?: Partial<Record<keyof T, (value: T[keyof T], values: T) => string | undefined>>\n /**\n * Submit handler(格式驗證全過後呼叫)。業務驗證(名稱重複 API / 跨欄位)在此判斷,\n * 回傳 field-keyed error object(如 `{ name: '名稱已存在' }`)→ hook 自動 setError +\n * anchor 到第一個錯誤(規則 9);回傳 undefined = 成功。\n */\n onSubmit: (values: T) => void | Partial<Record<keyof T, string>> | Promise<void | Partial<Record<keyof T, string>>>\n}\n\nexport interface FormFieldInputProps<V = unknown> {\n name: string\n value: V\n onChange: (eventOrValue: unknown) => void\n onBlur: () => void\n onKeyDown: (e: React.KeyboardEvent) => void\n}\n\nexport interface UseFormValidationReturn<T extends FieldValues> {\n /** 當前表單值(即時) */\n values: T\n /** field-keyed 錯誤訊息(餵 `<Field invalid>` + `<FieldError>`) */\n errors: Partial<Record<keyof T, string>>\n /** 任一欄位偏離 initialValues(深比對,還原回原值 = false) */\n isDirty: boolean\n /** Submit button disabled 狀態(intent 驅動,見 options.intent) */\n submitDisabled: boolean\n /** Spread 到 value/onChange 型控件:`<Input {...form.getInputProps('name')} />` */\n getInputProps: <K extends keyof T & string>(name: K) => FormFieldInputProps<T[K]>\n /** 接 `<form onSubmit={form.handleSubmit}>`(規則 7/8/9) */\n handleSubmit: (e?: React.FormEvent) => Promise<void>\n /** 整表重置回 initialValues(清 errors + dirty) */\n reset: () => void\n /** Escape hatch:非 value/onChange 控件(Checkbox/Switch)手動寫值 */\n setFieldValue: (name: keyof T & string, value: unknown) => void\n}\n\n/** onChange 收 event 或裸值皆可(對齊 Mantine getInputProps idiom):\n * native input event → e.target.value;自訂控件裸值(string / number / Date / array)→ 原樣。 */\nfunction extractValue(eventOrValue: unknown): unknown {\n if (\n eventOrValue &&\n typeof eventOrValue === 'object' &&\n 'target' in eventOrValue &&\n eventOrValue.target &&\n typeof eventOrValue.target === 'object' &&\n 'value' in (eventOrValue.target as object)\n ) {\n return (eventOrValue.target as HTMLInputElement).value\n }\n return eventOrValue\n}\n\n/** 規則 8:focus + scroll 到第一個錯誤欄位。以 DOM name 屬性定位(native input);\n * 找不到(非 native 控件)→ 靜默略過,error 視覺仍由 Field 紅框呈現。 */\nfunction focusFirstError(errorNames: string[]) {\n for (const name of errorNames) {\n const el = document.getElementsByName(name)[0] as HTMLElement | undefined\n if (el) {\n el.focus()\n el.scrollIntoView({ block: 'center', behavior: 'smooth' })\n return\n }\n }\n}\n\nexport function useFormValidation<T extends FieldValues>(\n options: UseFormValidationOptions<T>,\n): UseFormValidationReturn<T> {\n const { initialValues, intent = 'create', validate, onSubmit } = options\n\n // RHF 引擎(wrapped):驗證時機由本 hook own,故 RHF 自身 mode 鎖 onSubmit 且不掛 resolver\n // (所有 setError/clearErrors 走手動,RHF 只當 state + dirty + errors store)。\n const form = useForm<T>({\n defaultValues: initialValues as DefaultValues<T>,\n mode: 'onSubmit',\n shouldFocusError: false, // 規則 8 自己 focus(RHF 依賴 register ref,本 hook 不走 register)\n })\n\n // 訂閱全表(表單尺度 re-render 可接受;formState.isDirty 深比對 vs defaultValues)\n const values = form.watch()\n const { errors: rhfErrors, isDirty } = form.formState\n\n const errors = React.useMemo(() => {\n const out: Partial<Record<keyof T, string>> = {}\n for (const key of Object.keys(rhfErrors)) {\n const msg = (rhfErrors as Record<string, { message?: string } | undefined>)[key]?.message\n if (msg) out[key as keyof T] = msg\n }\n return out\n }, [rhfErrors])\n\n /** 規則 2/6:blur 驗證單一欄位 */\n const validateField = React.useCallback(\n (name: keyof T & string) => {\n const fn = validate?.[name]\n if (!fn) return\n const current = form.getValues()\n const message = fn(current[name], current)\n if (message) form.setError(name as Path<T>, { type: 'format', message })\n else form.clearErrors(name as Path<T>)\n },\n [form, validate],\n )\n\n const getInputProps = React.useCallback(\n <K extends keyof T & string>(name: K): FormFieldInputProps<T[K]> => {\n // 泛型 K 窄化到 Path<T> 需經 unknown(RHF Path 是 template-literal type,K 不直接 overlap)\n const path = name as unknown as Path<T>\n return {\n name,\n value: form.watch(path) as T[K],\n onChange: (eventOrValue: unknown) => {\n // 規則 5:開始編輯立即清除 error(不論新值合法與否,給修正空間)\n if (form.getFieldState(path).error) form.clearErrors(path)\n form.setValue(path, extractValue(eventOrValue) as PathValue<T, Path<T>>, {\n shouldDirty: true,\n })\n },\n // 規則 2:blur 驗證(focus 中永不驗 = 規則 1 自然成立)\n onBlur: () => validateField(name),\n // 規則 4:Escape 回復原值,不觸發驗證\n onKeyDown: (e: React.KeyboardEvent) => {\n if (e.key === 'Escape') {\n form.resetField(path)\n form.clearErrors(path)\n }\n },\n }\n },\n [form, validateField],\n )\n\n /** 規則 7/8/9:submit 全驗 + anchor 第一個錯誤 + 業務錯誤同軌 */\n const handleSubmit = React.useCallback(\n async (e?: React.FormEvent) => {\n e?.preventDefault()\n const current = form.getValues()\n // 規則 7:對所有 validate keys 全跑(不依賴個別 blur 狀態);每次 submit 重算(規則 8)\n const formatErrors: string[] = []\n if (validate) {\n for (const name of Object.keys(validate)) {\n const fn = validate[name as keyof T]\n if (!fn) continue\n const message = fn(current[name as keyof T], current)\n if (message) {\n form.setError(name as Path<T>, { type: 'format', message })\n formatErrors.push(name)\n } else {\n form.clearErrors(name as Path<T>)\n }\n }\n }\n if (formatErrors.length > 0) {\n focusFirstError(formatErrors)\n return\n }\n // 規則 9:業務 / async / 跨欄位驗證 defer 到 submit(onSubmit 回傳 field-keyed errors)\n const businessErrors = await onSubmit(current)\n if (businessErrors && typeof businessErrors === 'object') {\n const names = Object.keys(businessErrors).filter(\n (k) => businessErrors[k as keyof T] != null,\n )\n for (const name of names) {\n form.setError(name as Path<T>, {\n type: 'business',\n message: businessErrors[name as keyof T] as string,\n })\n }\n if (names.length > 0) focusFirstError(names)\n }\n },\n [form, validate, onSubmit],\n )\n\n return {\n values,\n errors,\n isDirty,\n // Submit Button 狀態 canonical:Create 永遠 enabled / Update disabled-until-dirty\n submitDisabled: intent === 'update' ? !isDirty : false,\n getInputProps,\n handleSubmit,\n reset: () => form.reset(),\n setFieldValue: (name, value) =>\n form.setValue(name as Path<T>, value as PathValue<T, Path<T>>, { shouldDirty: true }),\n }\n}\n"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"use-form-validation.js","sources":["../../../src/components/Field/use-form-validation.ts"],"sourcesContent":["/**\n * useFormValidation — form-validation.spec.md 方法論的可執行層(SSOT executable arm)\n *\n * ── 定位 ──\n * 把 `form-validation.spec.md` 的 9 條驗證方法論編成**不可配置的預設**——consumer 拿到就是\n * canonical 行為,沒有 API 可以違反(M17「SSOT 必可傳播」:方法論從 prose 變 executable)。\n *\n * ── 實作基礎 ──\n * 基於 react-hook-form(direct dependency,完全 wrapped 不外露——對齊 DS「基於 X」引擎慣例:\n * DataTable 基於 TanStack / DatePicker 基於 react-day-picker / Toast 基於 sonner)。\n * Consumer 不 install、不 import、看不到 RHF API。RHF 提供 values state / dirty 深比對 /\n * errors store / resetField;驗證「時機」由本 hook own(RHF 的 mode/reValidateMode 不外露)。\n *\n * ── 與 Field 家族的關係(engine-agnostic 分層,field.spec.md「定位」段)──\n * Field 保持純 layout + context(MUI FormControl 派,可用於 cell / display / 無引擎場景);\n * 本 hook 住 form 層,錯誤經 consumer 一行 `<Field invalid={!!form.errors.x}>` 接入——\n * Field 層零耦合。這是「A 派的自由 + B 派的 DX」混合位置。\n *\n * ── 方法論對應(form-validation.spec.md 規則 1-9)──\n * 1 Focus 中不顯示錯誤 → 驗證只在 blur / submit 跑(無 onChange 驗證路徑)\n * 2 Blur 時驗證 → getInputProps().onBlur 跑 validate[name]\n * 3 Enter 等同 blur → form 內 Enter 觸發 submit(全驗,超集);單行控件原生行為\n * 4 Escape 取消回復原值 → getInputProps().onKeyDown Escape → resetField + 清 error\n * 5 開始編輯立即清除 error → onChange 先清 errors[name](不論新值合法與否)\n * 6 Blur 重新驗證 → 同 2(離開時重判)\n * 7 Submit 驗證全部 → handleSubmit 對所有 validate keys 全跑(不依賴 blur 狀態)\n * 8 Anchor 到第一個錯誤 → focus + scrollIntoView({block:'center'});每次 submit 重算\n * 9 Async / 跨欄位 defer 到 submit → onSubmit 回傳 field-keyed errors → 同 8 anchor\n * + Submit button:Create 永遠 enabled / Update disabled-until-dirty → `submitDisabled`\n * + Double-submit 防護(2026-07-05 D4):await onSubmit 期間重入直接忽略;`isSubmitting`\n * 暴露餵 Button loading / disabled;onSubmit reject 先復位再原樣上拋(不吞錯)\n *\n * ── v1 邊界(spec「可執行層」段 documented)──\n * - getInputProps 支援 value/onChange 型控件(Input / Textarea / NumberInput / Select /\n * Combobox / DatePicker / TimePicker;onChange 收 event 或裸值皆可)。Checkbox / Switch\n * (onCheckedChange)consumer 自接 setFieldValue。\n * - focus-first-error 以 DOM `name` 屬性定位(native input 生效;非 native 控件 fallback\n * scroll 略過,errors 視覺仍由 Field 紅框 + FieldError 呈現)。\n */\nimport * as React from 'react'\nimport { useForm } from 'react-hook-form'\nimport type { FieldValues, Path, PathValue, DefaultValues } from 'react-hook-form'\n\nexport interface UseFormValidationOptions<T extends FieldValues> {\n /** 表單初始值(Update 場景 = 現有資料;dirty 比對基準) */\n initialValues: T\n /**\n * 表單意圖,驅動 submit button 狀態(form-validation.spec.md「Submit Button 狀態」):\n * - 'create'(default):submitDisabled 永遠 false(不讓使用者猜「為什麼按不了」)\n * - 'update':submitDisabled = !isDirty(沒改就不用存;變更還原回 pristine 即再 disabled)\n */\n intent?: 'create' | 'update'\n /**\n * 格式驗證(blur 層,規則 2):single-field 純 syntax(email 格式 / 必填 / URL)。\n * 回傳 error 訊息字串 = 不合法;undefined = 合法。\n * 業務 / async / 跨欄位驗證**不要**放這裡——放 onSubmit 回傳(規則 9)。\n */\n validate?: Partial<Record<keyof T, (value: T[keyof T], values: T) => string | undefined>>\n /**\n * Submit handler(格式驗證全過後呼叫)。業務驗證(名稱重複 API / 跨欄位)在此判斷,\n * 回傳 field-keyed error object(如 `{ name: '名稱已存在' }`)→ hook 自動 setError +\n * anchor 到第一個錯誤(規則 9);回傳 undefined = 成功。\n */\n onSubmit: (values: T) => void | Partial<Record<keyof T, string>> | Promise<void | Partial<Record<keyof T, string>>>\n}\n\nexport interface FormFieldInputProps<V = unknown> {\n name: string\n value: V\n onChange: (eventOrValue: unknown) => void\n onBlur: () => void\n onKeyDown: (e: React.KeyboardEvent) => void\n}\n\nexport interface UseFormValidationReturn<T extends FieldValues> {\n /** 當前表單值(即時) */\n values: T\n /** field-keyed 錯誤訊息(餵 `<Field invalid>` + `<FieldError>`) */\n errors: Partial<Record<keyof T, string>>\n /** 任一欄位偏離 initialValues(深比對,還原回原值 = false) */\n isDirty: boolean\n /** Submit button disabled 狀態(intent 驅動,見 options.intent;submit 進行中一併 disabled) */\n submitDisabled: boolean\n /** Submit 進行中(await onSubmit 期間 true)— 餵 Button loading;double-submit 防護的 state 面 */\n isSubmitting: boolean\n /** Spread 到 value/onChange 型控件:`<Input {...form.getInputProps('name')} />` */\n getInputProps: <K extends keyof T & string>(name: K) => FormFieldInputProps<T[K]>\n /** 接 `<form onSubmit={form.handleSubmit}>`(規則 7/8/9) */\n handleSubmit: (e?: React.FormEvent) => Promise<void>\n /** 整表重置回 initialValues(清 errors + dirty) */\n reset: () => void\n /** Escape hatch:非 value/onChange 控件(Checkbox/Switch)手動寫值 */\n setFieldValue: (name: keyof T & string, value: unknown) => void\n}\n\n/** onChange 收 event 或裸值皆可(對齊 Mantine getInputProps idiom):\n * native input event → e.target.value;自訂控件裸值(string / number / Date / array)→ 原樣。 */\nfunction extractValue(eventOrValue: unknown): unknown {\n if (\n eventOrValue &&\n typeof eventOrValue === 'object' &&\n 'target' in eventOrValue &&\n eventOrValue.target &&\n typeof eventOrValue.target === 'object' &&\n 'value' in (eventOrValue.target as object)\n ) {\n return (eventOrValue.target as HTMLInputElement).value\n }\n return eventOrValue\n}\n\n/** 規則 8:focus + scroll 到第一個錯誤欄位。以 DOM name 屬性定位(native input);\n * 找不到(非 native 控件)→ 靜默略過,error 視覺仍由 Field 紅框呈現。 */\nfunction focusFirstError(errorNames: string[]) {\n // 2026-07-07 修(deep-audit A.1b 殘項):「第一個錯誤」以 DOM 順序為準(= 使用者看到的視覺第一),\n // 非 validate key 宣告順序 — spec 規則 8 自然語意;對齊瀏覽器原生 reportValidity(DOM 序 focus\n // 首個 invalid)+ react-hook-form shouldFocusError。\n const els = errorNames\n .map((name) => document.getElementsByName(name)[0] as HTMLElement | undefined)\n .filter((el): el is HTMLElement => Boolean(el))\n .sort((a, b) => (a.compareDocumentPosition(b) & Node.DOCUMENT_POSITION_FOLLOWING ? -1 : 1))\n const el = els[0]\n if (el) {\n el.focus()\n el.scrollIntoView({ block: 'center', behavior: 'smooth' })\n }\n}\n\n// code-quality-allow: long-function — 單一 validation 生命週期 state machine(blur 驗證/edit 清 error/Escape 回復/submit 聚焦首錯)拆散會把耦合狀態跨函式傳遞,降低可讀性;對齊 react-hook-form useForm 本體同級長度\nexport function useFormValidation<T extends FieldValues>(\n options: UseFormValidationOptions<T>,\n): UseFormValidationReturn<T> {\n const { initialValues, intent = 'create', validate, onSubmit } = options\n\n // RHF 引擎(wrapped):驗證時機由本 hook own,故 RHF 自身 mode 鎖 onSubmit 且不掛 resolver\n // (所有 setError/clearErrors 走手動,RHF 只當 state + dirty + errors store)。\n const form = useForm<T>({\n defaultValues: initialValues as DefaultValues<T>,\n mode: 'onSubmit',\n shouldFocusError: false, // 規則 8 自己 focus(RHF 依賴 register ref,本 hook 不走 register)\n })\n\n // 訂閱全表(表單尺度 re-render 可接受;formState.isDirty 深比對 vs defaultValues)\n const values = form.watch()\n const { errors: rhfErrors, isDirty } = form.formState\n\n const errors = React.useMemo(() => {\n const out: Partial<Record<keyof T, string>> = {}\n for (const key of Object.keys(rhfErrors)) {\n const msg = (rhfErrors as Record<string, { message?: string } | undefined>)[key]?.message\n if (msg) out[key as keyof T] = msg\n }\n return out\n }, [rhfErrors])\n\n /** 規則 2/6:blur 驗證單一欄位 */\n const validateField = React.useCallback(\n (name: keyof T & string) => {\n const fn = validate?.[name]\n if (!fn) return\n const current = form.getValues()\n const message = fn(current[name], current)\n if (message) form.setError(name as Path<T>, { type: 'format', message })\n else form.clearErrors(name as Path<T>)\n },\n [form, validate],\n )\n\n const getInputProps = React.useCallback(\n <K extends keyof T & string>(name: K): FormFieldInputProps<T[K]> => {\n // 泛型 K 窄化到 Path<T> 需經 unknown(RHF Path 是 template-literal type,K 不直接 overlap)\n const path = name as unknown as Path<T>\n return {\n name,\n value: form.watch(path) as T[K],\n onChange: (eventOrValue: unknown) => {\n // 規則 5:開始編輯立即清除 error(不論新值合法與否,給修正空間)\n if (form.getFieldState(path).error) form.clearErrors(path)\n form.setValue(path, extractValue(eventOrValue) as PathValue<T, Path<T>>, {\n shouldDirty: true,\n })\n },\n // 規則 2:blur 驗證(focus 中永不驗 = 規則 1 自然成立)\n onBlur: () => validateField(name),\n // 規則 4:Escape 回復原值,不觸發驗證\n onKeyDown: (e: React.KeyboardEvent) => {\n if (e.key === 'Escape') {\n form.resetField(path)\n form.clearErrors(path)\n }\n },\n }\n },\n [form, validateField],\n )\n\n // 2026-07-05 D4 double-submit 防護:連點 submit / 連按 Enter 在 await onSubmit(規則 9\n // async 業務驗證)期間會並發呼叫 onSubmit(重複建立資源的經典事故)。ref 同步擋重入\n // (state 有 render 延遲,擋不住同 tick 連點);state 暴露 isSubmitting 餵 Button\n // loading / disabled。對齊 RHF formState.isSubmitting / Polaris / Mantine form submitting。\n const isSubmittingRef = React.useRef(false)\n const [isSubmitting, setIsSubmitting] = React.useState(false)\n\n /** 規則 7/8/9:submit 全驗 + anchor 第一個錯誤 + 業務錯誤同軌 */\n const handleSubmit = React.useCallback(\n async (e?: React.FormEvent) => {\n e?.preventDefault()\n if (isSubmittingRef.current) return // double-submit guard(見上方 comment)\n const current = form.getValues()\n // 規則 7:對所有 validate keys 全跑(不依賴個別 blur 狀態);每次 submit 重算(規則 8)\n const formatErrors: string[] = []\n if (validate) {\n for (const name of Object.keys(validate)) {\n const fn = validate[name as keyof T]\n if (!fn) continue\n const message = fn(current[name as keyof T], current)\n if (message) {\n form.setError(name as Path<T>, { type: 'format', message })\n formatErrors.push(name)\n } else {\n form.clearErrors(name as Path<T>)\n }\n }\n }\n if (formatErrors.length > 0) {\n focusFirstError(formatErrors)\n return\n }\n // 規則 9:業務 / async / 跨欄位驗證 defer 到 submit(onSubmit 回傳 field-keyed errors)\n // try/finally(2026-07-05 D4):onSubmit reject 先復位 isSubmitting 再讓 rejection 原樣\n // 上拋(不吞錯,對齊 RHF handleSubmit re-throw canonical)— 表單回到可重送狀態而非卡死。\n isSubmittingRef.current = true\n setIsSubmitting(true)\n try {\n const businessErrors = await onSubmit(current)\n if (businessErrors && typeof businessErrors === 'object') {\n const names = Object.keys(businessErrors).filter(\n (k) => businessErrors[k as keyof T] != null,\n )\n for (const name of names) {\n form.setError(name as Path<T>, {\n type: 'business',\n message: businessErrors[name as keyof T] as string,\n })\n }\n if (names.length > 0) focusFirstError(names)\n }\n } finally {\n isSubmittingRef.current = false\n setIsSubmitting(false)\n }\n },\n [form, validate, onSubmit],\n )\n\n return {\n values,\n errors,\n isDirty,\n // Submit Button 狀態 canonical:Create 永遠 enabled / Update disabled-until-dirty;\n // submit 進行中一律 disabled(double-submit 防護的 UI 面,2026-07-05 D4)\n submitDisabled: (intent === 'update' ? !isDirty : false) || isSubmitting,\n isSubmitting,\n getInputProps,\n handleSubmit,\n reset: () => form.reset(),\n setFieldValue: (name, value) =>\n form.setValue(name as Path<T>, value as PathValue<T, Path<T>>, { shouldDirty: true }),\n }\n}\n"],"names":["el"],"mappings":"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"}
|
|
@@ -28,8 +28,8 @@ import type { FieldSize } from '../../components/Field/field-context';
|
|
|
28
28
|
* ```tsx
|
|
29
29
|
* // Filter row: 2 fixed select + 1 flex value
|
|
30
30
|
* <FieldControlGroup>
|
|
31
|
-
* <Select className="w-[140px]" options={fields} />
|
|
32
|
-
* <Select className="w-[120px]" options={ops} />
|
|
31
|
+
* <Select className="!w-[140px]" options={fields} />
|
|
32
|
+
* <Select className="!w-[120px]" options={ops} />
|
|
33
33
|
* <Input className="flex-1" placeholder="輸入值..." />
|
|
34
34
|
* </FieldControlGroup>
|
|
35
35
|
*
|
|
@@ -37,7 +37,7 @@ import type { FieldSize } from '../../components/Field/field-context';
|
|
|
37
37
|
* <Field>
|
|
38
38
|
* <FieldLabel>電話</FieldLabel>
|
|
39
39
|
* <FieldControlGroup>
|
|
40
|
-
* <Select className="w-[80px]" options={codes} />
|
|
40
|
+
* <Select className="!w-[80px]" options={codes} />
|
|
41
41
|
* <Input className="flex-1" />
|
|
42
42
|
* </FieldControlGroup>
|
|
43
43
|
* </Field>
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"field-control-group.js","sources":["../../../src/components/FieldControlGroup/field-control-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.\n// ── 消費的 SSOT ──\n// - patterns/element-anatomy/element-anatomy.spec.md(Field 家族邊界)\n// - components/Field/field-wrapper.tsx(field-height token via h-field-{sm,md,lg})\n// - components/Field/field-context.ts(useFieldContext / size cascade)\n// - tokens/uiSize/uiSize.spec.md(--field-height-{sm,md,lg})\n// - 世界級對照:Ant Space.Compact compact-item.ts(verified github.com/ant-design/ant-design/blob/master/components/style/compact-item.ts)\n// Bootstrap input-group.scss(verified github.com/twbs/bootstrap/blob/v5.3.3/scss/forms/_input-group.scss)\n//\nimport * as React from 'react'\nimport { cn } from '@/lib/utils'\nimport type { FieldSize } from '@/design-system/components/Field/field-context'\n\n/**\n * FieldControlGroup — 多個 Field controls 視覺接合成一個 input frame\n *\n * **Naming rationale**(2026-05-04):\n * - Taxonomic 一致:FieldGroup(多 Field 堆疊)/ FieldControlGroup(多 control 接合)— scope 區分\n * - Idiom 一致:ButtonGroup(多 Button 接合)同 pattern,只是 X = field control\n * - 不撞 RadioGroup/CheckboxGroup(那是 1-question 多 options semantic group)\n *\n * **Behavior canonical**(verified Ant compact-item.ts L21-58):\n * - 子 controls 保留自身 border + radius;不 strip\n * - 鄰接子用負 margin 重疊 border(Bootstrap 也用此手法 但用 z-index 確保 focus 在最上層)\n * - first child 只留左 radii / last child 只留右 radii / middle 全 0 radii\n * - z-index:default 2 / hover|focus|focus-within 3 / disabled 0\n * - Container `display: inline-flex`(Ant default)/ `block` prop → `display: flex; width: 100%`\n *\n * **Size**(FCG `size` 為 no-op — 不傳遞 size 給 children,無 Context Provider / 無 cloneElement;見 spec「Size」段):\n * - children 尺寸完全由 child 自己決定;FCG 只負責 border-collapse 接合(border / radius / z-index)\n * - Mode A:整個 FieldControlGroup 包進 Field 當 control slot,children 各自讀外層 `<Field>` 的 context size(是 Field 的 context,不是 FCG 的)\n * - Mode B:standalone 用,逐一給每個 child 設 `size`(設 `<FieldControlGroup size>` 無效)\n *\n * **Width 配置**(Ant Space.Compact W-A canonical):\n * - 子 controls 自管 width(`className=\"w-[140px]\"` / `style={{width:120}}` / `flex-1` etc.)\n * - FieldControlGroup 不另開 Cell wrapper(避免 indirection,符合 Ant idiom)\n *\n * **使用範例**:\n * ```tsx\n * // Filter row: 2 fixed select + 1 flex value\n * <FieldControlGroup>\n * <Select className=\"w-[140px]\" options={fields} />\n * <Select className=\"w-[120px]\" options={ops} />\n * <Input className=\"flex-1\" placeholder=\"輸入值...\" />\n * </FieldControlGroup>\n *\n * // Phone: country code + number\n * <Field>\n * <FieldLabel>電話</FieldLabel>\n * <FieldControlGroup>\n * <Select className=\"w-[80px]\" options={codes} />\n * <Input className=\"flex-1\" />\n * </FieldControlGroup>\n * </Field>\n *\n * // Search + button\n * <FieldControlGroup>\n * <Input className=\"flex-1\" />\n * <Button variant=\"primary\">搜尋</Button>\n * </FieldControlGroup>\n * ```\n */\n\nexport interface FieldControlGroupProps extends React.HTMLAttributes<HTMLDivElement> {\n /** no-op — FCG 不傳遞 size 給 children;Mode A 由 children 讀外層 Field context、Mode B 逐一給每個 child 設 size(見 spec「Size」段) */\n size?: FieldSize\n /** Block 模式:width 100% 撐滿 parent(對齊 Ant Space.Compact `block` prop) */\n block?: boolean\n}\n\nconst FieldControlGroup = React.forwardRef<HTMLDivElement, FieldControlGroupProps>(\n ({ size: _size = 'md', block = false, className, children, ...props }, ref) => {\n return (\n <div\n ref={ref}\n // ── Ant compact-item 機制(verified):\n // [&>*]:relative — 子套 relative 才能 z-index 生效\n // [&>*]:z-[2] — default z 2(Ant 同值)\n // [&>*+*]:-ml-px — 鄰接子 margin-left -1px 重疊 border\n // hover/focus/focus-within → z-3(active border 在最上層,Bootstrap 用 z-5,Ant 用 z-3 我們對齊 Ant)\n // first/middle/last radii:對齊 Ant compactItemBorderRadius L67-92\n // ── Children 自管 width(W-A,Ant idiom);Group 自身僅控制 border/radius/z-index 接合機制\n className={cn(\n block ? 'flex w-full' : 'inline-flex',\n 'items-stretch',\n // z-index baseline + active layer\n '[&>*]:relative [&>*]:z-[2]',\n '[&>*:hover]:z-[3] [&>*:focus]:z-[3] [&>*:focus-within]:z-[3]',\n '[&>*[disabled]]:z-0 [&>*:has([disabled])]:z-0',\n // border overlap\n '[&>*+*]:-ml-px',\n // border radius — 中間 0,首/尾保留外側 radii\n '[&>*:not(:first-child):not(:last-child)]:rounded-none',\n '[&>*:first-child:not(:last-child)]:rounded-r-none',\n '[&>*:last-child:not(:first-child)]:rounded-l-none',\n // K12 fix(2026-05-04 v7 — semantic token):FCG 內 disabled cell border 用 `--border-opaque`:\n // ✓ 保留 global `bg-disabled`(neutral-2 灰底)— disabled state 視覺主要承載\n // ✓ 用 SEMANTIC `--border-opaque`(視覺等同 --border 但不跟 bg compositing)\n // v6 直接消費 primitive `--color-neutral-5-opaque` 違反 token 4 規則「禁 primitive 色名作 utility」,\n // v7 升 semantic alias `--border-opaque` 在 semantic.css(其 primitive 後盾仍是 neutral-5-opaque)\n // → 對齊 Ant Design colorBorderSecondary solid idiom(table 外框 / row divider 用 solid,跟 input alpha border 視覺層級分)\n // → 解決 alpha border 在 grey bg 上 composite 略深 perception(physical 對比問題)\n '[&>*[data-field-mode=\"disabled\"]]:border-[var(--border-opaque)]',\n className,\n )}\n data-field-control-group=\"\"\n {...props}\n >\n {children}\n </div>\n )\n },\n)\nFieldControlGroup.displayName = 'FieldControlGroup'\n\n// Story auto-compile metadata — Phase 4 migration(2026-05-10 #12 task complete)\n// Per scripts/compile-stories.mjs --check。FieldControlGroup is self-contained\n// structural wrapper(border-collapse pattern for Field controls)。\n// **No own sizes** — size prop 目前為 no-op(不傳遞給 children,無 Context Provider /\n// 無 cloneElement;children 尺寸自管,見上方 docblock「Size」段 + spec「FCG `size` prop 目前為 no-op」),\n// so sizes:{} matches spec frontmatter (no sizes declared)。\nexport const fieldControlGroupMeta = {\n component: 'FieldControlGroup',\n family: 'self-contained',\n variants: {},\n sizes: {}, // self-contained wrapper;size prop no-op(children 尺寸自管)\n states: ['default', 'children-hover', 'children-focus', 'children-disabled'],\n tokens: {\n bg: [], // structural wrapper has no own bg\n fg: [],\n border: ['var(--border-opaque)'], // K12 disabled cell border\n },\n defaultVariant: undefined,\n defaultSize: undefined,\n} as const\n\nexport { FieldControlGroup }\n"],"names":[],"mappings":";;;AAsEA,MAAM,oBAAoB,MAAM;AAAA,EAC9B,CAAC,EAAE,MAAM,QAAQ,MAAM,QAAQ,OAAO,WAAW,UAAU,GAAG,MAAA,GAAS,QAAQ;AAC7E,WACE;AAAA,MAAC;AAAA,MAAA;AAAA,QACC;AAAA,QAQA,WAAW;AAAA,UACT,QAAQ,gBAAgB;AAAA,UACxB;AAAA;AAAA,UAEA;AAAA,UACA;AAAA,UACA;AAAA;AAAA,UAEA;AAAA;AAAA,UAEA;AAAA,UACA;AAAA,UACA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,UAQA;AAAA,UACA;AAAA,QAAA;AAAA,QAEF,4BAAyB;AAAA,QACxB,GAAG;AAAA,QAEH;AAAA,MAAA;AAAA,IAAA;AAAA,EAGP;AACF;AACA,kBAAkB,cAAc;AAQzB,MAAM,wBAAwB;AAAA,EACnC,WAAW;AAAA,EACX,QAAQ;AAAA,EACR,UAAU,CAAA;AAAA,EACV,OAAO,CAAA;AAAA;AAAA,EACP,QAAQ,CAAC,WAAW,kBAAkB,kBAAkB,mBAAmB;AAAA,EAC3E,QAAQ;AAAA,IACN,IAAI,CAAA;AAAA;AAAA,IACJ,IAAI,CAAA;AAAA,IACJ,QAAQ,CAAC,sBAAsB;AAAA;AAAA,EAAA;AAAA,EAEjC,gBAAgB;AAAA,EAChB,aAAa;AACf;"}
|
|
1
|
+
{"version":3,"file":"field-control-group.js","sources":["../../../src/components/FieldControlGroup/field-control-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.\n// ── 消費的 SSOT ──\n// - patterns/element-anatomy/element-anatomy.spec.md(Field 家族邊界)\n// - components/Field/field-wrapper.tsx(field-height token via h-field-{sm,md,lg})\n// - components/Field/field-context.ts(useFieldContext / size cascade)\n// - tokens/uiSize/uiSize.spec.md(--field-height-{sm,md,lg})\n// - 世界級對照:Ant Space.Compact compact-item.ts(verified github.com/ant-design/ant-design/blob/master/components/style/compact-item.ts)\n// Bootstrap input-group.scss(verified github.com/twbs/bootstrap/blob/v5.3.3/scss/forms/_input-group.scss)\n//\nimport * as React from 'react'\nimport { cn } from '@/lib/utils'\nimport type { FieldSize } from '@/design-system/components/Field/field-context'\n\n/**\n * FieldControlGroup — 多個 Field controls 視覺接合成一個 input frame\n *\n * **Naming rationale**(2026-05-04):\n * - Taxonomic 一致:FieldGroup(多 Field 堆疊)/ FieldControlGroup(多 control 接合)— scope 區分\n * - Idiom 一致:ButtonGroup(多 Button 接合)同 pattern,只是 X = field control\n * - 不撞 RadioGroup/CheckboxGroup(那是 1-question 多 options semantic group)\n *\n * **Behavior canonical**(verified Ant compact-item.ts L21-58):\n * - 子 controls 保留自身 border + radius;不 strip\n * - 鄰接子用負 margin 重疊 border(Bootstrap 也用此手法 但用 z-index 確保 focus 在最上層)\n * - first child 只留左 radii / last child 只留右 radii / middle 全 0 radii\n * - z-index:default 2 / hover|focus|focus-within 3 / disabled 0\n * - Container `display: inline-flex`(Ant default)/ `block` prop → `display: flex; width: 100%`\n *\n * **Size**(FCG `size` 為 no-op — 不傳遞 size 給 children,無 Context Provider / 無 cloneElement;見 spec「Size」段):\n * - children 尺寸完全由 child 自己決定;FCG 只負責 border-collapse 接合(border / radius / z-index)\n * - Mode A:整個 FieldControlGroup 包進 Field 當 control slot,children 各自讀外層 `<Field>` 的 context size(是 Field 的 context,不是 FCG 的)\n * - Mode B:standalone 用,逐一給每個 child 設 `size`(設 `<FieldControlGroup size>` 無效)\n *\n * **Width 配置**(Ant Space.Compact W-A canonical):\n * - 子 controls 自管 width(`className=\"w-[140px]\"` / `style={{width:120}}` / `flex-1` etc.)\n * - FieldControlGroup 不另開 Cell wrapper(避免 indirection,符合 Ant idiom)\n *\n * **使用範例**:\n * ```tsx\n * // Filter row: 2 fixed select + 1 flex value\n * <FieldControlGroup>\n * <Select className=\"!w-[140px]\" options={fields} />\n * <Select className=\"!w-[120px]\" options={ops} />\n * <Input className=\"flex-1\" placeholder=\"輸入值...\" />\n * </FieldControlGroup>\n *\n * // Phone: country code + number\n * <Field>\n * <FieldLabel>電話</FieldLabel>\n * <FieldControlGroup>\n * <Select className=\"!w-[80px]\" options={codes} />\n * <Input className=\"flex-1\" />\n * </FieldControlGroup>\n * </Field>\n *\n * // Search + button\n * <FieldControlGroup>\n * <Input className=\"flex-1\" />\n * <Button variant=\"primary\">搜尋</Button>\n * </FieldControlGroup>\n * ```\n */\n\nexport interface FieldControlGroupProps extends React.HTMLAttributes<HTMLDivElement> {\n /** no-op — FCG 不傳遞 size 給 children;Mode A 由 children 讀外層 Field context、Mode B 逐一給每個 child 設 size(見 spec「Size」段) */\n size?: FieldSize\n /** Block 模式:width 100% 撐滿 parent(對齊 Ant Space.Compact `block` prop) */\n block?: boolean\n}\n\nconst FieldControlGroup = React.forwardRef<HTMLDivElement, FieldControlGroupProps>(\n ({ size: _size = 'md', block = false, className, children, ...props }, ref) => {\n return (\n <div\n ref={ref}\n // ── Ant compact-item 機制(verified):\n // [&>*]:relative — 子套 relative 才能 z-index 生效\n // [&>*]:z-[2] — default z 2(Ant 同值)\n // [&>*+*]:-ml-px — 鄰接子 margin-left -1px 重疊 border\n // hover/focus/focus-within → z-3(active border 在最上層,Bootstrap 用 z-5,Ant 用 z-3 我們對齊 Ant)\n // first/middle/last radii:對齊 Ant compactItemBorderRadius L67-92\n // ── Children 自管 width(W-A,Ant idiom);Group 自身僅控制 border/radius/z-index 接合機制\n className={cn(\n block ? 'flex w-full' : 'inline-flex',\n 'items-stretch',\n // z-index baseline + active layer\n '[&>*]:relative [&>*]:z-[2]',\n '[&>*:hover]:z-[3] [&>*:focus]:z-[3] [&>*:focus-within]:z-[3]',\n '[&>*[disabled]]:z-0 [&>*:has([disabled])]:z-0',\n // border overlap\n '[&>*+*]:-ml-px',\n // border radius — 中間 0,首/尾保留外側 radii\n '[&>*:not(:first-child):not(:last-child)]:rounded-none',\n '[&>*:first-child:not(:last-child)]:rounded-r-none',\n '[&>*:last-child:not(:first-child)]:rounded-l-none',\n // K12 fix(2026-05-04 v7 — semantic token):FCG 內 disabled cell border 用 `--border-opaque`:\n // ✓ 保留 global `bg-disabled`(neutral-2 灰底)— disabled state 視覺主要承載\n // ✓ 用 SEMANTIC `--border-opaque`(視覺等同 --border 但不跟 bg compositing)\n // v6 直接消費 primitive `--color-neutral-5-opaque` 違反 token 4 規則「禁 primitive 色名作 utility」,\n // v7 升 semantic alias `--border-opaque` 在 semantic.css(其 primitive 後盾仍是 neutral-5-opaque)\n // → 對齊 Ant Design colorBorderSecondary solid idiom(table 外框 / row divider 用 solid,跟 input alpha border 視覺層級分)\n // → 解決 alpha border 在 grey bg 上 composite 略深 perception(physical 對比問題)\n '[&>*[data-field-mode=\"disabled\"]]:border-[var(--border-opaque)]',\n className,\n )}\n data-field-control-group=\"\"\n {...props}\n >\n {children}\n </div>\n )\n },\n)\nFieldControlGroup.displayName = 'FieldControlGroup'\n\n// Story auto-compile metadata — Phase 4 migration(2026-05-10 #12 task complete)\n// Per scripts/compile-stories.mjs --check。FieldControlGroup is self-contained\n// structural wrapper(border-collapse pattern for Field controls)。\n// **No own sizes** — size prop 目前為 no-op(不傳遞給 children,無 Context Provider /\n// 無 cloneElement;children 尺寸自管,見上方 docblock「Size」段 + spec「FCG `size` prop 目前為 no-op」),\n// so sizes:{} matches spec frontmatter (no sizes declared)。\nexport const fieldControlGroupMeta = {\n component: 'FieldControlGroup',\n family: 'self-contained',\n variants: {},\n sizes: {}, // self-contained wrapper;size prop no-op(children 尺寸自管)\n states: ['default', 'children-hover', 'children-focus', 'children-disabled'],\n tokens: {\n bg: [], // structural wrapper has no own bg\n fg: [],\n border: ['var(--border-opaque)'], // K12 disabled cell border\n },\n defaultVariant: undefined,\n defaultSize: undefined,\n} as const\n\nexport { FieldControlGroup }\n"],"names":[],"mappings":";;;AAsEA,MAAM,oBAAoB,MAAM;AAAA,EAC9B,CAAC,EAAE,MAAM,QAAQ,MAAM,QAAQ,OAAO,WAAW,UAAU,GAAG,MAAA,GAAS,QAAQ;AAC7E,WACE;AAAA,MAAC;AAAA,MAAA;AAAA,QACC;AAAA,QAQA,WAAW;AAAA,UACT,QAAQ,gBAAgB;AAAA,UACxB;AAAA;AAAA,UAEA;AAAA,UACA;AAAA,UACA;AAAA;AAAA,UAEA;AAAA;AAAA,UAEA;AAAA,UACA;AAAA,UACA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,UAQA;AAAA,UACA;AAAA,QAAA;AAAA,QAEF,4BAAyB;AAAA,QACxB,GAAG;AAAA,QAEH;AAAA,MAAA;AAAA,IAAA;AAAA,EAGP;AACF;AACA,kBAAkB,cAAc;AAQzB,MAAM,wBAAwB;AAAA,EACnC,WAAW;AAAA,EACX,QAAQ;AAAA,EACR,UAAU,CAAA;AAAA,EACV,OAAO,CAAA;AAAA;AAAA,EACP,QAAQ,CAAC,WAAW,kBAAkB,kBAAkB,mBAAmB;AAAA,EAC3E,QAAQ;AAAA,IACN,IAAI,CAAA;AAAA;AAAA,IACJ,IAAI,CAAA;AAAA,IACJ,QAAQ,CAAC,sBAAsB;AAAA;AAAA,EAAA;AAAA,EAEjC,gBAAgB;AAAA,EAChB,aAAa;AACf;"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"file-item.d.ts","sourceRoot":"","sources":["../../../src/components/FileItem/file-item.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAA;AA6C9B,MAAM,WAAW,aAAc,SAAQ,IAAI,CAAC,KAAK,CAAC,cAAc,CAAC,cAAc,CAAC,EAAE,OAAO,CAAC;IACxF,IAAI,EAAE,MAAM,CAAA;IACZ;;;;OAIG;IACH,IAAI,CAAC,EAAE,SAAS,GAAG,MAAM,CAAA;IACzB;;;;;;;;OAQG;IACH,OAAO,CAAC,EAAE,MAAM,GAAG,gBAAgB,CAAA;IACnC,MAAM,CAAC,EAAE,WAAW,GAAG,WAAW,GAAG,OAAO,CAAA;IAC5C,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,mGAAmG;IACnG,WAAW,CAAC,EAAE,KAAK,CAAC,SAAS,CAAA;IAC7B,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,OAAO,CAAC,EAAE,KAAK,CAAC,SAAS,CAAA;IACzB,OAAO,CAAC,EAAE,MAAM,IAAI,CAAA;IACpB;;;;;;;;;OASG;IACH,UAAU,CAAC,EAAE,MAAM,IAAI,CAAA;IACvB,OAAO,CAAC,EAAE,MAAM,IAAI,CAAA;CACrB;AAGD,QAAA,MAAM,QAAQ,
|
|
1
|
+
{"version":3,"file":"file-item.d.ts","sourceRoot":"","sources":["../../../src/components/FileItem/file-item.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAA;AA6C9B,MAAM,WAAW,aAAc,SAAQ,IAAI,CAAC,KAAK,CAAC,cAAc,CAAC,cAAc,CAAC,EAAE,OAAO,CAAC;IACxF,IAAI,EAAE,MAAM,CAAA;IACZ;;;;OAIG;IACH,IAAI,CAAC,EAAE,SAAS,GAAG,MAAM,CAAA;IACzB;;;;;;;;OAQG;IACH,OAAO,CAAC,EAAE,MAAM,GAAG,gBAAgB,CAAA;IACnC,MAAM,CAAC,EAAE,WAAW,GAAG,WAAW,GAAG,OAAO,CAAA;IAC5C,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,mGAAmG;IACnG,WAAW,CAAC,EAAE,KAAK,CAAC,SAAS,CAAA;IAC7B,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,OAAO,CAAC,EAAE,KAAK,CAAC,SAAS,CAAA;IACzB,OAAO,CAAC,EAAE,MAAM,IAAI,CAAA;IACpB;;;;;;;;;OASG;IACH,UAAU,CAAC,EAAE,MAAM,IAAI,CAAA;IACvB,OAAO,CAAC,EAAE,MAAM,IAAI,CAAA;CACrB;AAGD,QAAA,MAAM,QAAQ,sFA8Ob,CAAA;AAKD,eAAO,MAAM,YAAY;;;;;;;;;;;CAef,CAAA;AAEV,OAAO,EAAE,QAAQ,EAAE,CAAA"}
|
|
@@ -65,7 +65,8 @@ const FileItem = React.forwardRef(
|
|
|
65
65
|
className: cn(
|
|
66
66
|
"shrink-0 transition-opacity",
|
|
67
67
|
statusConfig.color,
|
|
68
|
-
|
|
68
|
+
// 2026-07-05 D4:鍵盤 focus 同步觸發 swap(:has(:focus-visible),對齊 item-anatomy SUFFIX_HOVER_REVEAL_BY_GROUP SSOT)
|
|
69
|
+
hoverAction && "group-hover/row:opacity-0 group-has-[:focus-visible]/row:opacity-0"
|
|
69
70
|
),
|
|
70
71
|
"aria-hidden": true
|
|
71
72
|
}
|
|
@@ -82,7 +83,7 @@ const FileItem = React.forwardRef(
|
|
|
82
83
|
e.stopPropagation();
|
|
83
84
|
hoverAction.onClick();
|
|
84
85
|
},
|
|
85
|
-
className: "absolute inset-0 opacity-0 group-hover/row:opacity-100 transition-opacity"
|
|
86
|
+
className: "absolute inset-0 opacity-0 group-hover/row:opacity-100 group-has-[:focus-visible]/row:opacity-100 transition-opacity"
|
|
86
87
|
}
|
|
87
88
|
)
|
|
88
89
|
]
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"file-item.js","sources":["../../../src/components/FileItem/file-item.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 { Paperclip, CircleCheck, XCircle, Download, RotateCw } from 'lucide-react'\nimport { cn } from '@/lib/utils'\nimport { Avatar } from '@/design-system/components/Avatar/avatar'\nimport { Button } from '@/design-system/components/Button/button'\nimport { ProgressBar } from '@/design-system/components/ProgressBar/progress-bar'\nimport { ItemContent, ItemPrefix } from '@/design-system/patterns/element-anatomy/item-anatomy'\n\n/**\n * FileItem — 檔案顯示 / 上傳進度\n *\n * Typography: 兩 mode 統一 scanning — text-body (14px) + leading-compact (1.3)(詳 spec「Typography」段,2026-04-23)\n *\n * 兩種 mode(精簡 vs 完整內容呈現):\n *\n * compact(★ default): Paperclip 16px 在左。右側 content + bar。\n * padding 詳 spec「Padding」表(form: px-3 py-2;surface=upload-manager: px-0 保留 py-2)。\n * description 只有 error 才顯示。\n * bar 跟文字左邊對齊(在 icon 右邊的 column 內)。\n *\n * rich: Avatar 48px square 在左(顯示檔案內容縮圖)。右側 content + bar。\n * 多行 description(size / status message)。\n * 有 bar → justify-between(bar 底部對齊 avatar)\n * 無 bar → justify-center(文字垂直置中對齊 avatar)\n *\n * status 可選。不傳 = 已上傳檔案(無 bar,可點擊下載)。\n * onClick → 只加 cursor-pointer(**永不顯示 hover-bg**;FileItem 三型態皆 permanent-anchored,詳 spec「Hover 行為 canonical」)。\n */\n\nconst STATUS_ICON = {\n completed: { icon: CircleCheck, color: 'text-success' },\n error: { icon: XCircle, color: 'text-error' },\n} as const\n\n// ProgressBar status 映射:uploading=inProgress(藍) / completed=success(綠) / error=error(紅)\n// 與 ProgressBar 元件的 status prop 對齊,不需再維護 PROGRESS_COLOR 本地 map。\nconst PROGRESS_STATUS_MAP = {\n uploading: 'inProgress',\n completed: 'success',\n error: 'error',\n} as const\n\nconst AVATAR_SIZE = 48\nconst ICON_PX = 16\n\nexport interface FileItemProps extends Omit<React.HTMLAttributes<HTMLDivElement>, 'title'> {\n name: string\n /**\n * 兩種呈現 mode(精簡 vs 完整):\n * - `compact`(預設):paperclip + filename 單行 inline\n * - `rich`:縮圖 + 檔名 + size + status + progress 的完整 card 呈現\n */\n mode?: 'compact' | 'rich'\n /**\n * 清單所在的 surface context(2026-06-03 codify rich-borderless):\n * - `form`(預設):rich = border card(自立輪廓,Slack/Notion/Linear attachment 慣例)\n * - `upload-manager`:rich = **無邊框**(Google Drive/Dropbox 上傳管理面板 —— 面板自身已是容器,\n * card border 多餘 = 雙層容器);avatar 作每筆 item 視覺邊界。compact 的進度條/灰底不受 surface 影響\n * (由 status 決定)。\n * surface-driven(非 status-driven):避免 form 內 rich 上傳中變無邊框、存好變 card 的邊框閃爍。\n * 列間 gap 由 List wrapper canonical 決定(form rich 8px / upload-manager rich 12px tight / compact 4px,見 spec)。\n */\n surface?: 'form' | 'upload-manager'\n status?: 'uploading' | 'completed' | 'error'\n progress?: number\n /** rich mode: 檔案大小、狀態訊息。compact: 只有 error 才顯示。 ReactNode 支援 inline clickable link(如「View log」)。 */\n description?: React.ReactNode\n thumbnailSrc?: string\n actions?: React.ReactNode\n onClick?: () => void\n /**\n * Hover 動作(passive 狀態 icon 變互動 button 的 UX):\n * - `onDownload`:有值時,`status=\"completed\"` 的綠 ✓ icon 在 row hover 時\n * 換成 Download ↓ button;click 觸發 onDownload。無值保持 passive 綠 ✓。\n * - `onRetry`:有值時,`status=\"error\"` 的紅 ✗ icon 在 row hover 時換成 RotateCw ⟲\n * button;click 觸發 onRetry。無值保持 passive 紅 ✗。\n *\n * 世界級對照:Gmail / Slack / Dropbox 附件的 passive 狀態 → hover 變 action\n * 的 UX,使用者知道檔案狀態且能立即行動。\n */\n onDownload?: () => void\n onRetry?: () => void\n}\n\n// code-quality-allow: long-function — foundational composite main body — 拆 sub-fn 會複雜化 local state / ref / context binding\nconst FileItem = React.forwardRef<HTMLDivElement, FileItemProps>(\n (\n {\n name,\n mode = 'compact',\n surface = 'form',\n status,\n progress = 0,\n description,\n thumbnailSrc,\n actions,\n onClick,\n onDownload,\n onRetry,\n className,\n ...props\n },\n ref,\n ) => {\n const isRich = mode === 'rich'\n const hasStatus = !!status\n const statusConfig = status && status !== 'uploading' ? STATUS_ICON[status] : null\n const progressWidth = status === 'completed' ? 100 : progress\n\n // compact 只有 error 才顯示 description\n const showDesc = isRich ? !!description : (status === 'error' && !!description)\n\n // Hover 行為 canonical(2026-04-23 user 校準):**FileItem 永不顯示 hover-bg**。\n // 三種型態都有永久 visual anchor:rich = border card / compact 無 status = bg-secondary /\n // compact 有 status = 底部 progress bar(分隔線型 affordance)——再加 hover-bg 是\n // double-emphasis,視覺雜。世界級共識(Slack / Notion / Figma / Gmail 皆無 hover-bg):\n // permanent-anchored 元件 hover 只靠 cursor + action icon fade / border highlight,\n // 不靠 row bg。onClick 存在時只給 `cursor-pointer`,affordance 靠 cursor + 點擊行為本身。\n const hoverClass = onClick ? 'cursor-pointer' : ''\n\n // 消費 ProgressBar 元件(SSOT);不再自 roll bar。\n // height override:compact mode 用 2px(極密集 row layout),rich mode 用預設 4px。\n // 這是 ProgressBar `height` prop 的唯一合法 consumer(見 progress-bar.tsx docblock)。\n // a11y(2026-04-25 axe aria-progressbar-name):aria-label 用 file name 作 context。\n const progressBar = hasStatus ? (\n <ProgressBar\n value={progressWidth}\n status={PROGRESS_STATUS_MAP[status!]}\n height={isRich ? undefined : 2}\n aria-label={`${name} 上傳進度`}\n />\n ) : null\n\n // suffix 對齊 label 第一行(item-anatomy「24px 閾值對齊規則」小 suffix canonical):\n // icons 16 ≤ 24 屬小 suffix,統一 `h-[1lh]` inline,不因 desc wrap 改公式。\n // 兩 mode 同公式,跟 item-anatomy 一致。\n const suffixAlign = 'h-[1lh]'\n\n // Status slot 幾何(2026-04-23 user 統一):rich + compact 都用 `var(--field-height-xs)`(24)\n // 容器,裡面 Button xs iconOnly variant=\"text\"(auto data-unbounded)。\n // Compact 不影響 row 高度 = suffix wrapper 的 data-unbounded CSS 讓 Button layout\n // 收斂到 1lh(同 compact row 內容高),視覺/touch target 仍 24。\n const slotHw = 'var(--field-height-xs)'\n\n const hoverAction =\n status === 'completed' && onDownload ? { icon: Download, onClick: onDownload, label: '下載' } :\n status === 'error' && onRetry ? { icon: RotateCw, onClick: onRetry, label: '重試' } :\n null\n\n const statusSlot = statusConfig ? (\n <span\n data-unbounded=\"true\"\n className=\"relative inline-flex items-center justify-center shrink-0\"\n style={{ width: slotHw, height: slotHw }}\n >\n {/* Passive 狀態 icon:預設可見;若有 hover-swap,row-hover 時淡出 */}\n <statusConfig.icon\n size={ICON_PX}\n className={cn(\n 'shrink-0 transition-opacity',\n statusConfig.color,\n hoverAction && 'group-hover/row:opacity-0',\n )}\n aria-hidden\n />\n {/* Active action:row-hover 時淡入(rich + compact 同 Button xs 統一) */}\n {hoverAction && (\n <Button\n variant=\"text\"\n size=\"xs\"\n iconOnly\n startIcon={hoverAction.icon}\n aria-label={hoverAction.label}\n onClick={(e) => { e.stopPropagation(); hoverAction.onClick() }}\n className=\"absolute inset-0 opacity-0 group-hover/row:opacity-100 transition-opacity\"\n />\n )}\n </span>\n ) : null\n\n const suffix = (\n <div\n className={cn(\n 'flex items-center gap-2 shrink-0',\n suffixAlign,\n // data-unbounded chrome-canonical trick:let Button xs (24) live inside h-[1lh]\n // wrapper(compact ~18.2 / rich ~18.2 scanning)without pushing row height。\n // 視覺/hit area 仍 24,layout footprint 收斂到 1lh。同 overlay-surface\n // 的 SurfaceHeader dismiss canonical(2026-04-22 v5)。\n // **child selector `[&>[data-unbounded]]`(非 descendant)**:只針對 suffix\n // wrapper **直接子元素**(statusSlot span、actions Button)套 margin,\n // 避免 status slot 內部 hover-swap Button(nested)也套造成 layout 跳動。\n '[&>[data-unbounded]]:my-[calc((1lh-var(--field-height-xs))/2)]',\n )}\n >\n {status === 'uploading' && isRich && (\n <span className=\"text-fg-secondary tabular-nums\">{progress}%</span>\n )}\n {statusSlot}\n {actions}\n </div>\n )\n\n // content row — 消費 ItemContent primitive(封裝 label + desc + mt-gap token SSOT)。\n // 兩 mode 共用:primitive 改 → 兩 mode 同步,不需 grep。\n // typography:scanning mode(2026-04-23 user 指示)—— label body(14/1.3) + desc caption(12/1.3);\n // row 本身加 `leading-compact` 配合 scanning idiom(同 MenuItem row)。\n const contentRow = (\n <div className=\"flex items-start gap-2\">\n <ItemContent\n label={name}\n description={showDesc ? description : undefined}\n mode=\"scanning\"\n descriptionTone={status === 'error' ? 'error' : 'secondary'}\n />\n {suffix}\n </div>\n )\n\n // a11y(2026-04-25 nested-interactive fix):FileItem row 含 inner interactive\n // (hover-swap action button / ProgressBar / Avatar hoverCard trigger)。原本\n // role='button' + tabIndex=0 整列可鍵盤點,與 inner buttons 構成 nested-interactive\n // (axe serious)。移除 row 層 button semantic → mouse 仍可點(onClick 保留),\n // 鍵盤 user 直接 tab 到 inner primary action。Trade-off:失去「整列 Enter 開啟」\n // 但滿足 WCAG;世界級對照:Slack message row / Notion page row 同模式 — row 只\n // mouse 點,inner 有 explicit 按鈕負責鍵盤。\n const rowA11y = {}\n\n // Compact 靜態背景(AR20):無進度條 → 顯示 `bg-secondary`(= neutral-3)作「檔案已上傳 /\n // 靜態列表」視覺區隔,跟「上傳中(有 progress bar)」對照。hover 不改 bg(見上方\n // hoverClass canonical:FileItem 永不顯示 hover-bg)。\n // **為什麼 bg-secondary 不 bg-neutral-3**:`bg-neutral-3` 不是合法 Tailwind utility\n // (primitive token `--color-neutral-3` 沒經 `@theme inline` 橋接);`bg-secondary`\n // 是 semantic token 橋接的 utility(見 `tokens/color/semantic.css`@theme inline),\n // 底色同樣指向 `--color-neutral-3`。對齊 Badge low / ProgressBar track SSOT。\n const compactStaticBg = !progressBar ? 'bg-secondary' : ''\n\n // ── rich(含縮圖完整呈現)——AR17 canonical:加邊框 + gap-2 ──\n // Rich mode 是「檔案 card」風格,外框讓每個 row 視覺上是獨立 card\n // (Slack / Notion / Linear attachment 慣例)\n if (isRich) {\n return (\n <div\n ref={ref}\n className={cn(\n 'group/row flex items-start gap-2 w-full text-body leading-compact transition-colors',\n // surface=form → border card(自立輪廓);surface=upload-manager → 無邊框(box 自身是容器,\n // avatar 作 item 邊界)。2026-06-03 codify rich-borderless(原僅 spec 旁註,consumer 自己移除)。\n // 2026-06-03 圖五:upload-manager rich 拿掉 px+py(卡片移除後 py 多餘,列高靠 avatar 48 的 content minHeight;\n // 容器 + gap 控制間距)。form 保留 px-3 py-3 卡片內距。\n surface === 'upload-manager' ? 'rounded-md' : 'px-3 py-3 border border-divider rounded-md bg-surface',\n hoverClass,\n className,\n )}\n onClick={onClick}\n {...rowA11y}\n {...props}\n >\n <Avatar src={thumbnailSrc} alt={name} size={AVATAR_SIZE} shape=\"square\" className=\"shrink-0\" />\n {/* Rich layout invariant(2026-04-23 user 校準):\n - content col minHeight = AVATAR_SIZE(48),確保 1-line desc 時內容 ≥ avatar 高\n - `justify-between`(有 bar)/`justify-center`(無 bar):\n * 1-line desc:label 頂 + progress bar 底 **自動對齊 avatar 頂/底**\n * 無 bar:content 垂直 center 對齊 avatar 中\n - `gap-2`:desc ↔ progress bar **至少 8px gap**(multi-line desc 時 bar 溢出仍保 8px)\n - row `items-start`:avatar top-align 作視覺引導(tight-stack box 內 item 邊界) */}\n <div\n className={cn(\n 'flex flex-col flex-1 min-w-0 gap-2',\n progressBar ? 'justify-between' : 'justify-center',\n )}\n style={{ minHeight: AVATAR_SIZE }}\n >\n {contentRow}\n {progressBar}\n </div>\n </div>\n )\n }\n\n // ── compact: py-2, bar absolute 底部 ──\n // 左右 padding 單一來源(SSOT):form=12px(= px-3);upload-manager=0(由面板提供 L/R)。\n // progress bar 是 absolute 定位,其 left/right 必須跟此值「同源」—— 否則 surface 一拿掉 padding,\n // bar 的 offset 沒同步就會對齊跑掉(2026-06-03 圖五 bug:原本 left/right 寫死 0.75rem 假設 px-3)。\n const compactPadX = surface === 'upload-manager' ? 0 : 12\n return (\n <div\n ref={ref}\n className={cn(\n 'group/row relative flex items-start gap-2 py-2 w-full text-body leading-compact transition-colors rounded-md',\n compactStaticBg,\n hoverClass,\n className,\n )}\n style={{ paddingInline: compactPadX }}\n onClick={onClick}\n {...rowA11y}\n {...props}\n >\n <ItemPrefix>\n <Paperclip size={ICON_PX} className=\"shrink-0 text-fg-muted\" aria-hidden />\n </ItemPrefix>\n {/* Compact 共用 contentRow(via ItemContent primitive SSOT)—— 先前 inline\n hand-craft 導致 compact label↔desc gap 跟 rich 不同步。shared contentRow\n 保證兩 mode 修 primitive 一處全同步。 */}\n <div className=\"flex flex-col flex-1 min-w-0\">\n {contentRow}\n </div>\n\n {/* ProgressBar: absolute 底部。left/right 與 compactPadX 同源:\n left = padX + icon + gap-2(0.5rem)對齊 label 首字;right = padX 收在 row 內緣。 */}\n {progressBar && (\n <div\n className=\"absolute bottom-0\"\n style={{ left: `calc(${compactPadX}px + ${ICON_PX}px + 0.5rem)`, right: compactPadX }}\n >\n {progressBar}\n </div>\n )}\n </div>\n )\n },\n)\nFileItem.displayName = 'FileItem'\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 fileItemMeta = {\n component: 'FileItem',\n family: 2,\n variants: {\n\n },\n sizes: {\n\n },\n states: ['default'],\n tokens: {\n bg: ['bg-secondary', 'bg-surface'],\n fg: ['text-fg-muted', 'text-fg-secondary'],\n ring: [],\n },\n} as const\n\nexport { FileItem }\n"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"file-item.js","sources":["../../../src/components/FileItem/file-item.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 { Paperclip, CircleCheck, XCircle, Download, RotateCw } from 'lucide-react'\nimport { cn } from '@/lib/utils'\nimport { Avatar } from '@/design-system/components/Avatar/avatar'\nimport { Button } from '@/design-system/components/Button/button'\nimport { ProgressBar } from '@/design-system/components/ProgressBar/progress-bar'\nimport { ItemContent, ItemPrefix } from '@/design-system/patterns/element-anatomy/item-anatomy'\n\n/**\n * FileItem — 檔案顯示 / 上傳進度\n *\n * Typography: 兩 mode 統一 scanning — text-body (14px) + leading-compact (1.3)(詳 spec「Typography」段,2026-04-23)\n *\n * 兩種 mode(精簡 vs 完整內容呈現):\n *\n * compact(★ default): Paperclip 16px 在左。右側 content + bar。\n * padding 詳 spec「Padding」表(form: px-3 py-2;surface=upload-manager: px-0 保留 py-2)。\n * description 只有 error 才顯示。\n * bar 跟文字左邊對齊(在 icon 右邊的 column 內)。\n *\n * rich: Avatar 48px square 在左(顯示檔案內容縮圖)。右側 content + bar。\n * 多行 description(size / status message)。\n * 有 bar → justify-between(bar 底部對齊 avatar)\n * 無 bar → justify-center(文字垂直置中對齊 avatar)\n *\n * status 可選。不傳 = 已上傳檔案(無 bar,可點擊下載)。\n * onClick → 只加 cursor-pointer(**永不顯示 hover-bg**;FileItem 三型態皆 permanent-anchored,詳 spec「Hover 行為 canonical」)。\n */\n\nconst STATUS_ICON = {\n completed: { icon: CircleCheck, color: 'text-success' },\n error: { icon: XCircle, color: 'text-error' },\n} as const\n\n// ProgressBar status 映射:uploading=inProgress(藍) / completed=success(綠) / error=error(紅)\n// 與 ProgressBar 元件的 status prop 對齊,不需再維護 PROGRESS_COLOR 本地 map。\nconst PROGRESS_STATUS_MAP = {\n uploading: 'inProgress',\n completed: 'success',\n error: 'error',\n} as const\n\nconst AVATAR_SIZE = 48\nconst ICON_PX = 16\n\nexport interface FileItemProps extends Omit<React.HTMLAttributes<HTMLDivElement>, 'title'> {\n name: string\n /**\n * 兩種呈現 mode(精簡 vs 完整):\n * - `compact`(預設):paperclip + filename 單行 inline\n * - `rich`:縮圖 + 檔名 + size + status + progress 的完整 card 呈現\n */\n mode?: 'compact' | 'rich'\n /**\n * 清單所在的 surface context(2026-06-03 codify rich-borderless):\n * - `form`(預設):rich = border card(自立輪廓,Slack/Notion/Linear attachment 慣例)\n * - `upload-manager`:rich = **無邊框**(Google Drive/Dropbox 上傳管理面板 —— 面板自身已是容器,\n * card border 多餘 = 雙層容器);avatar 作每筆 item 視覺邊界。compact 的進度條/灰底不受 surface 影響\n * (由 status 決定)。\n * surface-driven(非 status-driven):避免 form 內 rich 上傳中變無邊框、存好變 card 的邊框閃爍。\n * 列間 gap 由 List wrapper canonical 決定(form rich 8px / upload-manager rich 12px tight / compact 4px,見 spec)。\n */\n surface?: 'form' | 'upload-manager'\n status?: 'uploading' | 'completed' | 'error'\n progress?: number\n /** rich mode: 檔案大小、狀態訊息。compact: 只有 error 才顯示。 ReactNode 支援 inline clickable link(如「View log」)。 */\n description?: React.ReactNode\n thumbnailSrc?: string\n actions?: React.ReactNode\n onClick?: () => void\n /**\n * Hover 動作(passive 狀態 icon 變互動 button 的 UX):\n * - `onDownload`:有值時,`status=\"completed\"` 的綠 ✓ icon 在 row hover 時\n * 換成 Download ↓ button;click 觸發 onDownload。無值保持 passive 綠 ✓。\n * - `onRetry`:有值時,`status=\"error\"` 的紅 ✗ icon 在 row hover 時換成 RotateCw ⟲\n * button;click 觸發 onRetry。無值保持 passive 紅 ✗。\n *\n * 世界級對照:Gmail / Slack / Dropbox 附件的 passive 狀態 → hover 變 action\n * 的 UX,使用者知道檔案狀態且能立即行動。\n */\n onDownload?: () => void\n onRetry?: () => void\n}\n\n// code-quality-allow: long-function — foundational composite main body — 拆 sub-fn 會複雜化 local state / ref / context binding\nconst FileItem = React.forwardRef<HTMLDivElement, FileItemProps>(\n (\n {\n name,\n mode = 'compact',\n surface = 'form',\n status,\n progress = 0,\n description,\n thumbnailSrc,\n actions,\n onClick,\n onDownload,\n onRetry,\n className,\n ...props\n },\n ref,\n ) => {\n const isRich = mode === 'rich'\n const hasStatus = !!status\n const statusConfig = status && status !== 'uploading' ? STATUS_ICON[status] : null\n const progressWidth = status === 'completed' ? 100 : progress\n\n // compact 只有 error 才顯示 description\n const showDesc = isRich ? !!description : (status === 'error' && !!description)\n\n // Hover 行為 canonical(2026-04-23 user 校準):**FileItem 永不顯示 hover-bg**。\n // 三種型態都有永久 visual anchor:rich = border card / compact 無 status = bg-secondary /\n // compact 有 status = 底部 progress bar(分隔線型 affordance)——再加 hover-bg 是\n // double-emphasis,視覺雜。世界級共識(Slack / Notion / Figma / Gmail 皆無 hover-bg):\n // permanent-anchored 元件 hover 只靠 cursor + action icon fade / border highlight,\n // 不靠 row bg。onClick 存在時只給 `cursor-pointer`,affordance 靠 cursor + 點擊行為本身。\n const hoverClass = onClick ? 'cursor-pointer' : ''\n\n // 消費 ProgressBar 元件(SSOT);不再自 roll bar。\n // height override:compact mode 用 2px(極密集 row layout),rich mode 用預設 4px。\n // 這是 ProgressBar `height` prop 的唯一合法 consumer(見 progress-bar.tsx docblock)。\n // a11y(2026-04-25 axe aria-progressbar-name):aria-label 用 file name 作 context。\n const progressBar = hasStatus ? (\n <ProgressBar\n value={progressWidth}\n status={PROGRESS_STATUS_MAP[status!]}\n height={isRich ? undefined : 2}\n aria-label={`${name} 上傳進度`}\n />\n ) : null\n\n // suffix 對齊 label 第一行(item-anatomy「24px 閾值對齊規則」小 suffix canonical):\n // icons 16 ≤ 24 屬小 suffix,統一 `h-[1lh]` inline,不因 desc wrap 改公式。\n // 兩 mode 同公式,跟 item-anatomy 一致。\n const suffixAlign = 'h-[1lh]'\n\n // Status slot 幾何(2026-04-23 user 統一):rich + compact 都用 `var(--field-height-xs)`(24)\n // 容器,裡面 Button xs iconOnly variant=\"text\"(auto data-unbounded)。\n // Compact 不影響 row 高度 = suffix wrapper 的 data-unbounded CSS 讓 Button layout\n // 收斂到 1lh(同 compact row 內容高),視覺/touch target 仍 24。\n const slotHw = 'var(--field-height-xs)'\n\n const hoverAction =\n status === 'completed' && onDownload ? { icon: Download, onClick: onDownload, label: '下載' } :\n status === 'error' && onRetry ? { icon: RotateCw, onClick: onRetry, label: '重試' } :\n null\n\n const statusSlot = statusConfig ? (\n <span\n data-unbounded=\"true\"\n className=\"relative inline-flex items-center justify-center shrink-0\"\n style={{ width: slotHw, height: slotHw }}\n >\n {/* Passive 狀態 icon:預設可見;若有 hover-swap,row-hover 時淡出 */}\n <statusConfig.icon\n size={ICON_PX}\n className={cn(\n 'shrink-0 transition-opacity',\n statusConfig.color,\n // 2026-07-05 D4:鍵盤 focus 同步觸發 swap(:has(:focus-visible),對齊 item-anatomy SUFFIX_HOVER_REVEAL_BY_GROUP SSOT)\n hoverAction && 'group-hover/row:opacity-0 group-has-[:focus-visible]/row:opacity-0',\n )}\n aria-hidden\n />\n {/* Active action:row-hover 時淡入(rich + compact 同 Button xs 統一) */}\n {hoverAction && (\n <Button\n variant=\"text\"\n size=\"xs\"\n iconOnly\n startIcon={hoverAction.icon}\n aria-label={hoverAction.label}\n onClick={(e) => { e.stopPropagation(); hoverAction.onClick() }}\n className=\"absolute inset-0 opacity-0 group-hover/row:opacity-100 group-has-[:focus-visible]/row:opacity-100 transition-opacity\"\n />\n )}\n </span>\n ) : null\n\n const suffix = (\n <div\n className={cn(\n 'flex items-center gap-2 shrink-0',\n suffixAlign,\n // data-unbounded chrome-canonical trick:let Button xs (24) live inside h-[1lh]\n // wrapper(compact ~18.2 / rich ~18.2 scanning)without pushing row height。\n // 視覺/hit area 仍 24,layout footprint 收斂到 1lh。同 overlay-surface\n // 的 SurfaceHeader dismiss canonical(2026-04-22 v5)。\n // **child selector `[&>[data-unbounded]]`(非 descendant)**:只針對 suffix\n // wrapper **直接子元素**(statusSlot span、actions Button)套 margin,\n // 避免 status slot 內部 hover-swap Button(nested)也套造成 layout 跳動。\n '[&>[data-unbounded]]:my-[calc((1lh-var(--field-height-xs))/2)]',\n )}\n >\n {status === 'uploading' && isRich && (\n <span className=\"text-fg-secondary tabular-nums\">{progress}%</span>\n )}\n {statusSlot}\n {actions}\n </div>\n )\n\n // content row — 消費 ItemContent primitive(封裝 label + desc + mt-gap token SSOT)。\n // 兩 mode 共用:primitive 改 → 兩 mode 同步,不需 grep。\n // typography:scanning mode(2026-04-23 user 指示)—— label body(14/1.3) + desc caption(12/1.3);\n // row 本身加 `leading-compact` 配合 scanning idiom(同 MenuItem row)。\n const contentRow = (\n <div className=\"flex items-start gap-2\">\n <ItemContent\n label={name}\n description={showDesc ? description : undefined}\n mode=\"scanning\"\n descriptionTone={status === 'error' ? 'error' : 'secondary'}\n />\n {suffix}\n </div>\n )\n\n // a11y(2026-04-25 nested-interactive fix):FileItem row 含 inner interactive\n // (hover-swap action button / ProgressBar / Avatar hoverCard trigger)。原本\n // role='button' + tabIndex=0 整列可鍵盤點,與 inner buttons 構成 nested-interactive\n // (axe serious)。移除 row 層 button semantic → mouse 仍可點(onClick 保留),\n // 鍵盤 user 直接 tab 到 inner primary action。Trade-off:失去「整列 Enter 開啟」\n // 但滿足 WCAG;世界級對照:Slack message row / Notion page row 同模式 — row 只\n // mouse 點,inner 有 explicit 按鈕負責鍵盤。\n const rowA11y = {}\n\n // Compact 靜態背景(AR20):無進度條 → 顯示 `bg-secondary`(= neutral-3)作「檔案已上傳 /\n // 靜態列表」視覺區隔,跟「上傳中(有 progress bar)」對照。hover 不改 bg(見上方\n // hoverClass canonical:FileItem 永不顯示 hover-bg)。\n // **為什麼 bg-secondary 不 bg-neutral-3**:`bg-neutral-3` 不是合法 Tailwind utility\n // (primitive token `--color-neutral-3` 沒經 `@theme inline` 橋接);`bg-secondary`\n // 是 semantic token 橋接的 utility(見 `tokens/color/semantic.css`@theme inline),\n // 底色同樣指向 `--color-neutral-3`。對齊 Badge low / ProgressBar track SSOT。\n const compactStaticBg = !progressBar ? 'bg-secondary' : ''\n\n // ── rich(含縮圖完整呈現)——AR17 canonical:加邊框 + gap-2 ──\n // Rich mode 是「檔案 card」風格,外框讓每個 row 視覺上是獨立 card\n // (Slack / Notion / Linear attachment 慣例)\n if (isRich) {\n return (\n <div\n ref={ref}\n className={cn(\n 'group/row flex items-start gap-2 w-full text-body leading-compact transition-colors',\n // surface=form → border card(自立輪廓);surface=upload-manager → 無邊框(box 自身是容器,\n // avatar 作 item 邊界)。2026-06-03 codify rich-borderless(原僅 spec 旁註,consumer 自己移除)。\n // 2026-06-03 圖五:upload-manager rich 拿掉 px+py(卡片移除後 py 多餘,列高靠 avatar 48 的 content minHeight;\n // 容器 + gap 控制間距)。form 保留 px-3 py-3 卡片內距。\n surface === 'upload-manager' ? 'rounded-md' : 'px-3 py-3 border border-divider rounded-md bg-surface',\n hoverClass,\n className,\n )}\n onClick={onClick}\n {...rowA11y}\n {...props}\n >\n <Avatar src={thumbnailSrc} alt={name} size={AVATAR_SIZE} shape=\"square\" className=\"shrink-0\" />\n {/* Rich layout invariant(2026-04-23 user 校準):\n - content col minHeight = AVATAR_SIZE(48),確保 1-line desc 時內容 ≥ avatar 高\n - `justify-between`(有 bar)/`justify-center`(無 bar):\n * 1-line desc:label 頂 + progress bar 底 **自動對齊 avatar 頂/底**\n * 無 bar:content 垂直 center 對齊 avatar 中\n - `gap-2`:desc ↔ progress bar **至少 8px gap**(multi-line desc 時 bar 溢出仍保 8px)\n - row `items-start`:avatar top-align 作視覺引導(tight-stack box 內 item 邊界) */}\n <div\n className={cn(\n 'flex flex-col flex-1 min-w-0 gap-2',\n progressBar ? 'justify-between' : 'justify-center',\n )}\n style={{ minHeight: AVATAR_SIZE }}\n >\n {contentRow}\n {progressBar}\n </div>\n </div>\n )\n }\n\n // ── compact: py-2, bar absolute 底部 ──\n // 左右 padding 單一來源(SSOT):form=12px(= px-3);upload-manager=0(由面板提供 L/R)。\n // progress bar 是 absolute 定位,其 left/right 必須跟此值「同源」—— 否則 surface 一拿掉 padding,\n // bar 的 offset 沒同步就會對齊跑掉(2026-06-03 圖五 bug:原本 left/right 寫死 0.75rem 假設 px-3)。\n const compactPadX = surface === 'upload-manager' ? 0 : 12\n return (\n <div\n ref={ref}\n className={cn(\n 'group/row relative flex items-start gap-2 py-2 w-full text-body leading-compact transition-colors rounded-md',\n compactStaticBg,\n hoverClass,\n className,\n )}\n style={{ paddingInline: compactPadX }}\n onClick={onClick}\n {...rowA11y}\n {...props}\n >\n <ItemPrefix>\n <Paperclip size={ICON_PX} className=\"shrink-0 text-fg-muted\" aria-hidden />\n </ItemPrefix>\n {/* Compact 共用 contentRow(via ItemContent primitive SSOT)—— 先前 inline\n hand-craft 導致 compact label↔desc gap 跟 rich 不同步。shared contentRow\n 保證兩 mode 修 primitive 一處全同步。 */}\n <div className=\"flex flex-col flex-1 min-w-0\">\n {contentRow}\n </div>\n\n {/* ProgressBar: absolute 底部。left/right 與 compactPadX 同源:\n left = padX + icon + gap-2(0.5rem)對齊 label 首字;right = padX 收在 row 內緣。 */}\n {progressBar && (\n <div\n className=\"absolute bottom-0\"\n style={{ left: `calc(${compactPadX}px + ${ICON_PX}px + 0.5rem)`, right: compactPadX }}\n >\n {progressBar}\n </div>\n )}\n </div>\n )\n },\n)\nFileItem.displayName = 'FileItem'\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 fileItemMeta = {\n component: 'FileItem',\n family: 2,\n variants: {\n\n },\n sizes: {\n\n },\n states: ['default'],\n tokens: {\n bg: ['bg-secondary', 'bg-surface'],\n fg: ['text-fg-muted', 'text-fg-secondary'],\n ring: [],\n },\n} as const\n\nexport { FileItem }\n"],"names":[],"mappings":"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"}
|
|
@@ -105,7 +105,7 @@ export declare const fileUploadMeta: {
|
|
|
105
105
|
};
|
|
106
106
|
};
|
|
107
107
|
readonly sizes: {};
|
|
108
|
-
readonly states: readonly ["default", "hover", "
|
|
108
|
+
readonly states: readonly ["default", "hover", "focus-visible", "disabled"];
|
|
109
109
|
readonly tokens: {
|
|
110
110
|
readonly bg: readonly ["bg-surface", "bg-disabled"];
|
|
111
111
|
readonly fg: readonly [];
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"file-upload.d.ts","sourceRoot":"","sources":["../../../src/components/FileUpload/file-upload.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAA;AAQ9B;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AAEH;;;GAGG;AACH,MAAM,WAAW,gBAAgB;IAC/B,EAAE,EAAE,MAAM,CAAA;IACV,IAAI,EAAE,MAAM,CAAA;IACZ,YAAY;IACZ,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,mDAAmD;IACnD,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,MAAM,CAAC,EAAE,WAAW,GAAG,WAAW,GAAG,OAAO,CAAA;IAC5C,oCAAoC;IACpC,WAAW,CAAC,EAAE,KAAK,CAAC,SAAS,CAAA;IAC7B,kCAAkC;IAClC,YAAY,CAAC,EAAE,MAAM,CAAA;CACtB;AAED,MAAM,WAAW,eAAgB,SAAQ,IAAI,CAAC,KAAK,CAAC,cAAc,CAAC,cAAc,CAAC,EAAE,QAAQ,CAAC;IAC3F,QAAQ,CAAC,EAAE,CAAC,KAAK,EAAE,IAAI,EAAE,KAAK,IAAI,CAAA;IAClC,QAAQ,CAAC,EAAE,CAAC,KAAK,EAAE,IAAI,EAAE,EAAE,MAAM,EAAE,MAAM,GAAG,MAAM,KAAK,IAAI,CAAA;IAC3D,QAAQ,CAAC,EAAE,OAAO,CAAA;IAClB,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,QAAQ,CAAC,EAAE,OAAO,CAAA;IAClB;;;;;OAKG;IACH,OAAO,CAAC,EAAE,UAAU,GAAG,QAAQ,CAAA;IAC/B,iDAAiD;IACjD,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB;;;;;;OAMG;IACH,OAAO,CAAC,EAAE,OAAO,CAAA;IACjB,gCAAgC;IAChC,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,kDAAkD;IAClD,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,+GAA+G;IAC/G,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,iCAAiC;IACjC,QAAQ,CAAC,EAAE,KAAK,CAAC,SAAS,CAAA;IAC1B;;;;;;;OAOG;IACH,KAAK,CAAC,EAAE,gBAAgB,EAAE,CAAA;IAC1B,yFAAyF;IACzF,YAAY,CAAC,EAAE,SAAS,GAAG,MAAM,CAAA;IACjC,6EAA6E;IAC7E,QAAQ,CAAC,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,IAAI,CAAA;IAC/B,4EAA4E;IAC5E,eAAe,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,MAAM,CAAA;CAC3C;AAGD,QAAA,MAAM,UAAU,
|
|
1
|
+
{"version":3,"file":"file-upload.d.ts","sourceRoot":"","sources":["../../../src/components/FileUpload/file-upload.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAA;AAQ9B;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AAEH;;;GAGG;AACH,MAAM,WAAW,gBAAgB;IAC/B,EAAE,EAAE,MAAM,CAAA;IACV,IAAI,EAAE,MAAM,CAAA;IACZ,YAAY;IACZ,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,mDAAmD;IACnD,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,MAAM,CAAC,EAAE,WAAW,GAAG,WAAW,GAAG,OAAO,CAAA;IAC5C,oCAAoC;IACpC,WAAW,CAAC,EAAE,KAAK,CAAC,SAAS,CAAA;IAC7B,kCAAkC;IAClC,YAAY,CAAC,EAAE,MAAM,CAAA;CACtB;AAED,MAAM,WAAW,eAAgB,SAAQ,IAAI,CAAC,KAAK,CAAC,cAAc,CAAC,cAAc,CAAC,EAAE,QAAQ,CAAC;IAC3F,QAAQ,CAAC,EAAE,CAAC,KAAK,EAAE,IAAI,EAAE,KAAK,IAAI,CAAA;IAClC,QAAQ,CAAC,EAAE,CAAC,KAAK,EAAE,IAAI,EAAE,EAAE,MAAM,EAAE,MAAM,GAAG,MAAM,KAAK,IAAI,CAAA;IAC3D,QAAQ,CAAC,EAAE,OAAO,CAAA;IAClB,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,QAAQ,CAAC,EAAE,OAAO,CAAA;IAClB;;;;;OAKG;IACH,OAAO,CAAC,EAAE,UAAU,GAAG,QAAQ,CAAA;IAC/B,iDAAiD;IACjD,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB;;;;;;OAMG;IACH,OAAO,CAAC,EAAE,OAAO,CAAA;IACjB,gCAAgC;IAChC,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,kDAAkD;IAClD,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,+GAA+G;IAC/G,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,iCAAiC;IACjC,QAAQ,CAAC,EAAE,KAAK,CAAC,SAAS,CAAA;IAC1B;;;;;;;OAOG;IACH,KAAK,CAAC,EAAE,gBAAgB,EAAE,CAAA;IAC1B,yFAAyF;IACzF,YAAY,CAAC,EAAE,SAAS,GAAG,MAAM,CAAA;IACjC,6EAA6E;IAC7E,QAAQ,CAAC,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,IAAI,CAAA;IAC/B,4EAA4E;IAC5E,eAAe,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,MAAM,CAAA;CAC3C;AAGD,QAAA,MAAM,UAAU,wFA2Pf,CAAA;AA0BD,eAAO,MAAM,cAAc;;;;;;;;;;;;;;;;;;CAiBjB,CAAA;AAEV,OAAO,EAAE,UAAU,EAAE,CAAA"}
|