@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
|
@@ -193,7 +193,7 @@ Field render 時迭代**全部** control child 的 `type.fieldLayout`,**任一**
|
|
|
193
193
|
|
|
194
194
|
**第一行對齊責任**:Field 的 block control area **不加 paddingTop**(避免跟 RadioGroup 自帶 `py = calc((field-height - 1lh) / 2)` double)。需 label 對齊的 block primitive 自行保證第一行位置(RadioGroup/CheckboxGroup 自帶 ✓;FileDropzone/RichTextEditor 自管)。
|
|
195
195
|
|
|
196
|
-
**已宣告 block**:`RadioGroup`、`CheckboxGroup`(checkbox-group.tsx:
|
|
196
|
+
**已宣告 block**:`RadioGroup`、`CheckboxGroup`(checkbox-group.tsx:93)。新增 `FileDropzone` 等同模式加 `fieldLayout = 'block'`。
|
|
197
197
|
|
|
198
198
|
---
|
|
199
199
|
|
|
@@ -253,8 +253,8 @@ Field 透過 Context 暴露以下狀態給子元件(Primitive 可以透過 `us
|
|
|
253
253
|
|
|
254
254
|
label 文字後可帶 info icon(ℹ)hover 出 tooltip 補充說明:`<FieldLabel info="說明文字">`。
|
|
255
255
|
|
|
256
|
-
- **與 label 間距 `gap-1`(4px)**、InfoIcon **16px 固定**、色 `fg-muted` hover `fg-secondary`(field.tsx
|
|
257
|
-
- **disabled 時整顆不渲染**(
|
|
256
|
+
- **與 label 間距 `gap-1`(4px)**、InfoIcon **16px 固定**、色 `fg-muted` hover `fg-secondary`(field.tsx L391-416)
|
|
257
|
+
- **disabled 時整顆不渲染**(L403)— info 是 action affordance(hover 互動),非類型身份 indicator,non-editable 隱藏
|
|
258
258
|
- 設計定位:inline action pattern(補充工具,視覺退後)— label 的 primary interaction 是 input,info 是輔助
|
|
259
259
|
|
|
260
260
|
---
|
|
@@ -386,16 +386,18 @@ Field 內的資料輸入控件(Input / NumberInput / DatePicker / Select / Com
|
|
|
386
386
|
|
|
387
387
|
## Field state machine SSOT(v13.3)
|
|
388
388
|
|
|
389
|
-
**Canonical
|
|
389
|
+
**Canonical**(2026-07-04 Q1 拍板修訂):**focus dominates everything — 除 error**。無 error 時 cursor in input = 藍;**有 error 時聚焦維持紅**(error 勝 focus 的「顏色」通道;修錯中不可失去 error 訊號)。開始編輯 → edit-clears-error(`useFormValidation`)→ 自然回 focus 藍。Benchmark 實查:MUI(error+focused 紅框加粗,OutlinedInput.js error rule 排 focused 後)/ Ant 5(`activeBorderColor: colorError` 紅框紅 glow,input/style/variants.ts)/ Polaris(紅 border 聚焦持續 + 獨立藍 outline,TextField.module.css)三家聚焦時 error 紅皆保留;唯一切藍的 Carbon 靠 in-field 紅 icon 補償(DS 無此通道故不採)。SSOT 全在 `field-wrapper.tsx` compoundVariants(**含 error** — 2026-07-04 起 `error` 為 cva 變體,原逐 control `border-error` 系列已 retire,控件只傳 `error` flag)。
|
|
390
390
|
|
|
391
391
|
| State | Token | CSS |
|
|
392
392
|
|---|---|---|
|
|
393
393
|
| rest | `--border` | `border-border` |
|
|
394
394
|
| hover(無 focus)| `--border-hover` | `hover:border-border-hover` |
|
|
395
|
-
| **focus** | `--primary` | **`focus-within:!border-primary`**(
|
|
395
|
+
| **focus(無 error)** | `--primary` | **`focus-within:!border-primary`**(error:false compound;`!important` 勝 data-state)|
|
|
396
396
|
| focus + hover | `--primary` | `focus-within:hover:!border-primary`(M11 AND case)|
|
|
397
397
|
| open(無 focus)| `--border-hover` | `data-[state=open]:border-border-hover` |
|
|
398
|
-
| error | `--error` | `focus-within
|
|
398
|
+
| **error(edit 全程,含聚焦)** | `--error` | `border-error hover:border-error-hover focus-within:!border-error focus-within:hover:!border-error`(error:true compound)|
|
|
399
|
+
|
|
400
|
+
**Error × focus 疊加 canonical(2026-07-05 user 拍板「照 Mantine」)**:錯誤欄位聚焦時**視覺完全不變**(紅框同色同寬,無加深/加粗/ring/暈)。依據:DS 聚焦主機制 =「邊框換 primary 基準色、無第二訊號」,12 家實查中唯一同款機制的 Mantine(Input.module.css:focus 僅 border 換 primary-filled、outline none)在此題同樣選擇不變;保紅又加訊號的三家(MUI 粗 / Ant 暈 / Bootstrap 暈)前提是其一般聚焦本來就有該第二訊號可染紅,DS 無此前提。**已知代價(documented tradeoff,非 bug)**:無游標的觸發器(Select / DatePicker / TimePicker)error 聚焦時無視覺聚焦指示(文字框靠游標);Mantine 同款弱點。未來若升級全 DS 聚焦語言(一般態加第二訊號),本題應同步重議(錯誤態染紅該訊號,對齊 MUI/Ant/Bootstrap 結構)。D4 finding「error+focus 零 focus delta」據此結案為 user-拍板 intentional。
|
|
399
401
|
|
|
400
402
|
副作用(自動達成 Ant「選後藍 / 取消灰」):選 option → Radix `onCloseAutoFocus` return focus → focus-within fires → 藍 / 點外取消 → focus 移外 → 灰。純 focus 機制無需 transient class。
|
|
401
403
|
|
|
@@ -414,7 +416,7 @@ Field 內的資料輸入控件(Input / NumberInput / DatePicker / Select / Com
|
|
|
414
416
|
| **L1 Slot** | `patterns/element-anatomy/item-anatomy.tsx` | `<ItemPrefix>` / `<ItemSuffix>`(永遠 `h-[1lh]` 對齊第 1 行) |
|
|
415
417
|
| **L2 Align** | `field-wrapper.tsx` `nakedCellRowModeAlign` | host cell `data-row-mode` propagate(autoRow→items-start / fixed→items-center)|
|
|
416
418
|
| **L3 State machine** | **繼承 Field default v13.3**(↑ 上方 SSOT)| 不重定義 |
|
|
417
|
-
| **L4 Display hover** | `field-wrapper.tsx` `nakedCellEditableDisplayHover` | editable cell display mode `outline-1 outline-offset:-1 outline-[var(--border-hover)]`(cell wrapper outline)。**v15.13 design intent(user-accepted 2026-05-07)**:hover ring 蓋 cell.borderRight。**known asymmetry**(2026-05-09 user 重抓):只 **right** 邊 outline 跟 cell own border-r 同位置覆蓋成 1 條;top/left/bottom 邊 outline 在 cell.outer 內 1px,grid lines(前 row border-b / 前 cell border-r / row 自己 border-b 在 cell.outer 外 1px)露出 → 視覺 4 邊不對稱。**Pending design decision**(等 user 拍板才 ship):4 邊都蓋(spec 1 selection-style)vs 4 邊都不蓋(spec 2 inset 1px)vs 維持現狀。三層 SSOT:state source = cell wrapper / paint owner = 混合(hover=cell / edit=Field)/ grid divider = DataTable own |
|
|
419
|
+
| **L4 Display hover** | `field-wrapper.tsx` `nakedCellEditableDisplayHover` | editable cell display mode `outline-1 outline-offset:-1 outline-[var(--cell-hover-outline-color,var(--border-hover))]`(cell wrapper outline;2026-05-10 Slice D:spreadsheet overlay 啟用時 DataTable cell host 可 set `--cell-hover-outline-color: transparent` 抑制 outline、讓 overlay layer 接管 hover ring paint,fallback 即 `--border-hover` 既有行為不變)。**v15.13 design intent(user-accepted 2026-05-07)**:hover ring 蓋 cell.borderRight。**known asymmetry**(2026-05-09 user 重抓):只 **right** 邊 outline 跟 cell own border-r 同位置覆蓋成 1 條;top/left/bottom 邊 outline 在 cell.outer 內 1px,grid lines(前 row border-b / 前 cell border-r / row 自己 border-b 在 cell.outer 外 1px)露出 → 視覺 4 邊不對稱。**Pending design decision**(等 user 拍板才 ship):4 邊都蓋(spec 1 selection-style)vs 4 邊都不蓋(spec 2 inset 1px)vs 維持現狀。三層 SSOT:state source = cell wrapper / paint owner = 混合(hover=cell / edit=Field)/ grid divider = DataTable own |
|
|
418
420
|
| **L5 Position(v14,2026-05-06 revert v12)** | naked compoundVariant flow | Field 留 layout flow,**不**用 `!absolute`(v12 absolute 跟 autoRowHeight 不相容)。視覺接受 cell `border-r grid` + Field border 2px 雙線;cell border-r divider 永遠保留(對齊 L4 Invariant divider-owner / editor-owner 分離)|
|
|
419
421
|
| **L6 Cell trigger indicator(v15,2026-05-09)** | naked-display branch + `showDisplayEndIcon?: boolean` opt-in | DataTable cell-registry 顯式傳 `showDisplayEndIcon` → Field component 內部 `<ItemSuffix>` 渲對應 trigger icon(Select/Combobox/PeoplePicker → ChevronDown / DatePicker → CalendarIcon / TimePicker → Clock / LinkInput 例外無 suffix)。table 外用法不傳此 prop = 純展示無 icon。詳 `.claude/planning/cell-indicator-ssot-rfc.md`。|
|
|
420
422
|
|
|
@@ -527,7 +527,8 @@ export const fieldMeta = {
|
|
|
527
527
|
sizes: {
|
|
528
528
|
|
|
529
529
|
},
|
|
530
|
-
|
|
530
|
+
// 'active' 移除 — field-wrapper compoundVariants 無按壓態(TimePicker 2026-07-04 audit 同結論)(2026-07-07 詞彙統一 DS-wide 按壓訊號盤點:檔內 0 active: utility / 0 *-active token)。
|
|
531
|
+
states: ['default', 'hover', 'focus-visible', 'disabled'],
|
|
531
532
|
tokens: {
|
|
532
533
|
bg: [],
|
|
533
534
|
fg: ['text-error-text', 'text-fg-disabled', 'text-fg-muted', 'text-fg-secondary', 'text-foreground'],
|
|
@@ -41,9 +41,11 @@
|
|
|
41
41
|
### Submit 驗證
|
|
42
42
|
|
|
43
43
|
7. **Submit 驗證全部**——點擊 submit 時對所有欄位執行驗證(不依賴個別 field 的 blur 狀態)
|
|
44
|
-
8. **Anchor 到第一個錯誤**——若有任何欄位出錯,scroll 並 focus
|
|
44
|
+
8. **Anchor 到第一個錯誤**——若有任何欄位出錯,scroll 並 focus 到第一個錯誤欄位(**「第一個」= DOM 視覺順序**,非 validate key 宣告順序;對齊瀏覽器原生 reportValidity + react-hook-form shouldFocusError,2026-07-07 code 同步)。多次 submit 重試時,每次都重新驗證全部欄位並重新計算「第一個錯誤」(rule 7 的自然結果),不保持上次 anchor 位置
|
|
45
45
|
9. **Async / cross-field 驗證 defer 到 submit**——某些驗證無法在 blur 當下完成(如「名稱是否重複」需要 API 查詢、跨欄位邏輯如「結束日不得早於開始日」),這些在 submit 時統一判斷。若有錯誤,同樣 anchor 到第一個出錯欄位。
|
|
46
46
|
|
|
47
|
+
**Double-submit 防護(2026-07-05 D4 codify,規則 9 的必然配套)**:async onSubmit 進行期間,重複 submit(連點按鈕 / 連按 Enter)一律忽略——否則業務層被並發呼叫兩次(重複建立資源的經典事故)。submit 期間狀態以 `isSubmitting` 暴露(餵 Button loading / disabled;`submitDisabled` 同步為 true);onSubmit 拋錯時先復位 `isSubmitting` 再讓錯誤原樣上拋(不吞錯,表單回到可重送狀態)。對齊 react-hook-form `formState.isSubmitting` / Polaris / Mantine form submitting 內建。 <!-- @benchmark-unverified -->
|
|
48
|
+
|
|
47
49
|
### 驗證分層
|
|
48
50
|
|
|
49
51
|
| 層級 | 負責者 | 時機 | 範例 |
|
|
@@ -81,6 +83,7 @@ const form = useFormValidation({
|
|
|
81
83
|
| Blur validation timing(1/2/6)+ Edit 清 error(5)+ Escape 回復(4) | **`useFormValidation` 內建(不可配置)** |
|
|
82
84
|
| Submit 全驗 + anchor 第一個錯誤(7/8)+ 業務/async 錯誤同軌(9) | **`useFormValidation.handleSubmit` 內建** |
|
|
83
85
|
| Dirty tracking + Submit button 狀態(Create/Update) | **`useFormValidation.submitDisabled`** |
|
|
86
|
+
| Double-submit 防護 + submit 進行中狀態(規則 9 配套) | **`useFormValidation.handleSubmit` 重入 guard + `isSubmitting`** |
|
|
84
87
|
| Field error visual(紅框 + FieldError) | Field 元件 `invalid` context(既有,hook 不侵入) |
|
|
85
88
|
|
|
86
89
|
**v1 邊界**(誠實 documented):(a) `getInputProps` 支援 value/onChange 型控件(Input / Textarea / NumberInput / Select / Combobox / DatePicker / TimePicker;onChange 收 event 或裸值皆可);Checkbox / Switch(onCheckedChange)用 `setFieldValue` 自接。(b) focus-first-error 以 DOM `name` 屬性定位 —— native input 生效;非 native 控件 focus 略過(error 視覺仍由 Field 紅框 + FieldError 呈現)。(c) 不用 hook 的 consumer 仍可全手動(Field `invalid` prop 是 engine-agnostic 的,見 field.spec.md 定位)。
|
|
@@ -147,10 +150,10 @@ Email 格式 / URL 格式 / 必填等「single-field 純 syntax」blur 即可判
|
|
|
147
150
|
|
|
148
151
|
Form validation 的 ARIA / 鍵盤行為(對齊 WCAG 3.3.1 Error Identification + 3.3.3 Error Suggestion):
|
|
149
152
|
|
|
150
|
-
- **Error message ARIA
|
|
153
|
+
- **Error message ARIA**:`<FieldError>` 容器 id = `{fieldId}-error`(fieldId 為 Field 的 `id` prop / `useId`,field.tsx:174),控件經 Field context 自動接 `aria-errormessage`(有 error 時指向 errorId)+ `aria-invalid="true"`;`aria-describedby` 保留給 FieldDescription(descriptionId)。SR 在 focus field 時可得 label + error 完整資訊;接線 SSOT 見 `field.spec.md`「驗證與 aria 屬性」段(input.tsx:187-190)
|
|
151
154
|
- **Submit error scroll**:submit 失敗後,focus 自動 jump 到第一個 invalid field(`field.focus()` + `scrollIntoView({block: 'center'})`);對齊 Material / Atlassian 慣例 <!-- @benchmark-unverified -->
|
|
152
155
|
- **Error live region**:跨欄位 / async error 用 `aria-live="polite"` 容器宣告 — SR 在空閒時讀出,不中斷使用者打字
|
|
153
|
-
- **Required indicator**:label 的 `*` 為純視覺、對讀屏隱藏(`aria-hidden="true"`,field.tsx:
|
|
156
|
+
- **Required indicator**:label 的 `*` 為純視覺、對讀屏隱藏(`aria-hidden="true"`,field.tsx:395);required 語意由內部輸入控件的 `aria-required`(input.tsx:188)承擔,避免讀屏讀出「asterisk」語義不清
|
|
154
157
|
- **Color-only error 警告**:error border 紅色之外必有文字訊息(WCAG 1.4.1 不僅靠顏色)— 由 `<FieldError>` 文字承擔;DS **不**在 input 內放 error 狀態 icon(見 `field-controls.spec.md`「禁止事項」)
|
|
155
158
|
|
|
156
159
|
## 相關
|
|
@@ -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 /
|
|
@@ -77,8 +79,10 @@ export interface UseFormValidationReturn<T extends FieldValues> {
|
|
|
77
79
|
errors: Partial<Record<keyof T, string>>
|
|
78
80
|
/** 任一欄位偏離 initialValues(深比對,還原回原值 = false) */
|
|
79
81
|
isDirty: boolean
|
|
80
|
-
/** Submit button disabled 狀態(intent 驅動,見 options.intent) */
|
|
82
|
+
/** Submit button disabled 狀態(intent 驅動,見 options.intent;submit 進行中一併 disabled) */
|
|
81
83
|
submitDisabled: boolean
|
|
84
|
+
/** Submit 進行中(await onSubmit 期間 true)— 餵 Button loading;double-submit 防護的 state 面 */
|
|
85
|
+
isSubmitting: boolean
|
|
82
86
|
/** Spread 到 value/onChange 型控件:`<Input {...form.getInputProps('name')} />` */
|
|
83
87
|
getInputProps: <K extends keyof T & string>(name: K) => FormFieldInputProps<T[K]>
|
|
84
88
|
/** 接 `<form onSubmit={form.handleSubmit}>`(規則 7/8/9) */
|
|
@@ -108,16 +112,21 @@ function extractValue(eventOrValue: unknown): unknown {
|
|
|
108
112
|
/** 規則 8:focus + scroll 到第一個錯誤欄位。以 DOM name 屬性定位(native input);
|
|
109
113
|
* 找不到(非 native 控件)→ 靜默略過,error 視覺仍由 Field 紅框呈現。 */
|
|
110
114
|
function focusFirstError(errorNames: string[]) {
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
115
|
+
// 2026-07-07 修(deep-audit A.1b 殘項):「第一個錯誤」以 DOM 順序為準(= 使用者看到的視覺第一),
|
|
116
|
+
// 非 validate key 宣告順序 — spec 規則 8 自然語意;對齊瀏覽器原生 reportValidity(DOM 序 focus
|
|
117
|
+
// 首個 invalid)+ react-hook-form shouldFocusError。
|
|
118
|
+
const els = errorNames
|
|
119
|
+
.map((name) => document.getElementsByName(name)[0] as HTMLElement | undefined)
|
|
120
|
+
.filter((el): el is HTMLElement => Boolean(el))
|
|
121
|
+
.sort((a, b) => (a.compareDocumentPosition(b) & Node.DOCUMENT_POSITION_FOLLOWING ? -1 : 1))
|
|
122
|
+
const el = els[0]
|
|
123
|
+
if (el) {
|
|
124
|
+
el.focus()
|
|
125
|
+
el.scrollIntoView({ block: 'center', behavior: 'smooth' })
|
|
118
126
|
}
|
|
119
127
|
}
|
|
120
128
|
|
|
129
|
+
// code-quality-allow: long-function — 單一 validation 生命週期 state machine(blur 驗證/edit 清 error/Escape 回復/submit 聚焦首錯)拆散會把耦合狀態跨函式傳遞,降低可讀性;對齊 react-hook-form useForm 本體同級長度
|
|
121
130
|
export function useFormValidation<T extends FieldValues>(
|
|
122
131
|
options: UseFormValidationOptions<T>,
|
|
123
132
|
): UseFormValidationReturn<T> {
|
|
@@ -185,10 +194,18 @@ export function useFormValidation<T extends FieldValues>(
|
|
|
185
194
|
[form, validateField],
|
|
186
195
|
)
|
|
187
196
|
|
|
197
|
+
// 2026-07-05 D4 double-submit 防護:連點 submit / 連按 Enter 在 await onSubmit(規則 9
|
|
198
|
+
// async 業務驗證)期間會並發呼叫 onSubmit(重複建立資源的經典事故)。ref 同步擋重入
|
|
199
|
+
// (state 有 render 延遲,擋不住同 tick 連點);state 暴露 isSubmitting 餵 Button
|
|
200
|
+
// loading / disabled。對齊 RHF formState.isSubmitting / Polaris / Mantine form submitting。
|
|
201
|
+
const isSubmittingRef = React.useRef(false)
|
|
202
|
+
const [isSubmitting, setIsSubmitting] = React.useState(false)
|
|
203
|
+
|
|
188
204
|
/** 規則 7/8/9:submit 全驗 + anchor 第一個錯誤 + 業務錯誤同軌 */
|
|
189
205
|
const handleSubmit = React.useCallback(
|
|
190
206
|
async (e?: React.FormEvent) => {
|
|
191
207
|
e?.preventDefault()
|
|
208
|
+
if (isSubmittingRef.current) return // double-submit guard(見上方 comment)
|
|
192
209
|
const current = form.getValues()
|
|
193
210
|
// 規則 7:對所有 validate keys 全跑(不依賴個別 blur 狀態);每次 submit 重算(規則 8)
|
|
194
211
|
const formatErrors: string[] = []
|
|
@@ -210,18 +227,27 @@ export function useFormValidation<T extends FieldValues>(
|
|
|
210
227
|
return
|
|
211
228
|
}
|
|
212
229
|
// 規則 9:業務 / async / 跨欄位驗證 defer 到 submit(onSubmit 回傳 field-keyed errors)
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
230
|
+
// try/finally(2026-07-05 D4):onSubmit reject 先復位 isSubmitting 再讓 rejection 原樣
|
|
231
|
+
// 上拋(不吞錯,對齊 RHF handleSubmit re-throw canonical)— 表單回到可重送狀態而非卡死。
|
|
232
|
+
isSubmittingRef.current = true
|
|
233
|
+
setIsSubmitting(true)
|
|
234
|
+
try {
|
|
235
|
+
const businessErrors = await onSubmit(current)
|
|
236
|
+
if (businessErrors && typeof businessErrors === 'object') {
|
|
237
|
+
const names = Object.keys(businessErrors).filter(
|
|
238
|
+
(k) => businessErrors[k as keyof T] != null,
|
|
239
|
+
)
|
|
240
|
+
for (const name of names) {
|
|
241
|
+
form.setError(name as Path<T>, {
|
|
242
|
+
type: 'business',
|
|
243
|
+
message: businessErrors[name as keyof T] as string,
|
|
244
|
+
})
|
|
245
|
+
}
|
|
246
|
+
if (names.length > 0) focusFirstError(names)
|
|
223
247
|
}
|
|
224
|
-
|
|
248
|
+
} finally {
|
|
249
|
+
isSubmittingRef.current = false
|
|
250
|
+
setIsSubmitting(false)
|
|
225
251
|
}
|
|
226
252
|
},
|
|
227
253
|
[form, validate, onSubmit],
|
|
@@ -231,8 +257,10 @@ export function useFormValidation<T extends FieldValues>(
|
|
|
231
257
|
values,
|
|
232
258
|
errors,
|
|
233
259
|
isDirty,
|
|
234
|
-
// Submit Button 狀態 canonical:Create 永遠 enabled / Update disabled-until-dirty
|
|
235
|
-
|
|
260
|
+
// Submit Button 狀態 canonical:Create 永遠 enabled / Update disabled-until-dirty;
|
|
261
|
+
// submit 進行中一律 disabled(double-submit 防護的 UI 面,2026-07-05 D4)
|
|
262
|
+
submitDisabled: (intent === 'update' ? !isDirty : false) || isSubmitting,
|
|
263
|
+
isSubmitting,
|
|
236
264
|
getInputProps,
|
|
237
265
|
handleSubmit,
|
|
238
266
|
reset: () => form.reset(),
|
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
// @
|
|
1
|
+
// @anatomy-rationale: Inspector + ColorMatrix N/A — FieldControlGroup 是
|
|
2
2
|
// self-contained layout-primitive(border-collapse 接合 N 個 Field child),
|
|
3
3
|
// spec frontmatter `variants: {}` / `sizes: {}` / `traits: []`(明示 layout-primitive 無 trait variants)。
|
|
4
4
|
// 無 cva color variant → ColorMatrix N/A;無 interactive prop control(僅 children layout)→ Inspector N/A。
|
|
5
|
-
// 對齊 design-system-audit Dim 13
|
|
5
|
+
// 對齊 design-system-audit Dim 13 `@anatomy-rationale` 例外 marker(audit-prompts.md Dim 13;
|
|
6
|
+
// 2026-07-04 修:原誤用 @story-trait-rationale — 那是 R3 trait escape,Dim 13 機械檢查認不得)。
|
|
6
7
|
import type { Meta, StoryObj } from '@storybook/react'
|
|
7
8
|
import { FieldControlGroup } from './field-control-group'
|
|
8
9
|
import { Select } from '@/design-system/components/Select/select'
|
|
@@ -43,8 +44,8 @@ export const Overview: Story = {
|
|
|
43
44
|
<section>
|
|
44
45
|
<h3 className="text-body font-bold mb-2">範例</h3>
|
|
45
46
|
<FieldControlGroup block>
|
|
46
|
-
<Select className="w-[120px]" options={FIELDS} value="name" onChange={() => {}} aria-label="篩選欄位" />
|
|
47
|
-
<Select className="w-[100px]" options={OPS} value="contains" onChange={() => {}} aria-label="篩選運算子" />
|
|
47
|
+
<Select className="!w-[120px]" options={FIELDS} value="name" onChange={() => {}} aria-label="篩選欄位" />
|
|
48
|
+
<Select className="!w-[100px]" options={OPS} value="contains" onChange={() => {}} aria-label="篩選運算子" />
|
|
48
49
|
<Input className="flex-1" defaultValue="無線滑鼠" aria-label="篩選值" />
|
|
49
50
|
</FieldControlGroup>
|
|
50
51
|
</section>
|
|
@@ -61,7 +62,7 @@ export const SizeMatrix: Story = {
|
|
|
61
62
|
<div key={size}>
|
|
62
63
|
<p className="text-caption text-fg-muted mb-2">size="{size}"</p>
|
|
63
64
|
<FieldControlGroup block>
|
|
64
|
-
<Select size={size} className="w-[120px]" options={FIELDS} value="name" onChange={() => {}} aria-label="篩選欄位" />
|
|
65
|
+
<Select size={size} className="!w-[120px]" options={FIELDS} value="name" onChange={() => {}} aria-label="篩選欄位" />
|
|
65
66
|
<Input size={size} className="flex-1" defaultValue="無線滑鼠" aria-label="篩選值" />
|
|
66
67
|
</FieldControlGroup>
|
|
67
68
|
</div>
|
|
@@ -78,21 +79,21 @@ export const StateBehavior: Story = {
|
|
|
78
79
|
<div>
|
|
79
80
|
<p className="text-caption text-fg-muted mb-2">default</p>
|
|
80
81
|
<FieldControlGroup block>
|
|
81
|
-
<Select className="w-[120px]" options={FIELDS} value="name" onChange={() => {}} aria-label="篩選欄位" />
|
|
82
|
+
<Select className="!w-[120px]" options={FIELDS} value="name" onChange={() => {}} aria-label="篩選欄位" />
|
|
82
83
|
<Input className="flex-1" defaultValue="無線滑鼠" aria-label="篩選值" />
|
|
83
84
|
</FieldControlGroup>
|
|
84
85
|
</div>
|
|
85
86
|
<div>
|
|
86
87
|
<p className="text-caption text-fg-muted mb-2">disabled(整 group children 各自 disabled)</p>
|
|
87
88
|
<FieldControlGroup block>
|
|
88
|
-
<Select className="w-[120px]" options={FIELDS} value="name" onChange={() => {}} disabled aria-label="篩選欄位" />
|
|
89
|
+
<Select className="!w-[120px]" options={FIELDS} value="name" onChange={() => {}} disabled aria-label="篩選欄位" />
|
|
89
90
|
<Input className="flex-1" defaultValue="無線滑鼠" disabled aria-label="篩選值" />
|
|
90
91
|
</FieldControlGroup>
|
|
91
92
|
</div>
|
|
92
93
|
<div>
|
|
93
94
|
<p className="text-caption text-fg-muted mb-2">cell error(其中一 child invalid → border-error;聚焦該 child 時才提升到 z-3 蓋過鄰接 border)</p>
|
|
94
95
|
<FieldControlGroup block>
|
|
95
|
-
<Select className="w-[120px]" options={FIELDS} value="name" onChange={() => {}} aria-label="篩選欄位" />
|
|
96
|
+
<Select className="!w-[120px]" options={FIELDS} value="name" onChange={() => {}} aria-label="篩選欄位" />
|
|
96
97
|
<Input className="flex-1" defaultValue="無線滑鼠" error aria-label="篩選值" />
|
|
97
98
|
</FieldControlGroup>
|
|
98
99
|
</div>
|
|
@@ -109,8 +110,8 @@ export const Accessibility: Story = {
|
|
|
109
110
|
Container 不加 ARIA role(透明 wrapper);children 各自 aria-label。Tab 鍵在 children 之間正常移動,no focus trap。
|
|
110
111
|
</p>
|
|
111
112
|
<FieldControlGroup block>
|
|
112
|
-
<Select className="w-[120px]" options={FIELDS} value="name" onChange={() => {}} aria-label="篩選欄位" />
|
|
113
|
-
<Select className="w-[100px]" options={OPS} value="contains" onChange={() => {}} aria-label="篩選運算子" />
|
|
113
|
+
<Select className="!w-[120px]" options={FIELDS} value="name" onChange={() => {}} aria-label="篩選欄位" />
|
|
114
|
+
<Select className="!w-[100px]" options={OPS} value="contains" onChange={() => {}} aria-label="篩選運算子" />
|
|
114
115
|
<Input className="flex-1" defaultValue="無線滑鼠" aria-label="篩選值" />
|
|
115
116
|
</FieldControlGroup>
|
|
116
117
|
</div>
|
|
@@ -101,7 +101,7 @@ export const UsageGuidance: Story = {
|
|
|
101
101
|
<Field>
|
|
102
102
|
<FieldLabel>聯絡電話</FieldLabel>
|
|
103
103
|
<FieldControlGroup block>
|
|
104
|
-
<Select className="w-[88px]" options={CODES} value="+886" onChange={() => {}} />
|
|
104
|
+
<Select className="!w-[88px]" options={CODES} value="+886" onChange={() => {}} />
|
|
105
105
|
<Input className="flex-1" placeholder="912 345 678" />
|
|
106
106
|
</FieldControlGroup>
|
|
107
107
|
</Field>
|
|
@@ -111,8 +111,8 @@ export const UsageGuidance: Story = {
|
|
|
111
111
|
<section>
|
|
112
112
|
<h2 className="text-body-lg font-bold mb-3">Mode B — Filter row 場景</h2>
|
|
113
113
|
<FieldControlGroup block>
|
|
114
|
-
<Select className="w-[140px]" options={FIELDS} value="name" onChange={() => {}} />
|
|
115
|
-
<Select className="w-[120px]" options={OPS} value="contains" onChange={() => {}} />
|
|
114
|
+
<Select className="!w-[140px]" options={FIELDS} value="name" onChange={() => {}} />
|
|
115
|
+
<Select className="!w-[120px]" options={OPS} value="contains" onChange={() => {}} />
|
|
116
116
|
<Input className="flex-1" defaultValue="無線滑鼠" />
|
|
117
117
|
</FieldControlGroup>
|
|
118
118
|
</section>
|
|
@@ -79,7 +79,7 @@ interface FieldControlGroupProps extends HTMLAttributes<HTMLDivElement> {
|
|
|
79
79
|
## Width 配置 canonical(W-A,對齊 Ant)
|
|
80
80
|
|
|
81
81
|
**子 controls 自管 width**:
|
|
82
|
-
- Fixed:`<Select className="w-[140px]">`
|
|
82
|
+
- Fixed:`<Select className="!w-[140px]">`
|
|
83
83
|
- Flex:`<Input className="flex-1">`
|
|
84
84
|
- 混合 OK(filter row 典型:fixed + fixed + flex)
|
|
85
85
|
|
|
@@ -94,7 +94,7 @@ interface FieldControlGroupProps extends HTMLAttributes<HTMLDivElement> {
|
|
|
94
94
|
<Field size="lg">
|
|
95
95
|
<FieldLabel>電話</FieldLabel>
|
|
96
96
|
<FieldControlGroup> {/* 各 child 自動繼承外層 Field context size */}
|
|
97
|
-
<Select className="w-[80px]" options={codes} />
|
|
97
|
+
<Select className="!w-[80px]" options={codes} />
|
|
98
98
|
<Input className="flex-1" />
|
|
99
99
|
</FieldControlGroup>
|
|
100
100
|
</Field>
|
|
@@ -103,7 +103,7 @@ interface FieldControlGroupProps extends HTMLAttributes<HTMLDivElement> {
|
|
|
103
103
|
**Mode B**(standalone):**逐一給每個 child 設 `size`**(設 `<FieldControlGroup size>` 無效)。
|
|
104
104
|
```tsx
|
|
105
105
|
<FieldControlGroup>
|
|
106
|
-
<Select size="md" className="w-[120px]" options={fields} />
|
|
106
|
+
<Select size="md" className="!w-[120px]" options={fields} />
|
|
107
107
|
<Input size="md" className="flex-1" />
|
|
108
108
|
</FieldControlGroup>
|
|
109
109
|
```
|
|
@@ -39,8 +39,8 @@ import type { FieldSize } from '@/design-system/components/Field/field-context'
|
|
|
39
39
|
* ```tsx
|
|
40
40
|
* // Filter row: 2 fixed select + 1 flex value
|
|
41
41
|
* <FieldControlGroup>
|
|
42
|
-
* <Select className="w-[140px]" options={fields} />
|
|
43
|
-
* <Select className="w-[120px]" options={ops} />
|
|
42
|
+
* <Select className="!w-[140px]" options={fields} />
|
|
43
|
+
* <Select className="!w-[120px]" options={ops} />
|
|
44
44
|
* <Input className="flex-1" placeholder="輸入值..." />
|
|
45
45
|
* </FieldControlGroup>
|
|
46
46
|
*
|
|
@@ -48,7 +48,7 @@ import type { FieldSize } from '@/design-system/components/Field/field-context'
|
|
|
48
48
|
* <Field>
|
|
49
49
|
* <FieldLabel>電話</FieldLabel>
|
|
50
50
|
* <FieldControlGroup>
|
|
51
|
-
* <Select className="w-[80px]" options={codes} />
|
|
51
|
+
* <Select className="!w-[80px]" options={codes} />
|
|
52
52
|
* <Input className="flex-1" />
|
|
53
53
|
* </FieldControlGroup>
|
|
54
54
|
* </Field>
|
|
@@ -46,8 +46,9 @@ export const Overview: Story = {
|
|
|
46
46
|
{[
|
|
47
47
|
['name', 'string', '必填', '檔名'],
|
|
48
48
|
['mode', "'compact' | 'rich'", "'compact'", 'compact=Paperclip 16px icon / rich=Avatar 48px 縮圖'],
|
|
49
|
+
['surface', "'form' | 'upload-manager'", "'form'", '所在容器 context:form=rich 為 border card / upload-manager=rich 無邊框無 bg(面板自身是容器,avatar 作 item 邊界)+ padding 縮減。詳 spec「邊框 / 背景」「Padding」表'],
|
|
49
50
|
['status', "'uploading' | 'completed' | 'error'", '—', '上傳狀態(不傳=已上傳靜態)'],
|
|
50
|
-
['progress', 'number', '
|
|
51
|
+
['progress', 'number', '0', '上傳進度 0-100(有 status 時顯示 bar;completed 強制 100%)'],
|
|
51
52
|
['description', 'ReactNode', '—', 'rich 任意場景 / compact 只有 error 才顯示。ReactNode — 可含 inline clickable link(如「View log」)'],
|
|
52
53
|
['thumbnailSrc', 'string', '—', 'rich mode 的縮圖 URL(圖片類檔案)'],
|
|
53
54
|
['actions', 'ReactNode', '—', 'suffix actions(例:delete / cancel button)'],
|
|
@@ -101,7 +102,7 @@ export const ColorMatrix: Story = {
|
|
|
101
102
|
<H3>Status × 元素 色彩矩陣</H3>
|
|
102
103
|
<Desc>
|
|
103
104
|
FileItem 本身無色彩變體——text 走 item-anatomy row primitive 共用 token
|
|
104
|
-
(`--foreground` / `--fg-secondary`);background 依 mode
|
|
105
|
+
(`--foreground` / `--fg-secondary`);background 依 mode × surface 決定(rich + surface=form = `--surface` + border / rich + surface=upload-manager = 無邊框無 bg / compact 無 status = `--secondary` / compact 有 status = transparent),**無 hover-bg**(見下方 Container background table)。
|
|
105
106
|
Status 才驅動色彩:progress bar 色(inProgress / success / error)+ status icon 色(check / X)+ description 色(error 時升階)。
|
|
106
107
|
</Desc>
|
|
107
108
|
<div className="overflow-x-auto mb-4">
|
|
@@ -152,7 +153,7 @@ export const ColorMatrix: Story = {
|
|
|
152
153
|
<div>
|
|
153
154
|
<H3>Container background(per mode,**無 hover-bg**)</H3>
|
|
154
155
|
<Desc>
|
|
155
|
-
FileItem 設計準則(2026-04-23):**永不顯示 hover-bg
|
|
156
|
+
FileItem 設計準則(2026-04-23):**永不顯示 hover-bg**。各型態皆已 permanent-anchored(rich + surface=form = border card / rich + surface=upload-manager = avatar 作 item 邊界 / compact 無 status = bg-secondary / compact 有 status = 底部 progress bar),再加 hover-bg 是 double-emphasis 視覺雜。affordance 只靠 `cursor-pointer`(onClick 時)+ hover-swap icon fade。詳 spec「Hover 行為 canonical」。
|
|
156
157
|
</Desc>
|
|
157
158
|
<div className="overflow-x-auto">
|
|
158
159
|
<table className="text-caption border-collapse">
|
|
@@ -160,7 +161,8 @@ export const ColorMatrix: Story = {
|
|
|
160
161
|
<tr><Th>Mode / State</Th><Th>Background</Th><Th>Rationale</Th></tr>
|
|
161
162
|
</thead>
|
|
162
163
|
<tbody>
|
|
163
|
-
<tr><Td mono>rich(all status)</Td><Td><span className="inline-flex items-center gap-1.5"><Swatch value="--surface" size="sm" /><span className="font-mono">--surface</span> + border</span></Td><Td
|
|
164
|
+
<tr><Td mono>rich + surface=form(all status)</Td><Td><span className="inline-flex items-center gap-1.5"><Swatch value="--surface" size="sm" /><span className="font-mono">--surface</span> + border</span></Td><Td>card(border + rounded + bg-surface)——Slack / Notion attachment 慣例</Td></tr>
|
|
165
|
+
<tr><Td mono>rich + surface=upload-manager(all status)</Td><Td><span className="font-mono">無邊框無 bg(只留 rounded-md)</span></Td><Td>面板自身是容器,avatar 作 item 邊界。詳 spec「邊框 / 背景」表</Td></tr>
|
|
164
166
|
<tr><Td mono>compact 無 status</Td><Td><span className="inline-flex items-center gap-1.5"><Swatch value="--secondary" size="sm" /><span className="font-mono">--secondary</span>(= neutral-3)</span></Td><Td>靜態 pill,對齊 Badge low / ProgressBar track SSOT</Td></tr>
|
|
165
167
|
<tr><Td mono>compact 有 status</Td><Td><span className="font-mono">transparent</span></Td><Td>底部 progress bar 作 permanent affordance(分隔線型)</Td></tr>
|
|
166
168
|
<tr><Td mono>hover(任意 mode)</Td><Td><span className="font-mono">無變化</span></Td><Td>permanent-anchored → 不加 hover-bg。cursor-pointer 作 affordance(onClick 時)</Td></tr>
|
|
@@ -160,7 +160,8 @@ const FileItem = React.forwardRef<HTMLDivElement, FileItemProps>(
|
|
|
160
160
|
className={cn(
|
|
161
161
|
'shrink-0 transition-opacity',
|
|
162
162
|
statusConfig.color,
|
|
163
|
-
|
|
163
|
+
// 2026-07-05 D4:鍵盤 focus 同步觸發 swap(:has(:focus-visible),對齊 item-anatomy SUFFIX_HOVER_REVEAL_BY_GROUP SSOT)
|
|
164
|
+
hoverAction && 'group-hover/row:opacity-0 group-has-[:focus-visible]/row:opacity-0',
|
|
164
165
|
)}
|
|
165
166
|
aria-hidden
|
|
166
167
|
/>
|
|
@@ -173,7 +174,7 @@ const FileItem = React.forwardRef<HTMLDivElement, FileItemProps>(
|
|
|
173
174
|
startIcon={hoverAction.icon}
|
|
174
175
|
aria-label={hoverAction.label}
|
|
175
176
|
onClick={(e) => { e.stopPropagation(); hoverAction.onClick() }}
|
|
176
|
-
className="absolute inset-0 opacity-0 group-hover/row:opacity-100 transition-opacity"
|
|
177
|
+
className="absolute inset-0 opacity-0 group-hover/row:opacity-100 group-has-[:focus-visible]/row:opacity-100 transition-opacity"
|
|
177
178
|
/>
|
|
178
179
|
)}
|
|
179
180
|
</span>
|
|
@@ -440,7 +440,7 @@ export const Accessibility = {
|
|
|
440
440
|
render: () => (
|
|
441
441
|
<div className="max-w-3xl text-body text-fg-secondary">
|
|
442
442
|
<h3 className="text-h5 text-foreground mb-2">無障礙設計</h3>
|
|
443
|
-
<p className="whitespace-pre-line">{"詳 `
|
|
443
|
+
<p className="whitespace-pre-line">{"詳 `file-upload.spec.md` 「A11y 預設」段。摘要:\n\n- role=\"button\" + tabIndex=0 (disabled 時 -1 )\n- Enter / Space 鍵觸發檔案選取浮窗(模擬 click)\n- aria-disabled={true} 當 disabled\n- <input type=\"file\"> 以 hidden (display:none) 隱藏,移出無障礙樹;互動由外層 role=\"button\" wrapper 承載,accessible name 來自 wrapper 內 Empty 的 title + description 文字"}</p>
|
|
444
444
|
</div>
|
|
445
445
|
),
|
|
446
446
|
}
|
|
@@ -59,7 +59,7 @@ FileUpload 是**拖放 / 點擊上傳區塊**——可拖曳檔案進入或點
|
|
|
59
59
|
| data-state | 情境 | 視覺 |
|
|
60
60
|
|------------|------|------|
|
|
61
61
|
| `idle`(預設) | 使用者未互動 | dashed `border-border`(**元件邊框 `--border`,非 `--divider` 分隔線**)+ surface 底 |
|
|
62
|
-
| `hover` = `drag-over`(**統一,純 border-driven,對齊 Ant Dragger `colorPrimaryHover`**)| 游標懸停 / 拖檔進入 | **`border-primary
|
|
62
|
+
| `hover` = `drag-over`(**統一,純 border-driven,對齊 Ant Dragger `colorPrimaryHover`**)| 游標懸停 / 拖檔進入 | **`border-primary-hover`**(只變邊框;2026-07-06 修 claim-vs-code drift——spec 一直宣稱 colorPrimaryHover、code 原寫 base,瞬時態歸 hover 階)+ 底維持 surface(不變 bg)。state 信號靠邊框,非底色;兩態同視覺 |
|
|
63
63
|
| `loading`(**deferred**)| `loading` prop 為 true | **Deferred:其唯一用途(無清單單檔/頭像替換)場景未定義,showcase 僅呈現 3-state(idle/drag-over/disabled);prop 在 tsx 保留供未來。** 行為:CircularProgress 取代內容;`cursor-progress`(不加 `pointer-events-none`,互動由 isBlocked guard 擋);aria-busy=true。有清單的 flow 進度一律走 FileItem 自身的 progress bar(uploading status)—— FileUpload 內建 list 仍 `surface=form`(dropzone 非獨立浮層 upload manager,見 file-item.spec.md「upload-manager 浮層面板 composition」)|
|
|
64
64
|
| `disabled` | `disabled` prop 為 true | **語意 token(非 opacity — dashed outline surface 走 DS outline-disabled 慣例,3/4 世界級 Ant/Polaris/Carbon 用 token)**:`bg-disabled` 底 + 邊框不變色 + 文字/icon → `fg-disabled`(由 `<Empty disabled>` 控,icon-circle 維持 muted)+ `cursor-not-allowed`(移除 `pointer-events-none` 後生效)|
|
|
65
65
|
|
|
@@ -134,6 +134,9 @@ FileUpload 是**拖放 / 點擊上傳區塊**——可拖曳檔案進入或點
|
|
|
134
134
|
- **多檔拖放 + `multiple=false`**:只取過濾後第一個檔交給 `onUpload`,不拋錯(見「Props 行為」)
|
|
135
135
|
- **空選取(0 檔)**:不觸發任何 callback;`files={[]}` 不渲染清單(等同不傳)
|
|
136
136
|
- **maxSize / accept 全擋下**:`onUpload` 不觸發,只觸發 `onReject`
|
|
137
|
+
- **同檔重選**(2026-07-05 D4):input value 於 dispatch 後清空 — 「上傳失敗修正後重選同檔」「刪除後重傳同檔」仍觸發 `onUpload`(瀏覽器對同路徑檔案不重發 change event,不清空則靜默無反應)
|
|
138
|
+
- **拖曳滑過內部子元素**(2026-07-05 D4):pointer 移過 Empty 的 icon/title 時 drag-over 高亮不熄滅(dragleave 帶 relatedTarget guard,避免經典 drag-flicker)
|
|
139
|
+
- **Disabled / loading 時拖檔 drop**(2026-07-05 D4):操作被吞掉 — 仍 `preventDefault` 擋瀏覽器預設「在當前 tab 開啟被丟檔案」的整頁導航,`onUpload` 不觸發
|
|
137
140
|
- Disabled / loading 見「四狀態視覺」;dark mode / density 由 semantic token 自動處理
|
|
138
141
|
|
|
139
142
|
---
|
|
@@ -132,6 +132,10 @@ const FileUpload = React.forwardRef<HTMLDivElement, FileUploadProps>(
|
|
|
132
132
|
) => {
|
|
133
133
|
const inputRef = React.useRef<HTMLInputElement>(null)
|
|
134
134
|
const [isDragOver, setDragOver] = React.useState(false)
|
|
135
|
+
// drag enter/leave 深度計數(react-dropzone / Mantine Dropzone 同款):跨進內部子元素時
|
|
136
|
+
// 瀏覽器發「子 dragenter → 外層 dragleave」成對事件,+1/−1 抵銷後計數仍 > 0 = 沒離開。
|
|
137
|
+
// 不依賴 e.relatedTarget —— Safari/WebKit 的 dragleave relatedTarget 恆 null,guard 法必失效。
|
|
138
|
+
const dragDepth = React.useRef(0)
|
|
135
139
|
|
|
136
140
|
const filterAndDispatch = (files: FileList | null) => {
|
|
137
141
|
if (!files || files.length === 0) return
|
|
@@ -178,7 +182,7 @@ const FileUpload = React.forwardRef<HTMLDivElement, FileUploadProps>(
|
|
|
178
182
|
// 消費 FileItem「List wrapper canonical」(file-item.spec.md),取代原硬寫 gap-2 / mt-3(不分 mode)。
|
|
179
183
|
fileListMode === 'rich' ? 'gap-2 mt-2' : 'gap-1 mt-1',
|
|
180
184
|
)}
|
|
181
|
-
aria-label="
|
|
185
|
+
aria-label="已上傳的檔案"
|
|
182
186
|
>
|
|
183
187
|
{files!.map((f) => (
|
|
184
188
|
<li key={f.id} className="list-none">
|
|
@@ -226,7 +230,12 @@ const FileUpload = React.forwardRef<HTMLDivElement, FileUploadProps>(
|
|
|
226
230
|
multiple={multiple}
|
|
227
231
|
accept={accept}
|
|
228
232
|
disabled={disabled}
|
|
229
|
-
onChange={(e) =>
|
|
233
|
+
onChange={(e) => {
|
|
234
|
+
// 2026-07-05 D4:dispatch 後清空 value — 瀏覽器對同路徑檔案不重發 change,
|
|
235
|
+
// 不清空則「失敗修正後重選同檔 / 刪除後重傳同檔」靜默無反應
|
|
236
|
+
filterAndDispatch(e.target.files)
|
|
237
|
+
e.target.value = ''
|
|
238
|
+
}}
|
|
230
239
|
/>
|
|
231
240
|
<Button
|
|
232
241
|
variant="tertiary"
|
|
@@ -259,20 +268,29 @@ const FileUpload = React.forwardRef<HTMLDivElement, FileUploadProps>(
|
|
|
259
268
|
onDragEnter={(e) => {
|
|
260
269
|
if (isBlocked) return
|
|
261
270
|
e.preventDefault()
|
|
271
|
+
// 2026-07-06 深度計數升級(user 實測抓到「拖入約一秒高亮熄滅」):游標從邊緣飄到中央
|
|
272
|
+
// 跨進 Empty icon/標題那刻,瀏覽器發「子 dragenter → 外層 dragleave」→ 舊版無 guard 直接
|
|
273
|
+
// 熄滅;2026-07-05 的 relatedTarget guard 只救 Chrome(Safari/WebKit dragleave 的
|
|
274
|
+
// relatedTarget 恆 null → guard 失效)。深度計數不依賴 relatedTarget,跨瀏覽器穩定。
|
|
275
|
+
dragDepth.current += 1
|
|
262
276
|
setDragOver(true)
|
|
263
277
|
}}
|
|
264
278
|
onDragLeave={(e) => {
|
|
265
279
|
if (isBlocked) return
|
|
266
280
|
e.preventDefault()
|
|
267
|
-
|
|
281
|
+
dragDepth.current = Math.max(0, dragDepth.current - 1)
|
|
282
|
+
if (dragDepth.current === 0) setDragOver(false)
|
|
268
283
|
}}
|
|
269
284
|
onDragOver={(e) => {
|
|
270
|
-
|
|
285
|
+
// 2026-07-05 D4:blocked 時仍先 preventDefault — dragover 未取消 = 非合法 drop target,
|
|
286
|
+
// 瀏覽器 default 會在當前 tab 直接開啟被丟檔案(整頁導航,app 狀態全失);吞掉操作而非讓瀏覽器接管
|
|
271
287
|
e.preventDefault()
|
|
272
288
|
}}
|
|
273
289
|
onDrop={(e) => {
|
|
274
|
-
|
|
290
|
+
// 2026-07-05 D4:preventDefault 先於 isBlocked guard — 同 dragover,擋瀏覽器開檔導航
|
|
275
291
|
e.preventDefault()
|
|
292
|
+
if (isBlocked) return
|
|
293
|
+
dragDepth.current = 0 // drop 結束整輪拖曳,計數歸零(下一輪從乾淨狀態起算)
|
|
276
294
|
setDragOver(false)
|
|
277
295
|
filterAndDispatch(e.dataTransfer.files)
|
|
278
296
|
}}
|
|
@@ -287,9 +305,11 @@ const FileUpload = React.forwardRef<HTMLDivElement, FileUploadProps>(
|
|
|
287
305
|
// idle:--border(元件邊框,非 --divider 分隔線 — 2026-06-03 Q2 token 修正)+ surface 底
|
|
288
306
|
'border-border bg-surface',
|
|
289
307
|
// hover = drag-over 統一(2026-06-03 Q2-A 純 border-driven,對齊 Ant Dragger colorPrimaryHover):
|
|
290
|
-
// 兩者都 → primary
|
|
291
|
-
//
|
|
292
|
-
|
|
308
|
+
// 兩者都 → primary-hover 邊框(2026-07-06 修:註解與 spec 一直宣稱 colorPrimaryHover、
|
|
309
|
+
// code 卻寫 base = claim-vs-code 反向 drift;瞬時態歸 hover 階,雙向全掃唯一 B 類違規),
|
|
310
|
+
// 底色維持 surface(不變 bg)。state 信號靠邊框,非底色。
|
|
311
|
+
// 2026-06-11 R2(M5 狀態疊加):hover 變 primary 僅限 idle —— disabled/loading 維持原邊框(spec「disabled 邊框不變色」;dropzone 狀態機無 error 態,error 屬 files 清單 FileItem status)
|
|
312
|
+
'data-[state=idle]:hover:border-primary-hover data-[state=drag-over]:border-primary-hover',
|
|
293
313
|
// loading(2026-06-03 Q4:移除 pointer-events-none — 它會讓 cursor-progress 失效;
|
|
294
314
|
// 互動已由 handleClick + drag/key handlers 的 isBlocked guard 擋,不需 pointer-events-none)
|
|
295
315
|
'data-[state=loading]:cursor-progress',
|
|
@@ -307,7 +327,12 @@ const FileUpload = React.forwardRef<HTMLDivElement, FileUploadProps>(
|
|
|
307
327
|
multiple={multiple}
|
|
308
328
|
accept={accept}
|
|
309
329
|
disabled={disabled}
|
|
310
|
-
onChange={(e) =>
|
|
330
|
+
onChange={(e) => {
|
|
331
|
+
// 2026-07-05 D4:dispatch 後清空 value — 瀏覽器對同路徑檔案不重發 change,
|
|
332
|
+
// 不清空則「失敗修正後重選同檔 / 刪除後重傳同檔」靜默無反應
|
|
333
|
+
filterAndDispatch(e.target.files)
|
|
334
|
+
e.target.value = ''
|
|
335
|
+
}}
|
|
311
336
|
/>
|
|
312
337
|
{loading ? (
|
|
313
338
|
<Empty
|
|
@@ -366,7 +391,8 @@ export const fileUploadMeta = {
|
|
|
366
391
|
sizes: {
|
|
367
392
|
|
|
368
393
|
},
|
|
369
|
-
|
|
394
|
+
// 'active' 移除 — dropzone 僅 hover / drag-over 態,無按壓視覺(2026-07-07 詞彙統一 DS-wide 按壓訊號盤點:檔內 0 active: utility / 0 *-active token)。
|
|
395
|
+
states: ['default', 'hover', 'focus-visible', 'disabled'],
|
|
370
396
|
tokens: {
|
|
371
397
|
bg: ['bg-surface', 'bg-disabled'],
|
|
372
398
|
fg: [],
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
// 2026-07-06 D3 bundle fix:canRenderImage 從 image-renderer.tsx 拆出成獨立小 module。
|
|
2
|
+
// 理由:file-viewer.tsx 需要「同步」呼叫 canRenderImage(renderer registry canRender 判斷
|
|
3
|
+
// + Filmstrip thumb 分流),但 image-renderer.tsx 頂層 import react-zoom-pan-pinch(~12KB gz)——
|
|
4
|
+
// 靜態 import 會把整個 lib 拉進 consumer 首屏 bundle,即使 viewer 從未打開。
|
|
5
|
+
// 拆出後 file-viewer.tsx 靜態 import 本檔(純 util,零依賴),ImageRenderer 本體走 React.lazy
|
|
6
|
+
// 動態 import,react-zoom-pan-pinch 延後到 viewer 首次渲染 image 時才載入。
|
|
7
|
+
|
|
8
|
+
const IMAGE_EXTS = new Set(['jpg', 'jpeg', 'png', 'gif', 'webp', 'svg', 'avif', 'bmp', 'ico'])
|
|
9
|
+
|
|
10
|
+
/** 判斷檔案是否可用 ImageRenderer 渲染。 */
|
|
11
|
+
export function canRenderImage(file: { mimeType: string; name: string }): boolean {
|
|
12
|
+
if (file.mimeType.startsWith('image/')) return true
|
|
13
|
+
const ext = file.name.split('.').pop()?.toLowerCase()
|
|
14
|
+
return ext ? IMAGE_EXTS.has(ext) : false
|
|
15
|
+
}
|