@qijenchen/design-system 0.1.0-beta.145 → 0.1.0-beta.147
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/components/Accordion/accordion.d.ts +1 -1
- package/dist/components/Accordion/accordion.d.ts.map +1 -1
- package/dist/components/Accordion/accordion.js +8 -3
- package/dist/components/Accordion/accordion.js.map +1 -1
- package/dist/components/AccountMenu/account-menu.d.ts.map +1 -1
- package/dist/components/AccountMenu/account-menu.js.map +1 -1
- package/dist/components/AgentPanel/agent-panel-fab.d.ts +7 -1
- package/dist/components/AgentPanel/agent-panel-fab.d.ts.map +1 -1
- package/dist/components/AgentPanel/agent-panel-fab.js +36 -14
- package/dist/components/AgentPanel/agent-panel-fab.js.map +1 -1
- package/dist/components/AgentPanel/agent-panel-logo.d.ts +3 -3
- package/dist/components/AgentPanel/agent-panel-logo.js.map +1 -1
- package/dist/components/AgentPanel/agent-panel.d.ts +1 -1
- package/dist/components/AgentPanel/agent-panel.d.ts.map +1 -1
- package/dist/components/AgentPanel/agent-panel.js +28 -22
- package/dist/components/AgentPanel/agent-panel.js.map +1 -1
- package/dist/components/AppShell/app-shell.d.ts +1 -1
- package/dist/components/AppShell/app-shell.js +1 -1
- package/dist/components/AppShell/app-shell.js.map +1 -1
- package/dist/components/Avatar/avatar.d.ts +95 -1
- package/dist/components/Avatar/avatar.d.ts.map +1 -1
- package/dist/components/Avatar/avatar.js +106 -15
- package/dist/components/Avatar/avatar.js.map +1 -1
- package/dist/components/Avatar/index.js +14 -2
- package/dist/components/Breadcrumb/breadcrumb.d.ts +4 -2
- package/dist/components/Breadcrumb/breadcrumb.d.ts.map +1 -1
- package/dist/components/Breadcrumb/breadcrumb.js +2 -2
- package/dist/components/Breadcrumb/breadcrumb.js.map +1 -1
- package/dist/components/BulkActionBar/bulk-action-bar.d.ts.map +1 -1
- package/dist/components/BulkActionBar/bulk-action-bar.js +27 -2
- package/dist/components/BulkActionBar/bulk-action-bar.js.map +1 -1
- package/dist/components/Button/button.d.ts +2 -2
- package/dist/components/Button/button.d.ts.map +1 -1
- package/dist/components/Button/button.js +118 -22
- package/dist/components/Button/button.js.map +1 -1
- package/dist/components/Calendar/calendar.d.ts +34 -10
- package/dist/components/Calendar/calendar.d.ts.map +1 -1
- package/dist/components/Calendar/calendar.js +210 -36
- package/dist/components/Calendar/calendar.js.map +1 -1
- package/dist/components/Carousel/carousel.d.ts +1 -1
- package/dist/components/Carousel/carousel.d.ts.map +1 -1
- package/dist/components/Carousel/carousel.js +3 -2
- package/dist/components/Carousel/carousel.js.map +1 -1
- package/dist/components/Chart/chart.d.ts +1 -0
- package/dist/components/Chart/chart.d.ts.map +1 -1
- package/dist/components/Chart/chart.js +8 -0
- package/dist/components/Chart/chart.js.map +1 -1
- package/dist/components/Checkbox/checkbox.d.ts +1 -1
- package/dist/components/Checkbox/checkbox.d.ts.map +1 -1
- package/dist/components/Checkbox/checkbox.js +10 -2
- package/dist/components/Checkbox/checkbox.js.map +1 -1
- package/dist/components/Chip/chip.d.ts +1 -1
- package/dist/components/Chip/chip.d.ts.map +1 -1
- package/dist/components/Chip/chip.js +4 -2
- package/dist/components/Chip/chip.js.map +1 -1
- package/dist/components/Combobox/combobox.d.ts +1 -1
- package/dist/components/Combobox/combobox.d.ts.map +1 -1
- package/dist/components/Combobox/combobox.js +37 -11
- package/dist/components/Combobox/combobox.js.map +1 -1
- package/dist/components/Command/command.d.ts +1 -1
- package/dist/components/Command/command.d.ts.map +1 -1
- package/dist/components/Command/command.js +106 -8
- package/dist/components/Command/command.js.map +1 -1
- package/dist/components/DataTable/cell-registry.d.ts.map +1 -1
- package/dist/components/DataTable/cell-registry.js +3 -2
- package/dist/components/DataTable/cell-registry.js.map +1 -1
- package/dist/components/DataTable/data-table-column-visibility-panel.d.ts.map +1 -1
- package/dist/components/DataTable/data-table-column-visibility-panel.js +24 -24
- package/dist/components/DataTable/data-table-column-visibility-panel.js.map +1 -1
- package/dist/components/DataTable/data-table-interaction-layer.d.ts +2 -1
- package/dist/components/DataTable/data-table-interaction-layer.d.ts.map +1 -1
- package/dist/components/DataTable/data-table-interaction-layer.js +3 -4
- package/dist/components/DataTable/data-table-interaction-layer.js.map +1 -1
- package/dist/components/DataTable/data-table-sort-manager.d.ts.map +1 -1
- package/dist/components/DataTable/data-table-sort-manager.js +20 -20
- package/dist/components/DataTable/data-table-sort-manager.js.map +1 -1
- package/dist/components/DataTable/data-table.d.ts +8 -5
- package/dist/components/DataTable/data-table.d.ts.map +1 -1
- package/dist/components/DataTable/data-table.js +197 -71
- package/dist/components/DataTable/data-table.js.map +1 -1
- package/dist/components/DateGrid/date-grid.d.ts +1 -1
- package/dist/components/DateGrid/date-grid.d.ts.map +1 -1
- package/dist/components/DateGrid/date-grid.js +358 -225
- package/dist/components/DateGrid/date-grid.js.map +1 -1
- package/dist/components/DatePicker/date-picker.d.ts.map +1 -1
- package/dist/components/DatePicker/date-picker.js +16 -1
- package/dist/components/DatePicker/date-picker.js.map +1 -1
- package/dist/components/Dialog/dialog.d.ts.map +1 -1
- package/dist/components/Dialog/dialog.js +55 -43
- package/dist/components/Dialog/dialog.js.map +1 -1
- package/dist/components/DropdownMenu/dropdown-menu-keyboard.d.ts +86 -0
- package/dist/components/DropdownMenu/dropdown-menu-keyboard.d.ts.map +1 -0
- package/dist/components/DropdownMenu/dropdown-menu-keyboard.js +172 -0
- package/dist/components/DropdownMenu/dropdown-menu-keyboard.js.map +1 -0
- package/dist/components/DropdownMenu/dropdown-menu.d.ts +6 -1
- package/dist/components/DropdownMenu/dropdown-menu.d.ts.map +1 -1
- package/dist/components/DropdownMenu/dropdown-menu.js +50 -39
- package/dist/components/DropdownMenu/dropdown-menu.js.map +1 -1
- package/dist/components/Field/field-context.d.ts +4 -0
- package/dist/components/Field/field-context.d.ts.map +1 -1
- package/dist/components/Field/field-context.js +1 -2
- package/dist/components/Field/field-context.js.map +1 -1
- package/dist/components/Field/field-edit-keys.d.ts +8 -0
- package/dist/components/Field/field-edit-keys.d.ts.map +1 -1
- package/dist/components/Field/field-edit-keys.js +3 -0
- package/dist/components/Field/field-edit-keys.js.map +1 -1
- package/dist/components/Field/field-wrapper.d.ts +18 -1
- package/dist/components/Field/field-wrapper.d.ts.map +1 -1
- package/dist/components/Field/field-wrapper.js +39 -10
- package/dist/components/Field/field-wrapper.js.map +1 -1
- package/dist/components/Field/field.d.ts +3 -2
- package/dist/components/Field/field.d.ts.map +1 -1
- package/dist/components/Field/field.js +2 -1
- package/dist/components/Field/field.js.map +1 -1
- package/dist/components/FileItem/file-item.d.ts +26 -2
- package/dist/components/FileItem/file-item.d.ts.map +1 -1
- package/dist/components/FileItem/file-item.js +82 -25
- package/dist/components/FileItem/file-item.js.map +1 -1
- package/dist/components/FileItem/index.js +4 -2
- package/dist/components/FileUpload/file-upload.d.ts +1 -1
- package/dist/components/FileUpload/file-upload.d.ts.map +1 -1
- package/dist/components/FileUpload/file-upload.js +132 -55
- package/dist/components/FileUpload/file-upload.js.map +1 -1
- package/dist/components/FileViewer/file-viewer.d.ts +1 -1
- package/dist/components/FileViewer/file-viewer.d.ts.map +1 -1
- package/dist/components/FileViewer/file-viewer.js +9 -4
- package/dist/components/FileViewer/file-viewer.js.map +1 -1
- package/dist/components/InlineEdit/inline-edit.d.ts.map +1 -1
- package/dist/components/InlineEdit/inline-edit.js +1 -1
- package/dist/components/InlineEdit/inline-edit.js.map +1 -1
- package/dist/components/Input/input.d.ts.map +1 -1
- package/dist/components/Input/input.js +6 -1
- package/dist/components/Input/input.js.map +1 -1
- package/dist/components/LinkInput/link-input.d.ts.map +1 -1
- package/dist/components/LinkInput/link-input.js +20 -4
- package/dist/components/LinkInput/link-input.js.map +1 -1
- package/dist/components/Notice/notice.js +1 -1
- package/dist/components/Notice/notice.js.map +1 -1
- package/dist/components/NumberInput/number-input.d.ts.map +1 -1
- package/dist/components/NumberInput/number-input.js +6 -1
- package/dist/components/NumberInput/number-input.js.map +1 -1
- package/dist/components/OverflowIndicator/index.js +2 -1
- package/dist/components/OverflowIndicator/overflow-indicator.d.ts +19 -1
- package/dist/components/OverflowIndicator/overflow-indicator.d.ts.map +1 -1
- package/dist/components/OverflowIndicator/overflow-indicator.js +47 -27
- package/dist/components/OverflowIndicator/overflow-indicator.js.map +1 -1
- package/dist/components/Pagination/pagination.d.ts +1 -1
- package/dist/components/Pagination/pagination.js +1 -1
- package/dist/components/Pagination/pagination.js.map +1 -1
- package/dist/components/PeoplePicker/avatar-stack-overflow.d.ts +7 -6
- package/dist/components/PeoplePicker/avatar-stack-overflow.d.ts.map +1 -1
- package/dist/components/PeoplePicker/avatar-stack-overflow.js +6 -7
- package/dist/components/PeoplePicker/avatar-stack-overflow.js.map +1 -1
- package/dist/components/PeoplePicker/people-picker-helpers.d.ts.map +1 -1
- package/dist/components/PeoplePicker/people-picker-helpers.js +1 -1
- package/dist/components/PeoplePicker/people-picker-helpers.js.map +1 -1
- package/dist/components/PeoplePicker/people-picker.d.ts.map +1 -1
- package/dist/components/PeoplePicker/people-picker.js +2 -2
- package/dist/components/PeoplePicker/people-picker.js.map +1 -1
- package/dist/components/PeoplePicker/person-display.d.ts.map +1 -1
- package/dist/components/PeoplePicker/person-display.js +26 -28
- package/dist/components/PeoplePicker/person-display.js.map +1 -1
- package/dist/components/ProfileCard/profile-card.d.ts.map +1 -1
- package/dist/components/ProfileCard/profile-card.js.map +1 -1
- package/dist/components/RadioGroup/radio-group.d.ts +1 -1
- package/dist/components/RadioGroup/radio-group.d.ts.map +1 -1
- package/dist/components/RadioGroup/radio-group.js +10 -4
- package/dist/components/RadioGroup/radio-group.js.map +1 -1
- package/dist/components/Rating/rating.d.ts +28 -6
- package/dist/components/Rating/rating.d.ts.map +1 -1
- package/dist/components/Rating/rating.js +97 -80
- package/dist/components/Rating/rating.js.map +1 -1
- package/dist/components/ScrollArea/scroll-area.d.ts +1 -1
- package/dist/components/ScrollArea/scroll-area.js +1 -1
- package/dist/components/ScrollArea/scroll-area.js.map +1 -1
- package/dist/components/SegmentedControl/segmented-control.d.ts +1 -1
- package/dist/components/SegmentedControl/segmented-control.d.ts.map +1 -1
- package/dist/components/SegmentedControl/segmented-control.js +3 -2
- package/dist/components/SegmentedControl/segmented-control.js.map +1 -1
- package/dist/components/Select/select.d.ts.map +1 -1
- package/dist/components/Select/select.js +15 -8
- package/dist/components/Select/select.js.map +1 -1
- package/dist/components/SelectMenu/index.js +2 -1
- package/dist/components/SelectMenu/index.js.map +1 -1
- package/dist/components/SelectMenu/select-menu-keyboard.d.ts +59 -0
- package/dist/components/SelectMenu/select-menu-keyboard.d.ts.map +1 -0
- package/dist/components/SelectMenu/select-menu-keyboard.js +107 -0
- package/dist/components/SelectMenu/select-menu-keyboard.js.map +1 -0
- package/dist/components/SelectMenu/select-menu.d.ts +1 -22
- package/dist/components/SelectMenu/select-menu.d.ts.map +1 -1
- package/dist/components/SelectMenu/select-menu.js +18 -34
- package/dist/components/SelectMenu/select-menu.js.map +1 -1
- package/dist/components/SelectionControl/selection-item.d.ts.map +1 -1
- package/dist/components/SelectionControl/selection-item.js +1 -0
- package/dist/components/SelectionControl/selection-item.js.map +1 -1
- package/dist/components/Sidebar/sidebar.d.ts +7 -9
- package/dist/components/Sidebar/sidebar.d.ts.map +1 -1
- package/dist/components/Sidebar/sidebar.js +171 -50
- package/dist/components/Sidebar/sidebar.js.map +1 -1
- package/dist/components/Slider/slider.d.ts +1 -1
- package/dist/components/Slider/slider.d.ts.map +1 -1
- package/dist/components/Slider/slider.js +9 -4
- package/dist/components/Slider/slider.js.map +1 -1
- package/dist/components/Steps/steps.d.ts.map +1 -1
- package/dist/components/Steps/steps.js +54 -26
- package/dist/components/Steps/steps.js.map +1 -1
- package/dist/components/Switch/switch.d.ts +1 -1
- package/dist/components/Switch/switch.d.ts.map +1 -1
- package/dist/components/Switch/switch.js +7 -2
- package/dist/components/Switch/switch.js.map +1 -1
- package/dist/components/Tabs/tabs.d.ts +1 -1
- package/dist/components/Tabs/tabs.d.ts.map +1 -1
- package/dist/components/Tabs/tabs.js +39 -4
- package/dist/components/Tabs/tabs.js.map +1 -1
- package/dist/components/Tag/tag.d.ts +1 -1
- package/dist/components/Tag/tag.d.ts.map +1 -1
- package/dist/components/Tag/tag.js +3 -2
- package/dist/components/Tag/tag.js.map +1 -1
- package/dist/components/Textarea/textarea.d.ts.map +1 -1
- package/dist/components/Textarea/textarea.js +8 -2
- package/dist/components/Textarea/textarea.js.map +1 -1
- package/dist/components/TimePicker/time-columns.d.ts.map +1 -1
- package/dist/components/TimePicker/time-columns.js +8 -4
- package/dist/components/TimePicker/time-columns.js.map +1 -1
- package/dist/components/TimePicker/time-picker.d.ts +1 -1
- package/dist/components/TimePicker/time-picker.js +1 -1
- package/dist/components/TimePicker/time-picker.js.map +1 -1
- package/dist/components/TreeView/tree-view.d.ts +9 -5
- package/dist/components/TreeView/tree-view.d.ts.map +1 -1
- package/dist/components/TreeView/tree-view.js +266 -214
- package/dist/components/TreeView/tree-view.js.map +1 -1
- package/dist/hooks/use-input-modality.d.ts +12 -17
- package/dist/hooks/use-input-modality.d.ts.map +1 -1
- package/dist/hooks/use-input-modality.js +4 -34
- package/dist/hooks/use-input-modality.js.map +1 -1
- package/dist/hooks/use-overflow-items.d.ts.map +1 -1
- package/dist/hooks/use-overflow-items.js +2 -0
- package/dist/hooks/use-overflow-items.js.map +1 -1
- package/dist/hooks/use-truncated.d.ts.map +1 -1
- package/dist/hooks/use-truncated.js.map +1 -1
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +23 -3
- package/dist/index.js.map +1 -1
- package/dist/lib/drag-announcements.d.ts +32 -0
- package/dist/lib/drag-announcements.d.ts.map +1 -1
- package/dist/lib/drag-announcements.js +39 -1
- package/dist/lib/drag-announcements.js.map +1 -1
- package/dist/lib/focus-after-trigger.d.ts +45 -0
- package/dist/lib/focus-after-trigger.d.ts.map +1 -0
- package/dist/lib/focus-after-trigger.js +66 -0
- package/dist/lib/focus-after-trigger.js.map +1 -0
- package/dist/lib/overlay-coexistence.js +1 -1
- package/dist/lib/roving-list-keyboard.d.ts +208 -0
- package/dist/lib/roving-list-keyboard.d.ts.map +1 -0
- package/dist/lib/roving-list-keyboard.js +174 -0
- package/dist/lib/roving-list-keyboard.js.map +1 -0
- package/dist/lib/utils.d.ts.map +1 -1
- package/dist/lib/utils.js +6 -1
- package/dist/lib/utils.js.map +1 -1
- package/dist/patterns/element-anatomy/item-anatomy.d.ts +4 -4
- package/dist/patterns/element-anatomy/item-anatomy.d.ts.map +1 -1
- package/dist/patterns/element-anatomy/item-anatomy.js +14 -4
- package/dist/patterns/element-anatomy/item-anatomy.js.map +1 -1
- package/dist/patterns/horizontal-overflow/horizontal-overflow.d.ts.map +1 -1
- package/dist/patterns/horizontal-overflow/horizontal-overflow.js +25 -0
- package/dist/patterns/horizontal-overflow/horizontal-overflow.js.map +1 -1
- package/dist/patterns/overlay-surface/overlay-surface.d.ts.map +1 -1
- package/dist/patterns/overlay-surface/overlay-surface.js.map +1 -1
- package/dist/tokens/categorical-color.d.ts +6 -1
- package/dist/tokens/categorical-color.d.ts.map +1 -1
- package/dist/tokens/categorical-color.js.map +1 -1
- package/dist/tokens/uiSize/icon-size.d.ts +1 -1
- package/dist/tokens/uiSize/stack-gap.d.ts +18 -0
- package/dist/tokens/uiSize/stack-gap.d.ts.map +1 -0
- package/dist/tokens/uiSize/stack-gap.js +5 -0
- package/dist/tokens/uiSize/stack-gap.js.map +1 -0
- package/ds-canonical/fork/codex/agents/skills/performance-audit/SKILL.md +1 -1
- package/ds-canonical/fork/codex/hooks.json +1 -1
- package/ds-canonical/fork/consumer/lock.json +17 -14
- package/ds-canonical/fork/consumer/managed-files/423729840acdd43c812f17c245b871716c6b60f76e4d3e94864d0e34a815bf8d.blob +9 -0
- package/ds-canonical/fork/consumer/managed-files/46f10bc55cdfdf834d0749eebff42ea0bb8576f361b36d2b6ba1851354c50150.blob +47 -9
- package/ds-canonical/fork/consumer/managed-files/5a0d134a2645114c8caac20458070b00a7f0fd55b3e8805b7325cb4cf52fa15e.blob +110 -0
- package/ds-canonical/fork/consumer/managed-files/5af0a46760af338d51fd0f7950675f8e97f7d2f09386b8818fcd7a58e6654e75.blob +790 -0
- package/ds-canonical/fork/consumer/managed-files/68bfcddff9c4a16388c248cd34fb66163e9e64b3d809a48ef3984f6ae4ff6d12.blob +211 -0
- package/ds-canonical/fork/consumer/managed-files/7eb6cc60abfdabaf6617c336058fad1454ccde08889295767bfd9b7fd153cc6e.blob +45 -6
- package/ds-canonical/fork/consumer/managed-files/92e4828cdcb96ff0d01daa16f6885f24045a8e72820665650f2a1890dc3a446c.blob +42 -2
- package/ds-canonical/fork/consumer/managed-files/ab072e7f2eca51f0e18a5e18c4b333260ee5d219a12cc6df5bb2d683f0b2734d.blob +27 -1
- package/ds-canonical/fork/consumer/managed-files/e71c65f45c6af7ce12b6b3c0d8028f185da5b719e2050c9b2a6b243d41ae4878.blob +62 -14
- package/ds-canonical/fork/governance.lock +41 -17
- package/ds-canonical/fork/hooks/check_consumer_app_invariants.sh +16 -4
- package/ds-canonical/fork/hooks/check_consumer_code_quality.sh +3 -1
- package/ds-canonical/fork/hooks/check_pattern_invariants.sh +13 -9
- package/ds-canonical/fork/launchers/settings-hooks.json +1 -1
- package/ds-canonical/fork/manifest.json +17 -11
- package/ds-canonical/fork/providers/claude/skills/performance-audit/SKILL.md +1 -1
- package/ds-canonical/fork/providers/codex/hook-config.json +1 -1
- package/ds-canonical/fork/providers/codex/skills/performance-audit/SKILL.md +1 -1
- package/ds-canonical/fork/skills/performance-audit/SKILL.md +1 -1
- package/ds-canonical/hooks/check_consumer_app_invariants.sh +16 -4
- package/ds-canonical/hooks/check_consumer_code_quality.sh +3 -1
- package/ds-canonical/hooks/check_datatable_invariants.sh +4 -1
- package/ds-canonical/hooks/check_pattern_invariants.sh +13 -9
- package/ds-canonical/hooks/check_plugin_fork_health.sh +8 -1
- package/ds-canonical/hooks/check_propose_discipline.sh +8 -1
- package/ds-canonical/hooks/check_storybook_addon_packaging.sh +8 -1
- package/ds-canonical/hooks/check_substantive_edit_approval_preflight.sh +64 -2
- package/ds-canonical/hooks/lib/_overlay_handcraft.sh +5 -2
- package/ds-canonical/hooks/registrations.json +12 -7
- package/ds-canonical/hooks/session_start_governance_check.sh +16 -0
- package/ds-canonical/hooks/tests/test_check_consumer_app_invariants.sh +25 -0
- package/ds-canonical/hooks/tests/test_check_consumer_code_quality.sh +4 -0
- package/ds-canonical/hooks/tests/test_check_consumer_ds_primitive_misuse.sh +14 -0
- package/ds-canonical/hooks/tests/test_check_datatable_invariants.sh +24 -0
- package/ds-canonical/hooks/tests/test_check_overlay_handcraft.sh +32 -0
- package/ds-canonical/hooks/tests/test_check_pattern_invariants.sh +59 -0
- package/ds-canonical/hooks/tests/test_check_plugin_fork_health.sh +32 -1
- package/ds-canonical/hooks/tests/test_check_propose_discipline.sh +21 -0
- package/ds-canonical/hooks/tests/test_check_storybook_addon_packaging.sh +21 -0
- package/ds-canonical/hooks/tests/test_check_substantive_edit_approval_preflight.sh +97 -0
- package/ds-canonical/hooks/tests/test_session_start_governance_check.sh +49 -0
- package/ds-canonical/references/README.md +12 -6
- package/ds-canonical/references/drag-canonical.md +3 -1
- package/ds-canonical/references/focus-canonical.md +26 -13
- package/ds-canonical/references/governance-audit-coverage.md +1 -1
- package/ds-canonical/references/hit-area-canonical.md +361 -0
- package/ds-canonical/references/item-anatomy-recipe.md +4 -2
- package/ds-canonical/references/keyboard-model-canonical.md +559 -0
- package/ds-canonical/rules/meta-patterns.md +4 -4
- package/ds-canonical/rules/self-verify.md +4 -4
- package/ds-canonical/skills/design-system-audit/references/audit-prompts.md +1 -1
- package/ds-canonical/skills/design-system-audit/references/historical-bugs.md +96 -0
- package/ds-canonical/skills/knowledge-prune/SKILL.md +4 -1
- package/ds-canonical/skills/performance-audit/SKILL.md +1 -1
- package/ds-story-manifest.json +19 -7
- package/llms-full.txt +2 -2
- package/llms.txt +2 -2
- package/package.json +3 -1
- package/src/components/Accordion/accordion.anatomy.stories.tsx +3 -3
- package/src/components/Accordion/accordion.spec.md +1 -1
- package/src/components/Accordion/accordion.tsx +8 -3
- package/src/components/AccountMenu/account-menu.spec.md +2 -3
- package/src/components/AccountMenu/account-menu.tsx +0 -1
- package/src/components/AgentPanel/agent-decision-card.spec.md +88 -0
- package/src/components/AgentPanel/agent-panel-fab.spec.md +151 -0
- package/src/components/AgentPanel/agent-panel-fab.tsx +67 -24
- package/src/components/AgentPanel/agent-panel-logo.spec.md +87 -0
- package/src/components/AgentPanel/agent-panel-logo.tsx +3 -3
- package/src/components/AgentPanel/agent-panel.anatomy.stories.tsx +1 -1
- package/src/components/AgentPanel/agent-panel.principles.stories.tsx +1 -1
- package/src/components/AgentPanel/agent-panel.spec.md +30 -275
- package/src/components/AgentPanel/agent-panel.stories.tsx +113 -8
- package/src/components/AgentPanel/agent-panel.tsx +59 -22
- package/src/components/Alert/alert.anatomy.stories.tsx +1 -1
- package/src/components/AppShell/app-shell.tsx +1 -1
- package/src/components/AspectRatio/aspect-ratio.anatomy.stories.tsx +10 -15
- package/src/components/Avatar/avatar.anatomy.stories.tsx +23 -26
- package/src/components/Avatar/avatar.principles.stories.tsx +10 -10
- package/src/components/Avatar/avatar.spec.md +45 -6
- package/src/components/Avatar/avatar.stories.tsx +8 -8
- package/src/components/Avatar/avatar.tsx +227 -21
- package/src/components/Badge/badge.anatomy.stories.tsx +16 -21
- package/src/components/Breadcrumb/breadcrumb.anatomy.stories.tsx +3 -3
- package/src/components/Breadcrumb/breadcrumb.spec.md +3 -3
- package/src/components/Breadcrumb/breadcrumb.tsx +5 -3
- package/src/components/BulkActionBar/bulk-action-bar.anatomy.stories.tsx +1 -1
- package/src/components/BulkActionBar/bulk-action-bar.principles.stories.tsx +1 -1
- package/src/components/BulkActionBar/bulk-action-bar.spec.md +1 -0
- package/src/components/BulkActionBar/bulk-action-bar.stories.tsx +57 -2
- package/src/components/BulkActionBar/bulk-action-bar.tsx +32 -2
- package/src/components/Button/button.anatomy.stories.tsx +174 -33
- package/src/components/Button/button.spec.md +23 -8
- package/src/components/Button/button.tsx +116 -21
- package/src/components/Calendar/calendar.anatomy.stories.tsx +34 -11
- package/src/components/Calendar/calendar.principles.stories.tsx +44 -1
- package/src/components/Calendar/calendar.spec.md +97 -20
- package/src/components/Calendar/calendar.stories.tsx +101 -4
- package/src/components/Calendar/calendar.tsx +379 -74
- package/src/components/Carousel/carousel.anatomy.stories.tsx +16 -17
- package/src/components/Carousel/carousel.spec.md +48 -2
- package/src/components/Carousel/carousel.tsx +9 -3
- package/src/components/Chart/chart.anatomy.stories.tsx +7 -5
- package/src/components/Chart/chart.spec.md +32 -7
- package/src/components/Chart/chart.tsx +9 -0
- package/src/components/Checkbox/checkbox.anatomy.stories.tsx +21 -28
- package/src/components/Checkbox/checkbox.spec.md +4 -2
- package/src/components/Checkbox/checkbox.tsx +10 -2
- package/src/components/Chip/chip.anatomy.stories.tsx +1 -1
- package/src/components/Chip/chip.spec.md +2 -1
- package/src/components/Chip/chip.tsx +4 -2
- package/src/components/Combobox/combobox.anatomy.stories.tsx +30 -37
- package/src/components/Combobox/combobox.spec.md +11 -3
- package/src/components/Combobox/combobox.tsx +43 -12
- package/src/components/Command/command.spec.md +11 -0
- package/src/components/Command/command.tsx +143 -10
- package/src/components/DataTable/cell-registry.tsx +5 -1
- package/src/components/DataTable/data-table-column-visibility-panel.tsx +5 -6
- package/src/components/DataTable/data-table-interaction-layer.tsx +27 -14
- package/src/components/DataTable/data-table-known-defects.spec.md +72 -0
- package/src/components/DataTable/data-table-scroll-performance.spec.md +295 -0
- package/src/components/DataTable/data-table-sort-manager.tsx +5 -6
- package/src/components/DataTable/data-table.anatomy.stories.tsx +1 -1
- package/src/components/DataTable/data-table.css +64 -0
- package/src/components/DataTable/data-table.spec.md +70 -346
- package/src/components/DataTable/data-table.stories.tsx +56 -9
- package/src/components/DataTable/data-table.tsx +392 -103
- package/src/components/DateGrid/date-grid.anatomy.stories.tsx +2 -2
- package/src/components/DateGrid/date-grid.spec.md +163 -1
- package/src/components/DateGrid/date-grid.tsx +270 -7
- package/src/components/DatePicker/date-picker.anatomy.stories.tsx +18 -27
- package/src/components/DatePicker/date-picker.spec.md +6 -2
- package/src/components/DatePicker/date-picker.tsx +20 -1
- package/src/components/Dialog/dialog.spec.md +6 -2
- package/src/components/Dialog/dialog.stories.tsx +18 -9
- package/src/components/Dialog/dialog.tsx +83 -48
- package/src/components/DropdownMenu/dropdown-menu-keyboard.ts +255 -0
- package/src/components/DropdownMenu/dropdown-menu.anatomy.stories.tsx +32 -33
- package/src/components/DropdownMenu/dropdown-menu.spec.md +18 -5
- package/src/components/DropdownMenu/dropdown-menu.stories.tsx +58 -1
- package/src/components/DropdownMenu/dropdown-menu.tsx +70 -33
- package/src/components/Empty/empty.spec.md +1 -2
- package/src/components/Field/field-context.ts +9 -4
- package/src/components/Field/field-controls.spec.md +5 -2
- package/src/components/Field/field-edit-keys.ts +12 -0
- package/src/components/Field/field-wrapper.tsx +76 -10
- package/src/components/Field/field.anatomy.stories.tsx +1 -1
- package/src/components/Field/field.spec.md +11 -6
- package/src/components/Field/field.stories.tsx +24 -1
- package/src/components/Field/field.tsx +10 -4
- package/src/components/FileItem/file-item.anatomy.stories.tsx +20 -10
- package/src/components/FileItem/file-item.principles.stories.tsx +14 -7
- package/src/components/FileItem/file-item.spec.md +50 -48
- package/src/components/FileItem/file-item.stories.tsx +41 -28
- package/src/components/FileItem/file-item.tsx +184 -50
- package/src/components/FileUpload/file-upload.anatomy.stories.tsx +11 -15
- package/src/components/FileUpload/file-upload.spec.md +27 -1
- package/src/components/FileUpload/file-upload.stories.tsx +33 -0
- package/src/components/FileUpload/file-upload.tsx +173 -43
- package/src/components/FileViewer/file-viewer.tsx +9 -4
- package/src/components/HoverCard/hover-card.stories.tsx +2 -2
- package/src/components/InlineEdit/inline-edit.spec.md +2 -0
- package/src/components/InlineEdit/inline-edit.tsx +8 -3
- package/src/components/Input/input.anatomy.stories.tsx +23 -31
- package/src/components/Input/input.spec.md +11 -0
- package/src/components/Input/input.tsx +6 -1
- package/src/components/LinkInput/link-input.anatomy.stories.tsx +18 -21
- package/src/components/LinkInput/link-input.principles.stories.tsx +4 -4
- package/src/components/LinkInput/link-input.spec.md +12 -1
- package/src/components/LinkInput/link-input.tsx +37 -4
- package/src/components/Menu/menu-item.anatomy.stories.tsx +50 -56
- package/src/components/Menu/menu-item.spec.md +1 -1
- package/src/components/Menu/menu-item.stories.tsx +48 -42
- package/src/components/Notice/notice.anatomy.stories.tsx +8 -16
- package/src/components/Notice/notice.spec.md +1 -1
- package/src/components/Notice/notice.tsx +1 -1
- package/src/components/NumberInput/number-input.anatomy.stories.tsx +21 -28
- package/src/components/NumberInput/number-input.spec.md +2 -0
- package/src/components/NumberInput/number-input.tsx +8 -1
- package/src/components/OverflowIndicator/overflow-indicator.anatomy.stories.tsx +28 -48
- package/src/components/OverflowIndicator/overflow-indicator.spec.md +13 -1
- package/src/components/OverflowIndicator/overflow-indicator.stories.tsx +12 -9
- package/src/components/OverflowIndicator/overflow-indicator.tsx +32 -12
- package/src/components/Pagination/pagination.spec.md +1 -0
- package/src/components/Pagination/pagination.tsx +1 -1
- package/src/components/PeoplePicker/avatar-stack-overflow.ts +14 -13
- package/src/components/PeoplePicker/people-picker-helpers.ts +5 -2
- package/src/components/PeoplePicker/people-picker.anatomy.stories.tsx +1 -1
- package/src/components/PeoplePicker/people-picker.spec.md +4 -2
- package/src/components/PeoplePicker/people-picker.tsx +1 -4
- package/src/components/PeoplePicker/person-display.tsx +45 -28
- package/src/components/Popover/popover.anatomy.stories.tsx +1 -1
- package/src/components/Popover/popover.principles.stories.tsx +3 -12
- package/src/components/Popover/popover.spec.md +7 -3
- package/src/components/ProfileCard/profile-card.anatomy.stories.tsx +1 -1
- package/src/components/ProfileCard/profile-card.spec.md +1 -1
- package/src/components/ProfileCard/profile-card.tsx +0 -3
- package/src/components/ProgressBar/progress-bar.anatomy.stories.tsx +11 -21
- package/src/components/ProgressBar/progress-bar.spec.md +1 -1
- package/src/components/RadioGroup/radio-group.anatomy.stories.tsx +17 -24
- package/src/components/RadioGroup/radio-group.spec.md +1 -1
- package/src/components/RadioGroup/radio-group.tsx +22 -4
- package/src/components/Rating/rating.anatomy.stories.tsx +131 -124
- package/src/components/Rating/rating.principles.stories.tsx +69 -43
- package/src/components/Rating/rating.spec.md +142 -63
- package/src/components/Rating/rating.stories.tsx +11 -15
- package/src/components/Rating/rating.tsx +131 -117
- package/src/components/ScrollArea/scroll-area.anatomy.stories.tsx +1 -1
- package/src/components/ScrollArea/scroll-area.principles.stories.tsx +6 -3
- package/src/components/ScrollArea/scroll-area.spec.md +3 -1
- package/src/components/ScrollArea/scroll-area.stories.tsx +5 -2
- package/src/components/ScrollArea/scroll-area.tsx +1 -1
- package/src/components/SegmentedControl/segmented-control.anatomy.stories.tsx +1 -1
- package/src/components/SegmentedControl/segmented-control.spec.md +2 -2
- package/src/components/SegmentedControl/segmented-control.tsx +3 -2
- package/src/components/Select/select.anatomy.stories.tsx +26 -35
- package/src/components/Select/select.spec.md +4 -3
- package/src/components/Select/select.tsx +11 -4
- package/src/components/SelectMenu/select-menu-keyboard.ts +218 -0
- package/src/components/SelectMenu/select-menu.anatomy.stories.tsx +6 -17
- package/src/components/SelectMenu/select-menu.spec.md +12 -4
- package/src/components/SelectMenu/select-menu.tsx +21 -56
- package/src/components/SelectionControl/selection-item.anatomy.stories.tsx +9 -33
- package/src/components/SelectionControl/selection-item.spec.md +2 -2
- package/src/components/SelectionControl/selection-item.tsx +3 -1
- package/src/components/Separator/separator.stories.tsx +4 -4
- package/src/components/Sheet/sheet.principles.stories.tsx +31 -13
- package/src/components/Sidebar/sidebar.anatomy.stories.tsx +1 -1
- package/src/components/Sidebar/sidebar.spec.md +73 -6
- package/src/components/Sidebar/sidebar.stories.tsx +82 -4
- package/src/components/Sidebar/sidebar.tsx +281 -64
- package/src/components/Skeleton/skeleton.spec.md +1 -1
- package/src/components/Slider/slider.anatomy.stories.tsx +1 -1
- package/src/components/Slider/slider.spec.md +6 -5
- package/src/components/Slider/slider.tsx +13 -8
- package/src/components/Steps/steps-state-visuals.spec.md +157 -0
- package/src/components/Steps/steps.anatomy.stories.tsx +1 -0
- package/src/components/Steps/steps.spec.md +59 -142
- package/src/components/Steps/steps.stories.tsx +1 -1
- package/src/components/Steps/steps.tsx +125 -33
- package/src/components/Switch/switch.spec.md +3 -3
- package/src/components/Switch/switch.tsx +6 -1
- package/src/components/Tabs/tabs.spec.md +5 -1
- package/src/components/Tabs/tabs.tsx +42 -3
- package/src/components/Tag/tag.anatomy.stories.tsx +24 -28
- package/src/components/Tag/tag.spec.md +10 -11
- package/src/components/Tag/tag.tsx +3 -3
- package/src/components/Textarea/textarea.tsx +7 -1
- package/src/components/TimePicker/time-columns.tsx +9 -4
- package/src/components/TimePicker/time-picker.spec.md +4 -1
- package/src/components/TimePicker/time-picker.tsx +1 -1
- package/src/components/Tooltip/tooltip.anatomy.stories.tsx +6 -15
- package/src/components/Tooltip/tooltip.principles.stories.tsx +4 -16
- package/src/components/Tooltip/tooltip.spec.md +2 -1
- package/src/components/TreeView/tree-view.anatomy.stories.tsx +35 -13
- package/src/components/TreeView/tree-view.spec.md +67 -38
- package/src/components/TreeView/tree-view.stories.tsx +221 -12
- package/src/components/TreeView/tree-view.tsx +463 -356
- package/src/hooks/use-input-modality.ts +17 -46
- package/src/hooks/use-overflow-items.ts +4 -0
- package/src/hooks/use-truncated.ts +3 -2
- package/src/index.ts +17 -0
- package/src/lib/README.md +2 -0
- package/src/lib/drag-announcements.ts +78 -0
- package/src/lib/focus-after-trigger.ts +102 -0
- package/src/lib/roving-list-keyboard.ts +400 -0
- package/src/lib/utils.ts +5 -0
- package/src/patterns/action-bar/action-bar.spec.md +2 -0
- package/src/patterns/element-anatomy/inline-action.spec.md +16 -12
- package/src/patterns/element-anatomy/item-anatomy.spec.md +98 -6
- package/src/patterns/element-anatomy/item-anatomy.stories.tsx +75 -70
- package/src/patterns/element-anatomy/item-anatomy.tsx +17 -6
- package/src/patterns/element-anatomy/truncated-text.spec.md +1 -0
- package/src/patterns/header-canonical/header-canonical.spec.md +3 -2
- package/src/patterns/horizontal-overflow/horizontal-overflow.spec.md +2 -0
- package/src/patterns/horizontal-overflow/horizontal-overflow.stories.tsx +36 -0
- package/src/patterns/horizontal-overflow/horizontal-overflow.tsx +28 -0
- package/src/patterns/overlay-surface/overlay-chrome-sizing.spec.md +180 -0
- package/src/patterns/overlay-surface/overlay-surface.spec.md +8 -162
- package/src/patterns/overlay-surface/overlay-surface.tsx +5 -3
- package/src/patterns/resize-handle/resize-handle.spec.md +3 -3
- package/src/styles/base.css +1 -1
- package/src/tokens/categorical-color.ts +6 -1
- package/src/tokens/color/color.spec.md +91 -24
- package/src/tokens/color/color.stories.tsx +50 -2
- package/src/tokens/color/primitives.css +41 -38
- package/src/tokens/color/semantic.css +105 -36
- package/src/tokens/layoutSpace/layoutSpace.spec.md +2 -0
- package/src/tokens/motion/motion.spec.md +31 -8
- package/src/tokens/orphan-tokens.spec.md +1 -1
- package/src/tokens/uiSize/icon-size.ts +1 -1
- package/src/tokens/uiSize/stack-gap.ts +17 -0
- package/src/tokens/uiSize/uiSize.css +12 -0
- package/src/tokens/uiSize/uiSize.spec.md +17 -16
- package/src/tokens/uiSize/uiSize.stories.tsx +1 -1
- package/dist/node_modules/aria-hidden/dist/es2015/index.js +0 -147
- package/dist/node_modules/aria-hidden/dist/es2015/index.js.map +0 -1
|
@@ -75,7 +75,7 @@ DataTable 有三種尺寸(`sm`、`md`、`lg`),透過 `size` prop 控制。
|
|
|
75
75
|
|
|
76
76
|
### 二、高度模式(有高度約束 vs 無高度約束,決定可用的功能組合)
|
|
77
77
|
|
|
78
|
-
- **有高度約束**(固定 `height="400px"` 等具體 px/rem,或 `height="100%"` 由父 flex 提供約束,Linear 做法——都走相同 cap 行為,無 dead surface):資料少→outer 內容高度;資料多→撐到上限後內部 scroll(虛擬捲動啟用,header 固定)
|
|
78
|
+
- **有高度約束**(固定 `height="400px"` 等具體 px/rem,或 `height="100%"` 由父 flex 提供約束,Linear 做法——都走相同 cap 行為,無 dead surface):資料少→outer 內容高度;資料多→撐到上限後內部 scroll(虛擬捲動啟用,header 固定)。**填滿高度量的是 outer 真正拿到的格子**(2026-09-29,待辦總帳 OE3):父層**內容盒**(扣 padding / border)減掉同一個流向裡的剛性兄弟(工具列、分頁列;`flex-grow > 0` 的兄弟不扣)與 gap;表頭與每個兄弟都在觀察名單上(表頭變高、分頁列後出現都會重算)。同一個父層放兩張填滿高度的表格不支援(各給自己的格子)。閘 `scripts/data-table-invariants.mjs` I37。
|
|
79
79
|
- **無高度約束**(auto):內容決定高度,table 框只包住內容;適合少量資料、預覽、嵌入式表格。犧牲:無虛擬捲動(全部渲染)、header 隨頁面捲走、水平捲軸在 table 最底部——這些不是 bug,是模式的取捨
|
|
80
80
|
|
|
81
81
|
### 三、三區域架構(AG Grid 模式)
|
|
@@ -130,6 +130,42 @@ Table 層級的模式切換,不是 column 層級。跟 AG Grid / Airtable 的
|
|
|
130
130
|
|
|
131
131
|
一般 column 只在 header 有短線——body 的欄位邊界由 header 引導,不需額外視覺噪音。但 frozen column 的邊界是結構性的分隔(固定區域 vs 捲動區域),需要全高度的線來明確標示。Row actions 欄本質上是 frozen right column,左邊界也使用 full-height 分隔線。
|
|
132
132
|
|
|
133
|
+
#### 三種垂直分隔線的**畫法歸屬**,以及選取欄為何會漏掉(2026-09-24 root cause)
|
|
134
|
+
|
|
135
|
+
上表原本只列兩種。**實際有三種**,而且三種各由**不同元件**負責畫 —— 這就是選取欄漏線的根因:
|
|
136
|
+
|
|
137
|
+
| 種類 | 幾何 | 誰畫的 | 什麼情況下不會被畫 |
|
|
138
|
+
|---|---|---|---|
|
|
139
|
+
| **表頭欄間短線** | 一個行高(`1lh`),垂直置中 | 畫在 **`ResizeHandle` 那個區塊**裡(`showLine` + `lineInsetStart/End="var(--table-cell-py)"`)。進入條件是 `if (!showDivider && !isResizable) return null` —— **只要該畫線就會渲染,與可不可調寬無關** | **選取欄有自己的 early-return 分支,到不了這一段**—— 它是 `headerCellEl` 裡**唯一**這樣的欄。拖拉欄與列動作欄走一般分支,所以有線 |
|
|
140
|
+
| **凍結邊界線** | 整欄高度,貫穿水平捲軸帶 | `dtPanelBoundaryLeft/Right` 畫在面板上 | 非面板邊界的欄 |
|
|
141
|
+
| **列身欄間線**(僅 inline edit / spreadsheet 模式)| 整格高度 | cell 自己的 `.dtCellGrid` | 選取欄的 render 分支在套上這個 class **之前就 early-return** |
|
|
142
|
+
|
|
143
|
+
⛔ **這一段的第一版寫錯過,同日更正**:原本寫「系統欄不可調寬 → 不渲染 `ResizeHandle` → 永遠沒線」。**不可調寬不是原因** —— 實測 `with-bulk-actions` 那則故事 `enableColumnResize` 預設 `false`、全部欄位都不可調寬,卻有 5 條表頭線。**兩件事同時成立 ≠ 前者導致後者**;真正的因是 early-return。
|
|
144
|
+
|
|
145
|
+
**Root cause 一句話:規範用「這是哪一種邊界」定義線,程式碼卻用「這裡剛好渲染了哪個元件」畫線。**
|
|
146
|
+
兩者在多數欄位上碰巧一致,所以看起來沒事;**一旦某一欄不渲染那個元件,線就靜默消失,而且沒有任何訊號**。
|
|
147
|
+
這是 M37「用一個當時剛好成立的觀察量代替要保證的性質」的標準形狀:
|
|
148
|
+
要保證的是「這是一個欄邊界」,實際判的是「這裡有沒有 `ResizeHandle`」。
|
|
149
|
+
|
|
150
|
+
**歷史怎麼走到這一步**:選取欄原本有自己的 ad-hoc 規則
|
|
151
|
+
(`[data-column-id="__select__"]:not(:last-child)` 加 border-right),2026-05-12 退役,
|
|
152
|
+
註解寫的理由是「走 inlineEdit canonical」—— 但那句話從來沒有被驗證過:
|
|
153
|
+
`dtCellGrid` 碰不到選取欄的 render 分支,`ResizeHandle` 也不會在系統欄渲染。
|
|
154
|
+
**退役一條規則時說「改由某某接手」,卻沒有當場驗證某某真的作用得到那個對象** —— 同一族的判準見
|
|
155
|
+
`ds-canonical/skills/design-system-audit/references/historical-bugs.md`。
|
|
156
|
+
|
|
157
|
+
**2026-09-24 補線時我自己又踩了同一族三次**(全部由 user 抓到,留檔警惕):
|
|
158
|
+
1. 直接把列身的 `.dtCellGrid`(整格高)套到表頭 → 表頭出現 30px 全高線、隔壁是 21px 短線。
|
|
159
|
+
**一般非 frozen 欄的線不是整高,整高只屬於 frozen 邊界** —— 上表第一行就寫著,我沒讀就照抄隔壁。
|
|
160
|
+
2. 給表頭加 `self-stretch` → 表頭格變 39、隔壁 38。**其他表頭格是內容高 + 列的 `align-items:center` 置中**,
|
|
161
|
+
把一般欄撐滿等於把它當成 frozen 在畫。
|
|
162
|
+
3. 線的內縮寫成 `top/bottom: var(--table-cell-py)` → 得到 16px,隔壁 21px。
|
|
163
|
+
因為那個 calc 含 `1lh`,而選取欄沒帶表頭字級 class,繼承到根字級(16/24)算出 7,隔壁是 14/21 算出 8.5。
|
|
164
|
+
**正解是直接寫 `height: 1lh` + 垂直置中**,不依賴格子自己的高度與字級推導。
|
|
165
|
+
|
|
166
|
+
**判準(寫任何一條線之前)**:先答「這是哪一種邊界」,再去看**那一種**的幾何與畫法,
|
|
167
|
+
**不要看隔壁那一格怎麼寫就照抄** —— 隔壁可能是另一種邊界,或是靠某個這一欄不會渲染的元件在畫。
|
|
168
|
+
|
|
133
169
|
**Header 文字弱化。** Header 是結構標籤,不是資訊本體。字體與 body 相同但使用次要文字色,搭配 muted 背景拉出層級,讓視覺重心留在 body 的資料上。
|
|
134
170
|
|
|
135
171
|
### 六、外框規則
|
|
@@ -187,350 +223,15 @@ Table 層級的模式切換,不是 column 層級。跟 AG Grid / Airtable 的
|
|
|
187
223
|
- **同型前例**(隱藏原生捲軸 + header 尾端補等寬):**MUI X DataGrid** 的 `GridScrollbarFillerCell`(寬 = `var(--DataGrid-hasScrollY) * var(--DataGrid-scrollbarSize)`)、**Handsontable** 的 `width = getWorkspaceWidth(); if (hasVerticalScroll()) width -= getScrollbarWidth()`;**Glide Data Grid** 整張表同一塊 canvas,同理免疫。
|
|
188
224
|
- **仍存在的架構差異**(不是缺陷,是已知取捨):v33 的水平捲軸是 `.ag-root` 層的一條假捲軸、垂直捲軸是 `.ag-body` 的 in-flow 兄弟元素 → 捲軸落在整表最右緣、三區共用同一個 `.ag-body-viewport`,所以「pinned 多露一列」在它那邊結構上不可能;我們用原生捲軸 + 兩軸量測補償達到同一個不變條件。**2026-09-04 撤回**:此處原本寫「換到的是慣性捲動、平台一致的捲軸外觀與零額外 a11y 風險」—— 三條逐條查證都不成立(v33 的 `.ag-body-viewport` 本身就是原生 `overflow-y:auto` + `-webkit-overflow-scrolling:touch`;它的可見捲軸是**代理元素上的原生捲軸**,不是自繪;代理捲軸帶 `aria-hidden`),依 M22 撤回。**真正換到的**只有一件可證的事:不必自己實作代理捲軸元件 —— v33 為此付 `fakeHScrollComp.ts` 174 行 + `fakeVScrollComp.ts` 80 行 + `abstractFakeScrollComp.ts` 110 行 = 364 行,外加 `gridBodyScrollFeature.ts` 776 行的同步(水平 6 個 partner)。代價見缺陷表 O / P / Q。
|
|
189
225
|
|
|
190
|
-
### 預掛緩衝(overscan)— 隨機器能力自適應(2026-09-12)
|
|
191
|
-
|
|
192
|
-
TanStack Virtual 的 `defaultRangeExtractor` 只渲染 `[startIndex − overscan, endIndex + overscan]`
|
|
193
|
-
(`@tanstack/virtual-core/dist/esm/index.js:7-14`),範圍外的列一 commit 就卸載,
|
|
194
|
-
**沒有任何「保留舊列直到新列就緒」的機制**。緩衝原本固定 5 列 × 40px = 200px,
|
|
195
|
-
而**一次普通滾輪就是 1000px** —— 結構上差 5 倍,所以快速捲動必然露出背景。
|
|
196
|
-
|
|
197
|
-
**世界級對照**:AG Grid 的 `rowBuffer` 預設**每側 10 列**,文件逐字寫明理由 ——
|
|
198
|
-
「By default the grid will render 10 rows before the first visible row and 10 rows after the last visible
|
|
199
|
-
row… This is to act as a buffer as **on some slower machines and browsers, a blank space can be seen as
|
|
200
|
-
the user scrolls**」(https://www.ag-grid.com/javascript-data-grid/dom-virtualisation/)。
|
|
201
|
-
AG Grid 是用兩倍於我們原值的緩衝把空白壓到看不見 —— 但**壓小不等於消除**,緩衝再大都追不上任意快的手勢。
|
|
202
|
-
|
|
203
|
-
> **2026-09-12 撤回**:這裡原本寫「**連 AG Grid 都不宣稱能消除空白**」,語氣上把「業界最好的也只能壓小」
|
|
204
|
-
> 當成本 DS 的天花板。那是錯的定位 —— user 2026-09-12 原話:「你知道 main 只是低標嗎?理想上 data table
|
|
205
|
-
> 整體互動和體驗越順暢越好」、「我認為任何情境『理想』上都不應該看到空白,但也不應該為了達成此目的而讓
|
|
206
|
-
> 體驗和互動卡頓」。**目的地是零空白**,見下方「零空白不變條件」。緩衝與列殼都只是逼近手段,不是終點。
|
|
207
|
-
|
|
208
|
-
**我們的做法**:**預測**「把緩衝加上去之後,一次全量 commit 會不會變成長工」,而不是只看每列成本。
|
|
209
|
-
|
|
210
|
-
一次全量 commit 要畫 `視窗列數 + 2 × overscan` 列(緩衝在上下各一側),成本 = 列數 × `costPerRow` + 固定成本。
|
|
211
|
-
要求它留在 **Long Tasks API 的 50ms 界線**內 —— 跨過去瀏覽器就把那一段算成長工,而長工正是 user 感受到的卡頓:
|
|
212
|
-
|
|
213
|
-
```
|
|
214
|
-
overscan = clamp(⌊(50 − fixedCost − 視窗列數 × costPerRow) ÷ (2 × costPerRow)⌋, consumer overscan, 10)
|
|
215
|
-
```
|
|
216
|
-
|
|
217
|
-
我們比 AG Grid 多的就是 `costPerRow` / `fixedCost` 這兩個實測值,所以緩衝可以按機器付得起的量給。
|
|
218
|
-
|
|
219
|
-
**兩個被推翻的版本(都留著,免得再走一次)**:
|
|
220
|
-
|
|
221
|
-
1. **固定 10**(直接照抄 AG Grid):4× 節流下最長連續空白從 451ms 惡化到 **566ms**、面積 516 → 972 ——
|
|
222
|
-
慢機器每次 commit 要掛的列變多。
|
|
223
|
-
2. **事後守衛**(`commitCost > 50ms 就不擴`):**不行,因為它是反應式的** —— `commitCost` 要先被量到變長
|
|
224
|
-
才會觸發,而那幾次變長的 commit 正是它該避免的。同機 3× 節流下 `--ref=main` 判定長工 374ms >
|
|
225
|
-
main 289×1.25;把自適應整個關掉則是 345 ≤ 379 全過。預測式才不會震盪。
|
|
226
|
-
|
|
227
|
-
**實測**(6000px/s 手勢,1400×800):
|
|
228
|
-
|
|
229
|
-
| CPU 節流 | 預測餘裕 | 緩衝 | 空白幀 | 最長連續空白 |
|
|
230
|
-
|---|---|---|---|---|
|
|
231
|
-
| 1×(一般機器) | +21.8ms | **10** | 30 → **5–9** | 67ms → **17–33ms** |
|
|
232
|
-
| 2× 以上 | 負值 | **5**(下限) | 與改前相同 | 與改前相同 |
|
|
233
|
-
|
|
234
|
-
也就是說:**只有真的有餘裕的機器才擴緩衝**;慢機器行為與改前一致,由列殼機制接手。
|
|
235
|
-
|
|
236
|
-
**量測紀律**:3× 節流的長工在同一份程式碼上跑出 **220ms 與 404ms**(雜訊主導),
|
|
237
|
-
所以這條不可用本機單跑判定,要看 CI 的同窗交錯 `--ref=main` 比值。
|
|
238
|
-
|
|
239
|
-
**機械閘** `scripts/data-table-overscan-adaptive-invariant.mjs`(刻意做成**機器無關** —— 絕對門檻只是在量跑閘的那台機器):
|
|
240
|
-
A1 緩衝不低於 consumer 的 overscan / A2 不超過 AG Grid 的 10 / A3 能力越強緩衝不得更小(抓公式方向反了)/
|
|
241
|
-
A4 有餘裕時機制必須真的動(沒餘裕的機器明白標為不適用,不假裝驗過)。
|
|
242
|
-
對照組把回報值改成 0 與 99 → A1/A2 必紅。
|
|
243
|
-
|
|
244
226
|
**機械閘** = `scripts/data-table-invariants.mjs` I11 / I11b(橫軸)+ I12(縱軸)。CI 的 headless Chromium 是 overlay 捲軸(gutter = 0),只驗自然狀態等於空轉——把 padding 整段拿掉 CI 照樣綠;I11b / I12 因此各用一條 15px 透明邊框造出與真捲軸同值的量測(`clientWidth` 不含 border、`offsetWidth` 含),補償分支在任何環境都會被走到(2026-09-03 實測:註入 `::-webkit-scrollbar` 寬度**無法**讓 CI 的捲軸佔版面,此路不通)。對應 `scripts/data-table-invariants.mjs`(script 內 I1-I3 label 字串仍用 `display↔edit` — 2026-07-16 FieldMode display→view 更名前的歷史命名,語意同 view↔edit)。改 `columnSizeStyle` / 切 layout 必跑 invariant test 才 commit。
|
|
245
227
|
|
|
246
|
-
###
|
|
247
|
-
|
|
248
|
-
上一輪的缺陷清單是**結構**盤點;這一輪只盤**捲軸**,而且是逐條跟 v33.3.2 對照。
|
|
249
|
-
19 條捲軸補丁的判定:**同款 9 / 半款 1 / v33 結構上用不到 6 / 我們自創 2 / 兩邊都沒解 1**。
|
|
250
|
-
其中四條是「程式碼在那裡、但實際上沒有作用」,已全部修掉:
|
|
251
|
-
|
|
252
|
-
| | 問題(白話) | 根因 | 修法 | 實測 |
|
|
253
|
-
|---|---|---|---|---|
|
|
254
|
-
| **α** | **游標放在釘選欄上滾輪,表格完全不動** —— 而釘選欄正是視線落點(SKU / 名稱 / ⋮) | 左右面板是 `overflow:hidden`,滾輪事件在它們身上什麼都不會發生,直接冒泡出去 | 原生監聽轉發到 center body。**必須原生不能用 React 的 `onWheel`** —— React 17 起把 `onWheel` 以 passive 註冊([release notes](https://legacy.reactjs.org/blog/2020/08/10/react-v17-rc.html)「Keep onTouchStart, onTouchMove, and onWheel passive」),合成事件裡 `preventDefault()` 不生效。center 自己也捲不動時不吃事件,讓它冒泡給頁面。**對照 v33.3.2** [`gridBodyCtrl.ts#L361-L411`](https://github.com/ag-grid/ag-grid/blob/v33.3.2/packages/ag-grid-community/src/gridBodyComp/gridBodyCtrl.ts#L361-L411):它也在釘選容器上掛 wheel,但只轉橫向(`shiftKey \|\| \|dx\| > \|dy\|` 視為橫向、`scrollBy({ left: deltaX \|\| deltaY })`),垂直靠結構(釘選欄與中間同一個垂直捲動容器);我們三區各自捲,兩軸都轉,Shift+滾輪同樣解讀為橫向(2026-09-05 稽核補齊) | 修前 `wheel(0,180)` → center `scrollTop` = **0**;修後 = **180**。center 無垂直溢出的那張表維持 0(正確) |
|
|
255
|
-
| **β** | **我 2026-09-04 加的裝飾軌道畫在真捲軸上方 11px,而且一捲就跟著跑掉** | 掛在釘選面板**裡面**:(a) `absolute; bottom:0` 解析到 **padding box**,而讓位用的 border 在 padding box 外面;(b) 面板即使 `overflow:hidden` 仍是捲動盒,絕對定位子元素屬於可捲內容 | 搬到凍結邊界線所在的那一層(外層 wrapper:不裁切、不捲動),位置用同一組 `--dt-left-w` / `--dt-right-w` | 修前軌道 y **773.2**、真捲軸帶 784.2–795.2,捲 150px 後軌道跑到 623.2;修後軌道 **784.2–795.2**,捲動後不動 |
|
|
256
|
-
| **γ** | **防回彈的守衛從寫下那天起就沒生效過** | 用 boolean 旗標 + `queueMicrotask` 解鎖 —— scroll 事件在「更新畫面」那一步派發,**一定晚於 microtask checkpoint**,事件到達時旗標早就放掉了 | v33.3.2 是「來源所有權 + 150ms 計時器」([`gridBodyScrollFeature.ts`](https://github.com/ag-grid/ag-grid/blob/v33.3.2/packages/ag-grid-community/src/gridBodyComp/gridBodyScrollFeature.ts) #L41-L42、#L64、#L205-L214、#L224-L241:第一個捲動的容器成為 `lastScrollSource`,其他容器的事件在持有期間丟掉,`setScrollLeftForAllContainersExceptCurrent` 寫所有容器但跳過持有者,停捲 150ms 後釋放)。**我們刻意不用計時器**:記「寫進去之後的實際值」,事件值相同才吞 —— 同步剛結束使用者立刻捲次要區也即時被接受,且與時間無關不會早放晚放;紀錄不論吞不吞都消費掉,不留過期記號。(2026-09-05 更正:此欄先前寫「換成 v33 的語意、不需要旗標」,與原始碼不符 —— v33 是旗標 + 計時器,我們是刻意的不同做法) | 五條時序全綠:正向同步 / 反向導回 / 同步後緊接使用者捲動 / header 導回 / 捲到最右不回彈 |
|
|
257
|
-
| **δ**(同日撤回) | 早上以為「縱軸量測把 body 自己的 border 算成捲軸高」是 bug | **誤判**:縱軸不變式是「pinned 與 center 的可視列高相等」,center 內容盒之下少掉的每一像素(捲軸或它自己的 border)pinned 都得讓出同樣多,border 本來就該算進去;橫軸禁令的理由是那一軸量得到不變式本身(header.clientWidth − body.clientWidth),縱軸沒有對照物 | 撤回,恢復 `offsetHeight − clientHeight`;上一段 I11b / I12 的「15px 透明邊框模擬捲軸」契約也正是靠這個式子才成立 | 撤回前 CI I12 三條紅(left 300 / center 285、maxScroll 1700 vs 1715、border-bottom 0 vs 15);撤回後由 I12 守 |
|
|
258
|
-
|
|
259
|
-
**機械閘 I17**:軌道與真捲軸帶同一條線(≤0.5px)、捲動後不飄、釘選欄滾輪轉發(左右兩側各驗,含 Shift+滾輪 = 橫向)。
|
|
260
|
-
gutter = 0 的平台(overlay 捲軸)自動跳過軌道那半,滾輪那半照驗。
|
|
261
|
-
**機械閘 I18 / I19**(2026-09-05 稽核補閘 —— 缺陷 C 的雙向導回與 γ 的身分守衛先前只有手動實測,守衛整段拿掉所有閘照樣全綠):I18 = header 被捲動 200 → center 200、左釘選欄被捲動 300 → center 300 且右側 300;I19 = center 捲到最右後 header === center === max(不回彈)、header 可捲範圍被縮小 40px 時 center 仍停在 max(被夾過的那一筆被認出並吞掉)。
|
|
262
|
-
|
|
263
|
-
**評估後不改的三條**(理由寫在這裡,免得下一輪又重查):
|
|
264
|
-
- **量測 effect 每次 commit 都跑**(`useLayoutEffect(measureScrollbarGutters)` 無依賴陣列):它是「列數變動時捲軸出現/消失」的安全網,而那個情境的觀測點(I16 的哨兵)雖然涵蓋得到,**拆掉安全網的風險大於未經量測的效能收益**。要動要先量。
|
|
265
|
-
- **`hasRowActions` 時右面板寬是量出來的**:乾淨解要求 rowActions 有宣告寬度,但它的內容是 consumer 傳的任意 JSX。已登記為殘留,不強修。
|
|
266
|
-
- **分頁表格預設 `height: 'auto'`**:是規格明載的決定(頁碼是唯一導覽通道,再疊 body 內捲動 = 雙重導覽),不是缺陷。
|
|
267
|
-
|
|
268
|
-
---
|
|
269
|
-
|
|
270
|
-
### 六之二之一、兩容器架構的已知缺陷清單(2026-09-03 對抗式稽核)
|
|
271
|
-
|
|
272
|
-
header 與 body 是**兩個容器、各自跑一次 CSS flex 分配**(見不變條件 (6));下列缺陷全部源自這一點,
|
|
273
|
-
補償只能讓「兩次計算的輸入相等」,不能讓它變成「一次計算」。已修的在上面,未修的列在這裡,
|
|
274
|
-
避免下一個人以為都修完了。**AG Grid v33 對「欄寬分歧」這一類結構性免疫**(欄寬只算一次寫進 `AgColumn.actualWidth`,header 與 body 讀同一個整數,**三個區都是**);對「捲軸吃掉版面」這一類則是把捲軸**抬出資料區**(`.ag-root` 層的假水平捲軸 + `.ag-body` 的 in-flow 垂直捲軸 + 三區共用同一個 `.ag-body-viewport`),不是靠補償。我們已對第一類採用同一個模型;第二類仍用補償,代價列在 O/P/Q。
|
|
275
|
-
|
|
276
|
-
| # | 缺陷 | 觸發條件 | 嚴重度 | 狀態 |
|
|
277
|
-
|---|---|---|---|---|
|
|
278
|
-
| A | `rowActions` 佔位欄:header 用固有寬、body 用 `flex-1`,右釘選區剩餘空間分給不同項目集合 | `pinnedRightColumns` 非空 **且** 有 `rowActions`(目前無 story 命中);真正能拉開差距的是 actions 內容**可收縮**(換行文字等)、佔位與 actions 格的 min-content 不同 | 「可達數十 px」是**推導值、未觀測**(2026-09-05 降級):現行架構右面板寬 = 量 header 固有寬 = Σ 欄寬 + 佔位寬,body 的 `flex-1` actions 格拿到的剩餘空間正好等於佔位寬,兩邊同值;推導式 (P−F)/(n+1) 假設面板寬獨立於佔位寬,在本架構不成立 | **已修**(header 佔位補 `flex-1`,防禦性對稱);**無閘覆蓋**:沒有 story 同時開 `pinnedRightColumns` + `rowActions`,I11c 對 0 欄的區直接跳過、也不比佔位寬 vs actions 格寬,把 `flex-1` 拿掉 CI 照樣綠(M15 / M32 缺口,登記) |
|
|
279
|
-
| B | 釘選區:欄寬沒進「算一次」→ 整區退回 `flex: 1 1 base`,另加「量 header → state → 灌給 body」的量測旁路(`offsetWidth` 取整殘差 + 首幀 0)。**2026-09-05 更正根因敘述**:原寫「= 使用者最初回報的同一個機制,只是換個區出現」—— 釘選區 header 與 body 兩個容器寫的是**同一個寬**,flex 沒有 15px 這類可分配差額,center 的 15/n 逐欄累積機制在釘選區並不成立;釘選區的真實風險是 flex 沿用 + 量測旁路(取整殘差、首幀 0),以及 header 內容撐寬面板(缺陷 E) | 一律;非整數縮放 / DPR 1.25 放大取整殘差 | 中高 | **已修**(2026-09-03):`resolvedWidths` 現在涵蓋 left / center / right 三區(v33 的 `HeaderCellCtrl` / `CellPositionFeature` 同樣不分區,釘選欄只是不參與 flex 分配);首幀由 `useLayoutEffect` 消掉;機械閘 I11c 對三區各自斷言。**面板寬也已算出來**(2026-09-04 `f3fe9f2e`,見不變條件 (9);原「未修:面板寬度仍是量出來的」已過期),殘留只剩 `hasRowActions` 右面板量測(六之二之零)。I11c 只鎖結果不鎖機制(不變條件 (9) 末段) |
|
|
280
|
-
| C | `overflow:hidden` 的面板仍會被瀏覽器因 focus 捲動,而同步是單向的 | Tab 到 center header 被截掉的欄寬把手 / 釘選區被截掉的可編輯 cell | 中高(一旦發生永久錯位) | **已修**(次要捲動區導回 center body) |
|
|
281
|
-
| D | 欄位群組(`columns: [...]` 巢狀)時 header 只渲染最上層、被葉 id 濾光 → 空白 header 列 | 任一欄用 TanStack 欄位群組寫法 | 高(完全對不上) | **明確不支援 + 開發期擋下**(2026-09-04):多層表頭要處理跨欄合併與釘選/拖寬/虛擬捲動的交互,是一個功能不是一個修補,現在不做。改成偵測到帶 `columns` 的欄位定義就在開發期 `console.error` 指名筆數與後果,production 不進 bundle。**不用型別擋的理由**:收窄 `columns` prop 會讓所有把欄位宣告成 `ColumnDef<Row>[]` 的既有 consumer(含 work-management)編譯失敗 —— 那才是真的副作用 |
|
|
282
|
-
| E | 釘選欄 header 的 hover ⌄ 選單與排序箭頭改變面板固有寬 → center 欄寬重排 | 預設模式 + 釘選欄 header 內容寬 ≥ 該欄 size | 中高(滑過去就跳) | **已修**(2026-09-03,由缺陷 B 的修正連帶解掉,經對抗式驗證確認):釘選欄 header cell 現在是解析出來的固定寬(inline width / minWidth / maxWidth;2026-09-10 拿掉了 `overflow-hidden` —— 它不是這個修正的一部分,卻把欄寬把手跨到鄰格的外側 3px 裁掉、右半不可點,見 `resize-handle.spec.md` 命中區),`hidden group-hover:inline-flex` 的 ⌄ 選單再也無法把面板的 max-content 撐大 —— 這個機制在現行程式碼裡已不存在。**面板寬已算出來**(2026-09-04 `f3fe9f2e`):`width: leftWidth` 字面仍在,但 `leftWidth` 現在是 Σ 解析寬(`panelWidth`),不再是量測值,量測旁路已移除(同 v33 `PinnedColumnService.setupHeaderPinnedWidth`,見不變條件 (9));**殘留** = `hasRowActions` 右面板量測(六之二之零)。**閘缺口**(2026-09-05 稽核登記):沒有任何 check 對釘選 header hover / 排序後再量面板寬與 center 欄寬,I11c 只量靜態;「機制已不存在」是對抗式讀碼的結論,不是 runtime 斷言 |
|
|
283
|
-
| F | `autoRowHeight` / cell error 時同一列在三區各自算高度;虛擬捲動只量 center | `autoRowHeight`、`cellErrors`、`meta.wrap` | 中高 | **已修**(2026-09-04):照 v33「算一次、廣播給該列所有 GUI」的模型 —— 量三區同一列的**自然高度**取 max,再把同一個數字寫回三區(`sharedRowHeights`)。量測前先整批清掉上一輪寫上去的 inline height 再強制一次 reflow,所以讀到的永遠是內容的自然高度;若直接讀 `offsetHeight` 取 max,列高只會漲不會縮。清完**同步**還原,ResizeObserver 在影格結束看不到淨變化,不會被自己的量測叫醒。只在「有 auto-height 列 **且** 有第二個區」時才跑,單區表完全不付這個 reflow 成本。虛擬捲動一併解掉:center 的 `measureElement` 讀到的就是共用高度,`vr.start` 因此對三區都成立。**新增覆蓋**:`自動行高` story 第三個 pane(auto-height × 釘選欄 × Row Actions)—— 這個組合先前**沒有任何 story 命中**(M15);機械閘 `data-table-invariants.mjs` I15 逐列比對三區高度(≤0.5px),並先斷言「真的量到三個區」與「真的有被撐高的列」避免空轉 |
|
|
284
|
-
| G | 右對齊欄被排序時,排序箭頭把標題推離右緣 20px(body 值仍貼右) | 該欄被單欄排序 / hover | 中 | **已失效**(2026-09-04):前提消失,不是修好 —— 2026-09-04 user 拍板「header 的規格就是要一致,只有內容會置右」後,表頭**一律靠左、不跟欄位 `align` 走**(`data-table.tsx` 表頭 cell 上的註解「表頭一律靠左,不跟著欄位的 align 走」;移除 align class 的 commit `2b805738`、清掉表頭端 `align` 變數的 `147e05a2` —— 不再引行號,會漂),系統裡不再存在「右對齊的表頭」,排序箭頭也就不可能把標題推離右緣。原敘述寫於表頭還會跟著 align 走的年代。**v33.3.2 對同一症狀的解法不同**:`numericColumn` 是表頭與儲存格**同向靠右**([`defaultColumnTypes.ts#L3-L11`](https://github.com/ag-grid/ag-grid/blob/v33.3.2/packages/ag-grid-community/src/entities/defaultColumnTypes.ts#L3-L11) 同時給 `headerClass: 'ag-right-aligned-header'` 與 `cellClass: 'ag-right-aligned-cell'`),再靠 `.ag-right-aligned-header` 把 label container 翻成 `flex-direction: row`、文字 `text-align: end`([`_general.css#L495-L501`](https://github.com/ag-grid/ag-grid/blob/v33.3.2/packages/ag-grid-community/src/theming/core/css/_general.css#L495-L501)),讓排序圖示換邊而標題仍貼右;我們依 user 拍板改為表頭恆靠左,屬**刻意偏離**(第七節) |
|
|
285
|
-
| H | `data-table.css` 的 `::-webkit-scrollbar` 客製(10px、track、thumb、corner)實測未生效,瀏覽器畫的是原生 15px | 一律 | 中(死碼 + 規格與 css 數字矛盾) | **已修**(2026-09-04):**原本記載的根因是錯的**。逐條隔離實測:`::-webkit-scrollbar:horizontal{height:10px}` → 實際 15px(無效);不帶偽類的 `::-webkit-scrollbar{width/height:10px}` → 實際 10px(有效)。**機制(2026-09-05 依 Blink 原始碼校正)**:沒有裸 `::-webkit-scrollbar` 規則命中時,Chrome **根本不會建立 custom scrollbar** —— [`selector_checker.cc` `CheckScrollbarPseudoClass`](https://chromium.googlesource.com/chromium/src/+/main/third_party/blink/renderer/core/css/selector_checker.cc) 在元素自身樣式解析階段沒有 scrollbar 物件時直接 `if (!scrollbar_) return false;`,帶 `:horizontal` / `:vertical` 偽類的規則全部不匹配,於是回到原生 15px;裸規則存在時 `:horizontal { height }` **是可以改尺寸的**,不可據此推出「偽類永遠改不了尺寸」的通則(原句「Chrome 的捲軸尺寸只讀不帶偽類的那條規則」是對此機制的近似描述,結論不變、通則不成立)。2026-04-30 拆成兩條偽類是為了躲「`display:block` 讓沒溢出的軸也冒捲軸」——但不帶偽類且不寫 `display` 時實測**不會**誤顯(只有水平溢出時 v=0、完全不溢出 0/0),當初要躲的是 `display:block`。原記載的「`@supports` 重新宣告標準屬性導致忽略偽元素」把該組合隔離重現出來是 10px,**不成立**。**修法選了「只留 W3C 標準屬性、webkit 那一整套刪除」**:讓 webkit 生效雖然控制最完整(含 corner 與 thumb hover),但 `::-webkit-scrollbar` 會把 overlay 捲軸的機器**強制變成佔版面的捲軸**,為兩項 webkit-only 加值付這個代價不划算;`scrollbar-width: thin` + `scrollbar-color` 是標準屬性、Firefox 也吃、不改變任何人現有的捲軸行為模式(實測 Chrome/macOS classic 由 15px → 11px)。實際寬度不寫死在任何地方,`measureScrollbarGutters` 每次量。**與 ScrollArea 的 SSOT**:機制不可能共用(ScrollArea 是 Radix 自繪 DOM 拇指、不吃版面寬),能共用的顏色早就是 SSOT —— 兩邊都吃 `--scrollbar-thumb`/`--scrollbar-track`(semantic.css:383-385,2026-05-09 抽出);**幾何不共用**:ScrollArea 自繪 10px,DataTable 是 `scrollbar-width: thin` 由瀏覽器決定(Chrome/macOS classic 11px),radius 不可控 —— `scroll-area.spec.md`「視覺 token」表 2026-09-05 同步,不再寫 DataTable 消費同一個 10px。**無機械閘**(登記):CI 是 overlay 捲軸(gutter = 0),還原 webkit 區塊或改回 `scrollbar-width: auto` 沒有測試會紅;可行的閘 = 以 `ignoreDefaultArgs: ['--hide-scrollbars']` 啟動 Chromium,斷言 `[data-datatable-hscroll]` 的 `offsetHeight − clientHeight` < 15 且 computed `scrollbar-width === 'thin'` |
|
|
286
|
-
|
|
287
|
-
| I | 補償用實體方向屬性,RTL 下捲軸在另一邊 | `dir="rtl"` —— **DS 從未支援**(`packages/design-system/README.md` 相容矩陣「Document direction:LTR only」,consumer 不得把 `dir="rtl"` 視為已支援);程式碼裡也沒有任何 `enableRtl` 對應選項,本列是**前瞻登記**,不是今天可觸發的缺陷 | 中(僅在未來支援 RTL 時) | **原擔憂的兩處已換成方向無關機制**(2026-09-03;2026-09-05 把「已修」改成這句 —— 沒有任何 `document.dir = 'rtl'` 下的檢查,RTL 安全**未經驗證**):兩處 padding 補償都移除後,橫軸補償是內層 wrapper 的 `minWidth`(內容寬,無方向)、縱軸是 `border-bottom`(block 方向,RTL 不鏡射),與 logical property 無關。**實體方向殘留清單**(2026-09-05 補齊):表頭段面板邊界線 `.dtPanelBoundaryRight/Left::after`(`right:0` / `left:0`)、列區段 `.dtLeftBoundary::before` / `.dtRightBoundary::after`(`left` / `right` 由 `--dt-left-w` / `--dt-right-w` 定位)、裝飾軌道同用這兩個變數。**橫軸 spacer 的閘缺口**:I11 的 `headerRange === bodyRange` 在 CI(overlay 捲軸,`vScrollbarSpacer = 0`)下兩個 wrapper 同讀 `centerColsWidth`,刪掉 `+ vScrollbarSpacer` 照樣綠;I11b 注入邊框後明文不斷言捲動範圍相等(理由「RO 看不到這種變化」已被缺陷 S 的哨兵 + I16 推翻),補上斷言即可補閘,登記待補 |
|
|
288
|
-
| J | 三處量測都是整數量化(`offsetWidth` / `clientWidth`),殘差餵回 flex 分配 | 縮放 110%、DPR 1.25/1.5 | 中低(≤1px) | **不修,登記為已量化的取捨**(2026-09-04 改判):本規格第六節已載明「整數量測刻意用 `offsetWidth/clientWidth`(同一座標系),**不可**改用 `getBoundingClientRect()` —— 縮放時它與 `clientWidth` 不同座標系會算錯;非整數縮放下的殘差實測 ≤ 0.33px」。也就是這條的「修法」早就評估過而且更糟,原本標「未修」是分類錯誤。**AG Grid v33 免疫的原因是結構不是精度**:`AgColumn.setActualWidth` 存的是未取整的浮點數,欄寬只在 JS 算一次寫進 DOM,**從不把量到的值回讀進版面**,所以沒有量化殘差可以回饋。那條結構解 = 缺陷 O 已登記的架構取捨 |
|
|
289
|
-
| K | 拖拉下限 80 從未生效(讀 `columnDef.minSize`,而 TanStack `defaultColumnSizing.minSize = 20` 會 merge 進每個 columnDef → `??` 永不 fall back,使用者真能拖到 20px,而 cell 左右 padding 合計就 24px → 內容區變負值) | `enableColumnResize` 拖到底 | 中(契約與文件失效) | **已修**(2026-09-03):拖拉下限與 auto-fit 下限都改讀**公開契約** `meta.minWidth ?? MIN_COLUMN_WIDTH`,與旁邊 `meta.maxWidth` 的既有作法對稱。**未修的那半**:`maxSize` 預設 `MAX_SAFE_INTEGER` 仍被 `columnSizeStyle` 寫進每個 cell 的 inline style(瀏覽器當無上限,無視覺影響,屬 DOM 噪音);`columnSizeStyle` 的 resize 分支 `minSize` 也還在讀 `columnDef.minSize` |
|
|
290
|
-
| L | 「自動調整寬度」三個獨立缺陷:(a) 量的元素**本身已被裁字** —— `truncate` = `overflow:hidden`,其 `scrollWidth` 不會超過自己的寬 → 量到「現在的寬」而非「需要的寬」,按一次只 +buffer,被截斷的字永遠露不出來;(b) 量 `firstElementChild`,而樹狀列的第一個子元素是縮排/chevron 前綴 span(24px)→ 欄寬塌到下限;(c) 不量 header → 標題比 cell 長時 auto-fit 後看不到欄位名(v33 `skipHeaderOnAutoSize` 預設 false 就是要避免這件事) | 用該選單項時 | 高((a) 讓功能等於無效) | **已修**(2026-09-03,照 v33 `AutoWidthCalculator.cloneItemIntoDummy`):深拷貝進離畫面 dummy、清掉 `width/minWidth/maxWidth/flex` 後量 `offsetWidth`;量 cell 本身 + header;padding 由 clone 繼承真實 CSS,`+32` 硬寫改成小 buffer(不再隨 `--table-cell-px` override 漂移) |
|
|
291
|
-
| M | 欄間分隔線的歸屬 header 與 body 不對稱(右釘選 + rowActions 時同一位置 header 無線 / body 有線;拖曳中最後一格突然畫線)| 該組合;拖曳懸停路徑(見狀態欄) | 中低(1px 視覺)| **部分已修**(2026-09-04 修表頭側;2026-09-05 狀態校正):根因是**表頭與列用了兩個不同語意的判準**。列那側是 CSS `.dtCellGrid:last-child::after { content: none }`,判「DOM 上的最後一個子元素」;右釘選區後面還接一格 rowActions,所以最後一**欄**的 cell 不是 `:last-child`,照畫線。表頭那側只看欄陣列索引(`i < headers.length - 1`),不知道後面還有 rowActions → 同一位置變成列有線、表頭沒線。表頭判準補上 rowActions 這一項後,**靜態 DOM 下**兩邊結果一致;但判準仍是兩份(表頭 = JS 索引 + rowActions;列 = CSS `:last-child`),原句「判準只有一份語意」不成立,已更正。同時移除兩處死碼:表頭的 `&& !(isRight && i === headers.length - 1)`(前半成立時後半恆真)與 `cellEl` 那個從宣告日起就沒被用過的 `_isLastInRow` 參數。**「拖曳中最後一格突然畫線」撤回「查無此事」**:`ResizeHandle` 那條線確實 `showLine=false` 時不渲染(表頭側無此事),但**列側**的 drop indicator(`lib/drag-visual.ts` `dropIndicatorRow.after`)是插進列 DOM 的絕對定位子元素,拖曳懸停時它成為最後一個子元素,原本的最後一格因此失去 `:last-child`、當場畫線 —— 症狀存在,路徑在列側。乾淨解 = 兩邊共用一個 `isLastInRegion(index, isRight, hasRowActions)` 判準並以 data attribute 取代 `:last-child`(登記,未做;也沒有逐區比對表頭線 x 與列線 x 的像素閘)。**v33.3.2 的對照(2026-09-05 依原始碼校正)**:表頭欄線是 [`.ag-header-cell::after` + `border-right: var(--ag-header-column-border)`](https://github.com/ag-grid/ag-grid/blob/v33.3.2/packages/ag-grid-community/src/theming/core/css/_header.css#L215-L224)(`::before` 是 hover / moving 底色,不是線),儲存格是 [`.ag-cell { border-right: var(--ag-column-border) }`](https://github.com/ag-grid/ag-grid/blob/v33.3.2/packages/ag-grid-community/src/theming/core/css/_grid-layout.css#L117-L119);**兩者預設都關**([`core-css.ts#L854-L859`](https://github.com/ag-grid/ag-grid/blob/v33.3.2/packages/ag-grid-community/src/theming/core/core-css.ts#L854-L859):`columnBorder` 色 `transparent`、`headerColumnBorder: false`),開啟後最右欄由 [`.ag-column-last` 把 `border-right-color` 設透明](https://github.com/ag-grid/ag-grid/blob/v33.3.2/packages/ag-grid-community/src/theming/core/css/_grid-layout.css#L131-L134)(僅 `.ag-body-horizontal-content-no-gap` 時)壓掉。原句「兩邊都無條件畫、`.ag-header-cell::before`」與原始碼不符,已更正。我們的區邊界另有 `dtLeftBoundary/dtRightBoundary` 專責,所以取「本區最後一格不畫」 |
|
|
292
|
-
| N | `centerColsWidth` 被算兩次(`:1475` 與 `renderBodyRows` 內),註解宣稱是同一個 SSOT | 一律 | 低(今天數值相同)| **已修**(2026-09-04):`renderBodyRows` 不再自己重算一次中段總寬,改由呼叫端把 `centerColsWidth` 傳進去;`regionWidth ?? …` 的 fallback 三個呼叫端都傳值、已是不可達的死碼(登記,改為必填參數即可刪)。**AG Grid v33.3.2 的對應解是 `CenterWidthFeature`**(2026-09-05 依原始碼更正,原寫「共用同一個 feature 實例 + 同一段 spacer 邏輯」不符):是**同一個 class、兩個實例** —— 列容器對每個 center 容器各 `new` 一個、無 spacer([`rowContainerCtrl.ts#L348`](https://github.com/ag-grid/ag-grid/blob/v33.3.2/packages/ag-grid-community/src/gridBodyComp/rowContainer/rowContainerCtrl.ts#L348)),表頭容器另 `new` 一個、`addSpacer = true`([`headerRowContainerCtrl.ts#L256`](https://github.com/ag-grid/ag-grid/blob/v33.3.2/packages/ag-grid-community/src/headerRendering/rowContainer/headerRowContainerCtrl.ts#L256)),兩者共讀**同一個資料欄位** `visibleCols.bodyWidth`([`visibleColsService.ts#L53`](https://github.com/ag-grid/ag-grid/blob/v33.3.2/packages/ag-grid-community/src/columns/visibleColsService.ts#L53);[`centerWidthFeature.ts#L33-L57`](https://github.com/ag-grid/ag-grid/blob/v33.3.2/packages/ag-grid-community/src/gridBodyComp/centerWidthFeature.ts#L33-L57))—— **單一來源在資料不在實例**,與我們「一個 `centerColsWidth`、兩個 wrapper 各讀」同構。**語意差**(登記):我們的 `centerColsWidth` = Σ `getSize()`(base),v33 `bodyWidth` = Σ `actualWidth`(解析寬);溢出時兩者相等、未溢出時 `minWidth` 不起作用,今天無可觀測差異。**無專屬閘**:I11 在 CI(spacer = 0)下不會因 fallback 或公式分岔而紅 |
|
|
293
|
-
| O | 垂直捲軸的位置:center 持有垂直捲軸 → 有右釘選區(`pinnedRightColumns` **或** `rowActions`)時捲軸落在 center 與右釘選區**之間**;v33 與 MUI X 都把它放在整表最右緣(v33 因此把 `.ag-pinned-right-header` 加寬一個捲軸寬,原始碼註解逐字:「we add extra space to keep header aligned with the body」) | 上述組合 + 捲軸佔版面的平台(Windows / Linux;macOS 與 CI headless 是 overlay,gutter = 0 不會出現任何帶) | 低(架構取捨的可見差異,非錯誤結果) | **不修,已登記為已知取捨**。2026-09-03 對抗式驗證推翻了「這是未登記缺陷」的判定:捲軸落在 center 早已明載於本規格「center-body 是唯一的水平**與垂直** scroll container」,捲軸視覺落點的差異也已在上方「世界級對照」明示;`PinnedColumns` story 的「垂直捲動 + Pinned + Row Actions」子案例(`height="300px"` + 50 列 + `rowActions`)正是此組合,機械閘 I12 載入的就是它;而「Windows 捲軸蓋住外緣邊界線」這個實際症狀 2026-05-12 已被抓到並針對性修過(邊界線改成不佔盒模型的 1px 偽元素)|
|
|
294
|
-
| P | 凍結邊界線的垂直範圍只到水平捲軸帶上緣,表格底緣少 15px | 捲軸佔版面的平台 + 有釘選欄 | 中低(1px 視覺缺口) | **已修**(2026-09-04,user 要求「頂天立地」):線的**列區段改掛在列區外層**(`.dtLeftBoundary` / `.dtRightBoundary`),那一層不裁切也不捲動,高度就是三個面板的 border-box 高。**同日的第一次嘗試是假宣稱、已作廢**:用 `bottom: calc(-1 * var(--dt-hscroll-gutter))` 把 `::after` 負向拉出 padding box —— `overflow` 的裁切邊正是 padding box,面板自己 `overflow:hidden` 會把它裁掉,**版面盒延伸了、一個像素都沒畫**;而當時的 I13b 比的是 `getComputedStyle(::after).height`(255)與 border-box 高(255),兩個都是版面數字,所以給了假綠 —— 正是 M32 禁止的那一類。現在 I13 取像素並涵蓋捲動狀態。v33.3.2 確實是讓線貫穿的(它在 spacer 上補 border),貫穿才與上游一致 |
|
|
295
|
-
| Q | 水平捲軸帶在釘選區底下不連續(pinned 底下那 15px 是透明 border,露出的是 root 底色;凹槽只在 center 段)→ 表格底緣看起來是半截捲軸帶;v33 讓 spacer 帶 `overflow-x: scroll` 刻意畫出同款凹槽,只有剩角落時才關掉。兩條捲軸交會的角同源:v33 用自己控制的 `ag-scroller-corner`,我們交給瀏覽器,而想救回底色的那段 CSS 是死碼(見缺陷 H)| 捲軸佔版面的平台 | 低(純視覺)| **已修**(2026-09-04):取 v33 的**手段**而非結構 —— 我們的水平捲軸長在 center body 裡(架構取捨見缺陷 O),沒有 v33 那一列可放 spacer,所以改在釘選面板底部疊一條同高、`overflow-x: scroll` 的裝飾帶(`pointer-events: none`,只負責被瀏覽器畫出凹槽),讓整條捲軸帶在同一條線上連續。gutter = 0 的平台(overlay 捲軸)不渲染,零成本。角落交給瀏覽器這一點維持不變 |
|
|
296
|
-
| U | 欄寬把手改用 `patterns/resize-handle` 後,pointerdown 多了 `preventDefault()`(舊路徑只有 `stopPropagation` 再交給 TanStack 的 handler,而那個 handler 不 preventDefault)→ **編輯中拖欄寬不再自動結算退出編輯**,cell 維持在編輯態、naked Field 隨欄寬即時變形,放開後仍在編輯 | `enableColumnResize` + 正在編輯某格時拖該欄的把手 | 低(行為變更,無資料遺失) | **不修,登記為既定行為**(2026-09-04 對抗式驗證已實機確認,非推測):拖欄寬時保住編輯內容比中途結算更合理,且與 DS 其他拖曳把手一致。列在此是為了讓下一個人知道這是刻意的,不是漏掉 |
|
|
297
|
-
| T | 「自動調整寬度」的量測覆蓋與 view↔edit 0-delta 的機械斷言只涵蓋 string / select / textarea / currency 四種型別;date / time / person / multiPerson / boolean / url / multiSelect(原本還列了 avatar / tag —— 兩者不是 `meta.type` 的成員(`column-types.ts` `columnTypes`),2026-09-05 刪除)、巢狀列 cell 與 `meta.wrap` 沒有斷言 | 這些型別 | 低(覆蓋缺口,非已知缺陷) | **部分已修**(2026-09-04 擴閘;2026-09-05 狀態校正):`InlineEdit` story 本來就把 13 個欄位放進表(`data-table.stories.tsx` 的 `editableColumns`),缺的只是把閘指過去 —— 不必新增 story。I1-I4 抽成 `checkDisplayEditStability(storyId, cellTypes)` 後跑兩支 story:`InlineEdit` **13 欄中 10 欄有斷言**(SKU 唯讀 `skipEdit` 整欄跳過;boolean / url 標 `noInCellField` 跳過),加上 `RowAutoHeightInlineEdit` 的 textarea 欄,型別覆蓋 = string / number / select / multiSelect / person / multiPerson / currency / date / time / textarea **共 10 種**(原寫「11 種」與「13 格全為 0」把跳過的欄也算了進去,撤回)。**boolean 是 Checkbox 直接 toggle、沒有編輯欄位,設計上 N/A**;**url 不是**:`cell-registry.tsx` 的 url 分支有 in-cell 編輯 Field,只是入口是 hover 後的 Pencil(`aria-label="編輯連結"`),不走點格 / 鍵盤進 edit —— `data-table.tsx` 的鍵盤進 edit 判斷寫死 `meta.type !== 'boolean' && meta.type !== 'url'` 是**入口排除**,不是「沒有欄位」;閘尚未走 Pencil 路徑,**登記待補**(閘裡「設計上就沒有」的 SKIP 註解對 url 是錯的)。**仍未涵蓋**:巢狀列 cell、`meta.wrap`。**校正紀錄**:我原本憑印象把 multiSelect / person / multiPerson / date / time 標成「走 Popover 不適用」,量過才發現五種**都有** in-cell Field(date 會同時開 Popover,但 cell 內仍有 `data-field-mode="edit"`)——若照原本的猜測落地,這五種會被靜默跳過,覆蓋缺口只是換個地方存在 |
|
|
298
|
-
| S | **垂直捲軸的出現/消失若沒有伴隨 React 重繪,橫向補償會過期**。量測靠兩個來源:每次 render 後的 layout effect + ResizeObserver。但 2026-09-03 在真實 Chrome 實測證明 **RO 看不到這類變化** —— 新掛一個 RO 觀察 center body,`clientWidth` 1143 → 1128 → 1143 期間它 fire **0 次**(面板的 border-box 一直是 1173 沒變;RO 實際是對 border-box 變化才通知)。body 的內層 wrapper 也救不了:它的寬度由(過期的)欄寬決定而不是由容器決定,容器變窄時它反而溢出、寬度不變 → 同樣不 fire。**症狀**:表格右端多/少一個捲軸寬的空隙;**header 與 cell 仍完全對齊**(兩邊讀同一組整數,實測 delta 0),所以不是錯位。列數變動走 React 重繪會自動修正,真正會卡住的是「捲軸因非 React 因素出現」(圖片載入撐高列、字體 swap、動畫結束) | 捲軸佔版面的平台 + 上述非 React 觸發 | 中低(≤ 一個捲軸寬的右端空隙,無錯位) | **已修**(2026-09-04):照本欄原本寫下的第一個解法做 —— center body 內放一個 0 高、`width:100%`、`role="presentation" aria-hidden` 的空元素交給 RO 觀察。`width:100%` 是相對**內容盒**算的,捲軸一出現內容盒就變窄,它的 border-box **真的**變了,RO 就一定會響。實測:容器 `clientWidth` 300 → 285 時它同步 300 → 285;釘選 story 兩張表 518/507 與各自的 `clientWidth` 完全相等。**a11y 實跑 axe**(`aria-required-children` / `aria-required-parent` / `aria-hidden-focus` / `presentation-role-conflict`)= **0 violations**(`aria-required-children` 通過 67 個節點)—— 這條是必驗的,2026-07-29 WM beta.95 錨例正是「修 `scrollable-region-focusable` 反而引爆 `aria-required-children`」。**機械閘 I16**:哨兵存在 / 0 高 / 不進無障礙樹、哨兵寬 === `clientWidth`、注入 20px 透明右邊框(與捲軸同型的內容盒縮減,同 I11b/I12 既有模擬手法)時哨兵跟著縮、還原後回原值。實測 518 → 498 → 518 與 507 → 487 → 507。**不依賴真捲軸**,所以在 overlay 捲軸的 CI 上也不是空轉 |
|
|
299
|
-
| R | 「透明 border 之下 panel 底色照樣畫」的機制敘述指錯盒:pinned body panel 自己沒宣告底色,那 15px 露出的是 root 的 `bg-surface`。今天結果一樣(root 底色 = 列的視覺底色)所以看不出接縫,但**一旦 consumer 給列 zebra / 選取底色,最後一列的底色不會延伸進那 15px** | 列有自訂底色時 | 低 | **已修**(2026-09-04):兩個釘選面板自己宣告 `bg-surface`。`background-clip` 預設 border-box,所以面板有底色就會畫進下方那條透明 border 的讓位帶;沒宣告時露出的是 root 的底色 —— 今天同色所以看不出來,加上之後即使 consumer 給列 zebra/選取底色,讓位帶也永遠屬於表格表面而不是背後的東西。**今天零視覺差異**(root 本來就是 `bg-surface`)|
|
|
300
|
-
|
|
301
|
-
**已用實測推翻的疑慮**:稽核擔心 header 的 `padding-right` 會讓水平捲動範圍比 body 少一個捲軸寬
|
|
302
|
-
(Chromium 對區塊容器 inline-end padding 不算進 scrollable overflow)。實測相反:
|
|
303
|
-
header `scrollWidth` 915 = body 900 + padding 15,兩邊捲動範圍都是 397,捲到最右端 header 與 body
|
|
304
|
-
`scrollLeft` 同為 397、每欄位移 0。
|
|
305
|
-
|
|
306
|
-
### 六之三、Runtime perf budget canonical(2026-05-14 codex+Layer A)
|
|
307
|
-
|
|
308
|
-
`scripts/runtime-perf-datatable.mjs`(Playwright,cooldown 60-90s)+ 60fps 16.67ms canonical(web.dev / MDN long-task >50ms)。**4 test cases**:
|
|
309
|
-
|
|
310
|
-
| Case | Story | Avg / p95 / long-task | Gate |
|
|
311
|
-
|---|---|---|---|
|
|
312
|
-
| A Plain virt | `VirtualScroll` | ≤16.67/≤33/0 | hard |
|
|
313
|
-
| B Rich budget | `RoadmapPerfBudget`(500×13 rich+inline-edit,fixed 600px;2026-05-17 整併誤 retire → 2026-06-12 R2 重建,gate 恢復可跑) | ≤50/≤80/≤1 | hard |
|
|
314
|
-
| C Row drag | `RowDragWithVirtualization` | ≤33/≤50/longest<100 | soft(DnD thermal) |
|
|
315
|
-
| D Edit isolation | `<Profiler>` TBD | skip ≥ visible−active | hard |
|
|
316
|
-
|
|
317
|
-
**2026-06-12 重建後實測**(static build,3 runs/case):1x CPU — A 16.56/16.8/0、B 24.0/33.4/0、C 16.67/16.8/0 全過 gate;script 預設 4x throttle 在有背景負載機器上全 case 一致 ~2.3x 超標(含未動過的對照組 A)→ 本表門檻實證對應 1x/閒置條件,4x 門檻是否重校 = user 決策(本表數字不動)。
|
|
318
|
-
|
|
319
|
-
**Anti-pattern**:`RoadmapAllInOne`(全 features stack)≈117ms 不達 B,因 SortableRowProvider/column reorder/resize/selection/overlay 同時開。Consumer:13+ cols rich-cell → 拆 detail drawer / column visibility 預設 hide,不該期 60fps + 全 feature stack。Cite + Phase 1/2 history → `cell-registry.tsx` perf-fix JSDoc(`buildCellWithSurface` 上方)+ commits log。
|
|
320
|
-
|
|
321
|
-
**捲動的結構不變條件(2026-09-08,fiber 歸因實測後 codify)**:虛擬捲動每一步**只准新掛載的列與它自己的副作用執行**,步前就存在的列與表頭不得有任何元件 render。修前 `RoadmapAllInOne` 每步舊列 2754 + 表頭 593 個元件重繪;修後舊列 ≈2(Avatar 圖片載入失敗的 fallback state,沙箱擋外網)、表頭 0。同一支 canonical 儀器(上表,1x CPU)前後:RoadmapAllInOne 平均每幀 57.3 → 17.7ms、p95 66.7 → 33.3、最長任務 87 → 0;VirtualScroll 39.5 → 16.5;RoadmapPerfBudget 21.3 → 16.7;RowDrag 19.9 → 16.6。三個根因與對應的守法(Codex R7 對抗審查校正後):
|
|
322
|
-
- **dnd-kit sensor options 必須身分穩定**:`useSensor(Sensor, options)` 是 `useMemo(…, [sensor, options])`,options 寫成 render 內的字面值就每次換新 → `useSensors` → DndContext activators → 每列 `useDraggable().listeners` → 每列 ctxValue → render-prop `children(ctxValue)` 重產整列。守法 = `dndSensorOptions` useMemo + Provider 內 `useMemo(() => children(ctxValue), [children, ctxValue])`。(useDraggable / useDroppable 整個消費 InternalContext、無 selector;列掛卸造成的 droppable 集合換身分是 PublicContext 那條鏈,列層不得訂閱它。)
|
|
323
|
-
- **列與表頭都是「元素快取」**:`rowElCacheRef`(per row)與 `headerElCacheRef`(per region)在 deps 全等時回同一個元素,React 在該 fiber 直接 bailout。快取的正確性完全取決於 `epochDeps` / `headerEpochDeps` 有沒有列全:漏一個 = 舊列拿到過期資料(I2 抓到漏 `resolvedWidths` → 顯示↔編輯寬差 0.59px)。守法 = `scripts/data-table-row-cache-deps-invariant.mjs` 用 TypeScript 語法樹(scope 堆疊 + 最近宣告解析 + 排除型別位置)列出 `renderRowFresh` / `renderHeaderRowFresh` 的全部外層自由變數,每個必須在 deps 或白名單(附理由),`--selftest` = 拿掉 `resolvedWidths` 必紅 + 四個假陰性合成 fixture。**它只證明 lexical free variables**:經 `ref.current` / `table.getState()` / `table.options` 讀到的可變值要靠 deps 明列對應 state 與 props(`tableStateForEpoch.*`、`tableOptions`、各 `enable*`),自訂 header / cell renderer 讀 table 其他狀態不在保證內;useCallback 本體的 stale capture 是 exhaustive-deps lint 的職責。
|
|
324
|
-
- **controlled 值不得每 render 正規化成新物件**:`normalizeSelection(selectionProp)` 改 useMemo,否則 `selection` 身分每步變、一切依賴它的 memo 每步失效。
|
|
325
|
-
|
|
326
|
-
**機械閘** = `scripts/data-table-scroll-cost.mjs`:R0 每步真的重算的列 ≤ 換列數 + 2、R1 屬性變動、R2 節點增減、R3 commits ≤ 8(固定版本回歸預算:1 次虛擬捲動 + 5 次新列掛載副作用鏈,全在新列;Radix Tooltip / Checkbox 的 ref-state 可 batching,ref → 量測有先後依賴不能併)、**R4 單步內舊列裡被碰到的元件 fiber(含 bailout)最大值 ≤ 12(實測 0;舊列 = 步前就存在且不是最近 2 步內掛載的列,新列的掛載副作用鏈會跨到下一步的量測窗)、R5 表頭 ≤ 4(實測 0)**(fiber 歸因,React DevTools 同法;PerformedWork 會漏算「執行了但 bailout」所以另計 touched;逐步最大值不是平均;計數器丟例外即紅)、1px 步進 gBCR;`--selftest` = 預算歸零必紅 + 正向對照組(點全選 → 表頭與舊列都必須量到 render)。已知邊界:主要量連續捲動,開始/停止捲動時 `TableScrollProvider` 的 context 切換(Avatar / PersonDisplay 的 scroll-defer)不在 R4 內。
|
|
327
|
-
|
|
328
|
-
**零空白不變條件(2026-09-12;user 拍板「確認,改 data-table.tsx」)**:**送出的任何一幀,中央捲動區都不得有任何一帶是空的** —— 而且**不得為了達成它而讓幀距或長工變差**。兩條必須同時成立,單獨任一條都可以被作弊繞過(只要零空白 → 整片蓋死;只要不卡 → 維持空白)。
|
|
329
|
-
|
|
330
|
-
為什麼緩衝與列殼都不夠:捲動跑在**合成執行緒**上,跟主執行緒刻意隔離(Chromium RenderingNG:「Separating the main and compositor threads is critically important for performance isolation of animation and scrolling from main thread work.」<https://developer.chrome.com/docs/chromium/renderingng-architecture>)。主執行緒被長工佔住時,合成器照樣每 16ms 送一幀,React 不可能在那段期間把列放到新位置 —— **任何需要主執行緒的機制都輸掉這場競速**。實測 CPU×1、6000px 手勢:手勢 663ms 中長工 4 個合計 324ms,送出 38 幀有 **27 幀整片空白、17/17 帶全空、最長連續 410ms**,而列殼只出現 1 幀(舊判準 `cannotDrawViewport` 問的是「這台機器畫得完一個視窗嗎」= **能力**,快機器恆為 false)。
|
|
331
|
-
|
|
332
|
-
**已知殘留(2026-09-12 實測定位,非未解)**:上述覆蓋在 **DOM/幾何層恆真**,但**合成器層有例外** —— 捲動前緣新露出的圖磚偶爾在該幀送出時尚未完成光柵化,合成器就畫成圖層底色(白)。證據:把骨架底改塗單一實色後,絕大多數「空白帶」量到 **100% 該色**(幾何與繪製都對),少數幀同一區域是 **100% 白**,還有 **75% / 81% 部分覆蓋**的中間態 —— 那正是圖磚逐塊補上的簽名。發生率本機約每 6 趟 1 次、1–2 帶、16–18ms,CI 三趟皆 0。**繪製成本不是槓桿**(連純色實填也會發生),所以不是把漸層改便宜就能消除;剩下的槓桿是圖層/圖磚提示,但 6 對 6 的 A/B 沒有檢定力(對照組同樣 0/6),要宣稱修好需遠更大的樣本。
|
|
333
|
-
|
|
334
|
-
**做法**:已掛載的列必然是連續一段 `[first.start, last.end]`;這一段以外的整個虛擬高度,在同一次 render 裡鋪成兩塊**骨架底**(`[data-row-shell-band]`,CSS `repeating` 漸層,bar = `--muted`、列底線 = `--divider`,幾何抄列殼的 `h-3 w-3/5` / 系統欄 `h-4 w-4`)。覆蓋 = 上帶 ∪ 已掛載段 ∪ 下帶 = 整個捲動區,在 **DOM/幾何層恆真且與時序無關**(合成器層的例外見上);成本是每次 render 兩個 div,之後純由合成器搬運,因此不可能造成卡頓。用漸層而非真 DOM,是因為未掛載區可達數十萬 px,鋪真列是無上限的主執行緒工作。已知落差:漸層畫不出 `Skeleton` 的 `rounded-md` 圓角(12px bar 在 9000px/s 下看不出來)。
|
|
335
|
-
|
|
336
|
-
**機械閘**:`scripts/data-table-fast-scroll.mjs --assert-max-blank-frames=0`(**絕對**判準,判每趟最大值不判中位數 —— 空白幀是缺陷不是雜訊);對偶的「不得變卡」仍由同檔 `--ref=main` 的相對閘擋。
|
|
337
|
-
|
|
338
|
-
**快速捲動的列殼(2026-09-09 codify;Codex R8 解法 (b))**:上面的不變條件管的是「主執行緒做了多少事」,但使用者看到的是合成器送出的幀 ——
|
|
339
|
-
真實呈現幀量測(`scripts/data-table-fast-scroll.mjs --mode=gesture`,CDP screencast + 合成手勢)抓到 **main 與分支都有**的白:滾輪一甩
|
|
340
|
-
(每秒 6,000–12,000px)時,每側只預掛 5 列 = 200px 緩衝,而把整窗 27 列有錢的儲存格重畫一次要 100ms 以上,合成器一幀就把視窗推到
|
|
341
|
-
還沒掛任何列的區域,連續 17 幀(約 280ms)中央整片白、左右釘選面板停在舊位置。修法對齊 AG Grid `cellRendererParams.deferRender`
|
|
342
|
-
(捲動中先顯示 skeleton cell,停捲後補 renderer)與 MUI X server-side lazy loading 的 skeleton rows:
|
|
343
|
-
- **判準(自適應機器畫列能力,2026-09-10)**:只看**捲動造成的 commit**(初次載入、換頁、靜止時資料變動照舊全畫)。
|
|
344
|
-
跟得上的機器完全走原路:一般速度直接畫真列、零骨架。「跟不上」看真正的症狀 —— 兩次畫圖之間視窗移動的距離超過預掛緩衝
|
|
345
|
-
(overscan 列 × 列高;只拿 scrollTop 真的變了的畫圖當樣本;這一次的位移 > 2 倍緩衝(≈ 一個視窗)就立刻進入、平滑值 < 0.5 才退出),不看畫圖成本(CI 機器每次畫圖都超過 20ms 卻在 4,500px/s 跟得上,用成本判會
|
|
346
|
-
在一般速度出殼)。量到跟不上才啟用兩件事:
|
|
347
|
-
(1) **幀預算**:每次 render 先算「這一幀畫得完幾列真列」=(12ms − 每次 commit 固定成本)÷ 量到的每列成本(都指數平滑、夾範圍);
|
|
348
|
-
要新畫的列(新進視窗的、上次是殼的)依**可見優先、再依索引**排隊,排進預算的畫真內容,排不進的先畫**列殼**(`[data-row-shell]`,
|
|
349
|
-
`aria-busy`;同高、同分隔線、同欄寬),下一幀再補;沒有新列進窗的 commit(scrollTop 沒變、還在 isScrolling 尾巴)與停捲後預算放寬到
|
|
350
|
-
4 幀,停捲後可見殼一律當次全升級(固定成本很貴,一次多補幾列才補得快;但尾巴不能放到 8 幀 —— 讀到舊 scrollTop 時一次畫太多會讓視窗跑出前掛殼)。
|
|
351
|
-
(2) **前掛殼**:一次 commit 的時間內視窗會移動「速度 × commit 時間」這麼遠,這段距離的列先掛殼在**捲動方向**前面等著(最多 48 列;
|
|
352
|
-
只掛前方,不用對稱 overscan 浪費一半在視窗後面),
|
|
353
|
-
否則畫好的列落地時視窗早已捲過去、整片白。「跟不上」的判定有遲滯(見上),退出後殼還沒補完時用 4 幀預算
|
|
354
|
-
分幾次補、前掛不歸零(否則便宜的預算 commit 與一次全畫的長 commit 會來回震盪);停捲後(scrollTop 不再變、且虛擬化器 250ms 內沒有
|
|
355
|
-
scroll 事件)可見的殼一律當次全升級,
|
|
356
|
-
預算只節制視窗外 —— 只比 scrollTop 不夠,長 commit 之後下一次 render 讀到的 scrollTop 常常還沒更新。**緊急跳轉**看「上一次 render 開始到
|
|
357
|
-
這一次 render 開始」的位移(含上一次 commit 自己花掉的時間)跨過整個可視窗 → 新列一律先殼、殼 overscan 擴到半個視窗;不看
|
|
358
|
-
commit 結束後的位移(慢機器每次 commit 一結束下一次 render 就開始,中間位移很小,連續慢 commit 永遠觸發不了)。不用兩次 render 的瞬時速度決定
|
|
359
|
-
可見列要不要殼(快機器的正常短捲會被誤判)、也不用固定跳距(慢機器 commit 頻繁、位移不到一個 viewport 而永不觸發,CI 曾整片白 1 秒)。
|
|
360
|
-
保留中的真列、拖曳中、編輯中與選取格所在列不退回骨架;升級後仍同步三區列高。
|
|
361
|
-
- **兩個閘同時守**:`scripts/data-table-fast-scroll.mjs`(極速 6,000px/s 白區 ≤ 400ms、補齊 ≤ 1,000ms,CI 的 ubuntu runner 不節流就是慢機器 —— R17 判準在它上面整片白 1.3s、v8 降到 64 / 103ms;
|
|
362
|
-
CPU 節流 2× / 4× / 6× 只當本機重現與壓力工具,不當 CI 閘:CDP 節流在 2 vCPU 的共享 runner 上不是線性的,同一 job 不節流 64ms、4× 卻 1,769ms,
|
|
363
|
-
本機 4× 是 167ms,數字不能跨機器搬;2× 是「跟不上」旗標最容易震盪的中速帶,本機必跑)與下方一般速度的呈現閘(零骨架;內容延遲 p95 ≤ 兩幀(門檻 = max(34ms, 2 × 該機器截圖幀距的中位數))且單列最長 ≤ 3 倍門檻,CI 只在 dpr1 斷言延遲 —— dpr2 在共享 runner 是 raster 成本決定(幀距正常卻 p95 72),回歸對照改用本機 CPU 節流跑 main vs 分支(2× 節流 main p95 83 vs 分支 0);不用「最長 ≤ 34」,~100 列出現一次 3 幀停頓是機器雜訊;單一 scroll 事件跳過整個視窗 = 機器停頓造成的整窗跳轉,那一跑作廢重跑最多 3 次)。列快取閘 `data-table-scroll-cost.mjs` R0
|
|
364
|
-
(「重算的列 ≤ 換進的列 + 2」)只在不節流下斷言:慢機器模式下殼 → 真列的升級本來就是同一列畫兩次(先殼後真),4× 節流實測 11.8 vs 10.8,
|
|
365
|
-
這是設計上的兩段式畫法,不是舊列被重算。
|
|
366
|
-
- **一般捲動的呈現閘** = `scripts/data-table-scroll-perception.mjs`:PNG 同幀解碼列身分與骨架狀態,量首次進窗至真內容的取樣延遲、骨架幀比例與面積時間。
|
|
367
|
-
1,500/3,000/4,500px/s 的減速與連續短捲均納入對照;`--sabotage=on --assert=on` 故意延後內容,必須拒絕。
|
|
368
|
-
- **機械閘** = `scripts/data-table-fast-scroll.mjs --mode=gesture`:量合成器實際送出的每一幀(不是 DOM、不是預估),中央區每 40px 帶完全
|
|
369
|
-
沒有墨跡 = 空白帶;閘 = 最長連續空白 ms 與停捲後殼補齊 ms。儀器自帶對照組(`--selftest`):500 列不虛擬化的靜態頁同手勢必須 0 空白
|
|
370
|
-
(高速位移本身不會被誤判成白)、每個 scroll 事件忙等 120ms 必須量到 ≥ 3 幀空白(該紅會紅)。
|
|
371
|
-
- **修後實測(2026-09-09,headless 軟體光柵,各 3 跑中位數;儀器 PNG 幀 + 骨架色可見 + 分隔線不算內容)**:12,000px/s 最長連續空白 556 → 31ms、空白幀 30 → 4;
|
|
372
|
-
6,000px/s 502 → 18ms、56 → 3 幀;3,000px/s 兩邊都 0、殼 0 幀(正常速度完全不出殼;初次載入 / 換頁也不出殼 —— 只有「上次 commit 是殼」的列吃補齊配額)。殼的 Skeleton 不做脈動動畫(幾百格透明度動畫讓光柵每幀重畫,消融實測空白幀 42 → 20);
|
|
373
|
-
合成器超前時 overscan 擴到半個視窗(每側上限 24 列)。CI 閘:6,000px/s 最長連續空白 ≤ 400ms、停手後殼 ≤ 1000ms 補齊(同句跑 main 紅 550–700ms)。
|
|
374
|
-
|
|
375
|
-
**捲動中不量沒人看的東西(2026-09-10 codify)**:這一輪處理「在捲動中量了、但畫面上沒人看得到」的幾何讀取。
|
|
376
|
-
新儀器:攔截 `scrollTop` / `clientHeight` / `offsetWidth` / `getBoundingClientRect` 等的 getter,按呼叫點統計一次手勢的讀取次數
|
|
377
|
-
(全功能整合範例、40 步滾輪)。**11,095 → 10,163**。
|
|
378
|
-
- **列拖曳把手在閂上期間延後量測**:捲動時把手一定是隱藏的(`rowDragScrollLatch`),但 hover 代理會不停把 `data-hovered`
|
|
379
|
-
換到指標底下的新列,每換一次就量一次位置(243 次 `getBoundingClientRect` + 81 次 `clientHeight`),全算在隱藏的東西上。
|
|
380
|
-
改成延後到閂鎖放開再補量 —— **延後不是跳過**:跳過會留下「指標停在同一列不動、放開後沒有任何事件重新量 → 把手回不來」的洞。
|
|
381
|
-
- **標籤溢出只量一次**(`Combobox`,1,080 → 540):細節見 `combobox.spec.md`「單行溢出」。
|
|
382
|
-
- **`clientHeight` 只在權威更新點讀**(掛載 + ResizeObserver),render 與 layout effect 讀快取。
|
|
383
|
-
**`scrollTop` 反過來必須讀 DOM 現值**:下面「機器跟不跟得上」的判準用它算位移,而長 commit 期間捲動事件送不進來,
|
|
384
|
-
讀快取等於永遠看到舊位置、永遠判成跟得上,慢機器的殼列安全網就不會啟動。
|
|
385
|
-
|
|
386
|
-
**同一輪量到、但**沒有**採用的三條(留檔免得重走;每條都附擋下它的數字)**:
|
|
387
|
-
1. **`useFlushSync: false`**(捲動事件裡不同步重畫)。`@tanstack/react-virtual` 3.13.23 的預設是在每個 scroll 事件的處理器裡
|
|
388
|
-
`flushSync(rerender)`;關掉它讓捲動事件耗時 p95 從 15.5ms 掉到 1.0ms。世界級對照也支持關掉 —— AG Grid 33.3.2
|
|
389
|
-
[`onVScroll`(ag-grid-community.js:25744-25777)](https://www.npmjs.com/package/ag-grid-community/v/33.3.2) 在捲動事件裡只記
|
|
390
|
-
`nextScrollTop` + `animationFrameSvc.schedule()`,重畫在 rAF 的 `executeFrame(60)`(同檔 :34057)。
|
|
391
|
-
**但量畫面就翻盤**:6,000px/s 空白幀 2 → 7.5、最長連續空白 17 → 27ms;dpr2 + 節流下合成器送出的幀 p95 19.2 → 32.4ms。
|
|
392
|
-
AG Grid 能走 rAF 是因為它的儲存格是輕量 DOM 且自帶 60ms 預算的分幀佇列,**不是同一個成本結構**;
|
|
393
|
-
照抄 API 形狀而不看成本結構會壞。仲裁一律看畫面,「捲動事件耗時」是歸因指標。
|
|
394
|
-
2. **共用量測排程器**(截斷偵測 / 頭像堆疊量測捲動中延後、停下後分批補)。快機器上很漂亮(幾何讀取再降 8,300 次、
|
|
395
|
-
6,000px/s 空白幀降到 2),但**慢機器上會把殼列安全網關掉**:5× 節流實測殼幀 17 → 9.5、最長連續空白 199 → 359ms,
|
|
396
|
-
CI 的 2 vCPU runner 上更是 528ms(閘上限 400ms)。
|
|
397
|
-
3. **捲動中一律方向預掛**。同樣在快機器上很好、慢機器上更糟(5× 節流最長連續空白 359 → 1,080ms)。
|
|
398
|
-
|
|
399
|
-
**根因(下一輪的前提條件,沒解決之前 2、3 不能上)**:殼列安全網的觸發條件是「視窗移動距離 ÷ 預掛緩衝」與「每列成本」,
|
|
400
|
-
兩者都是 **commit 成本的代理值**。它們之所以在 main 上準,是因為 main 的 commit 恰好把量測也算進去、與慢光柵正相關;
|
|
401
|
-
一旦把量測移出手勢窗、commit 變便宜、render 變密,同樣的速度下每次位移變小 → 判準說「跟得上」→ 安全網不啟動 →
|
|
402
|
-
慢機器整片白。**代理值被自己的改善打敗了。** 兩種替代判準都試過並量過,都沒有還原殼列:
|
|
403
|
-
(a)「進來的列數 × 每列成本 > 幀預算」(仍是 JS commit 時間,量不到光柵);(b)「量到的幀間隔 > 1.5 幀」(整條管線的產出,
|
|
404
|
-
理論上對,但實測殼幀仍只有 10)。下一輪要先**直接觀測殼列狀態**(把 `budgeted` / `budgetRows` / `costPerRow` 暴露成可讀的
|
|
405
|
-
測試訊號)再改判準,不要再靠推理猜。
|
|
406
|
-
|
|
407
|
-
**每列成本(2026-09-11;user 在自己的機器上回報「非常卡頓」後,在真實瀏覽器量出來的)**
|
|
408
|
-
|
|
409
|
-
**量測方式的更正**:先前所有效能結論都來自無頭瀏覽器,而它量到的每列成本是 1–2ms;**user 的真實 Chrome 上是 10–20ms**,
|
|
410
|
-
兩者差一個數量級,所有以無頭數字推導的判準因此全部失準。現在的做法是:沙箱起本機靜態站(`127.0.0.1`),
|
|
411
|
-
用**真實瀏覽器 + 真實滾輪事件**做 A/B;殼列的決策狀態掛成 `data-shell-state`(`window.__DT_DEBUG_SHELL = true` 才出現),
|
|
412
|
-
可直接讀出 `slow / budgeted / ahead / behind / commitCost / costPerRow / budgetRows`,不必再從程式碼推測。
|
|
413
|
-
|
|
414
|
-
**已落地:把每列內容量測的重複工作拿掉**(細節見各元件 spec)——
|
|
415
|
-
`Tag` 的截斷量測加字型 / 文字寬度兩層快取;`PeoplePicker` 頭像串的元素查詢改快取。
|
|
416
|
-
CPU 剖析(5× 節流)顯示這兩處原本是每列成本的第二、三大項(169–232ms 與 139.7ms,後者修後 48.7ms)。
|
|
417
|
-
**實測**:user 的真實 Chrome 上每列成本 **13.7 → 6.4–7.8ms**;本機 5× 節流、6,000px/s、4 次交錯對照,
|
|
418
|
-
最長連續空白 576 → **350ms**、空白幀 50.5 → **45.5**、停捲補齊 537 → **470ms**、主執行緒最長任務 154 → **133ms**,
|
|
419
|
-
每一項都更好。
|
|
420
|
-
|
|
421
|
-
**同一輪試過、被實測擋下而撤回的五項**(留檔免得重走;每項都附擋下它的數字):
|
|
422
|
-
1. **用「一次 commit ≥ 兩幀」當出殼的必要前提**。動機對 —— 原判準把「使用者甩得快」和「機器畫不動」混為一談
|
|
423
|
-
(真實滾輪一次十格位移約 1,000px = 5 倍緩衝,於是每次正常甩動都被判成跟不上;user 的機器實測 100% 的幀有骨架)。
|
|
424
|
-
但擋下它的是慢機器:CI 的 2 vCPU runner 殼幀 16 → 8.5、最長連續空白 438–476ms(閘上限 400ms)。
|
|
425
|
-
2. **殼列記帳休眠**(機制沒作用時不做 Map/Set 記帳)。它會把「哪些列已畫成真列」清掉,一旦轉回需要殼時所有列都被當成新列 →
|
|
426
|
-
本機 5× 節流最長連續空白衝到 2,102ms。
|
|
427
|
-
3. **layout effect 不讀 DOM**(改用 render 開始時的值)。`lastOffset` 記的是 commit 結束那一刻的位置,commit 期間合成器還在捲,
|
|
428
|
-
改用 render 開始的值會少算這一段。
|
|
429
|
-
4. **前掛時間基底加下限(兩幀 / 四幀)**、5. **預掛緩衝下限 10 列**。兩者都沒有改善,後者還讓 script 時間上升。
|
|
430
|
-
|
|
431
|
-
**方法論(這一輪最貴的教訓)**:5× 節流的空白量測**跑間變異極大**(同一個建置量到 234 / 242 / 576 / 601 / 818ms),
|
|
432
|
-
單次或兩次跑不足以判定;而且**基準一定要抓對**——我一度把 `b29fd702` 的建置當成上一版,據此連下五個「本輪更差」的結論,
|
|
433
|
-
換回真正的上一版(`eb5b42fc`)並跑 4 次交錯之後,結論完全相反。判定一律用**同一跑之內的交錯對照 + 至少 4 次**。
|
|
434
|
-
|
|
435
|
-
**剩下的成本是什麼(量過才寫,兩條路都實測走不通)**:5× 節流、6,000px/s 的 CPU 剖析上,最大一項是
|
|
436
|
-
commit 後那個 layout effect(自身時間 292.6ms / 18.7%)—— 它只讀一個 `scrollTop`,但那是在 React 剛改完 DOM 之後,
|
|
437
|
-
會**逼出整張表的版面計算**,所以整筆版面成本都記在它頭上。
|
|
438
|
-
|
|
439
|
-
1. **把那個讀取拿掉(改用 render 開始時的位置)**:成本不會消失,只是換一個函式背 —— 直接改建置產物做對照,
|
|
440
|
-
292.6ms 只是搬到另一處變成 255.0ms,手勢反而從 1,556ms 變長到 1,657ms、scroll 事件處理中位數 0.47 → 53.02ms。
|
|
441
|
-
**結論:那是整張表固有的版面成本,不是多餘的讀取**;這也是前面第 3 項撤回的獨立佐證。
|
|
442
|
-
2. **不要在 scroll 事件裡同步重繪**(`useFlushSync: false`,對齊 AG Grid 把捲動處理丟進 animation frame 的作法):
|
|
443
|
-
4 次交錯對照下長工最長 157 → 130ms、script 1,109 → 1,049ms 是好的,但**使用者看得到的那幾項全部變差** ——
|
|
444
|
-
空白幀 49.5 → 50.5、最長連續空白中位 516 → 608ms、殼幀 9 → 13.5、停捲後補齊 490 → 600ms。
|
|
445
|
-
user 抱怨的是空白與骨架,所以不採用。
|
|
446
|
-
|
|
447
|
-
`Tag` 的截斷量測仍需讀一次 `clientWidth`、`use-truncated` 的 `scrollWidth/clientWidth` 同理,
|
|
448
|
-
但兩者都只是上面那筆版面成本的一部分,不是額外的。post-paint 重新量測(`useTruncated` 的 rAF + 100ms 各一次)
|
|
449
|
-
實測整趟手勢共 442 + 337 次、合計 39.3ms,加了快取之後已經很便宜,不值得再動。
|
|
450
|
-
|
|
451
|
-
**出殼的判準:機器畫不動,不是使用者捲很遠(2026-09-11 根因;user:「還是非常卡頓…比 main 還明顯超多」)**
|
|
452
|
-
|
|
453
|
-
**怎麼量到的**:在 user 的**真實 Chrome** 裡用 `MutationObserver`(不受背景分頁的 rAF / timer 節流影響)+ 真實滾輪輸入。
|
|
454
|
-
先前每一輪都在沙箱 headless 裡量,那裡每列成本 1–2ms,這套機制幾乎不會啟動 —— 所以 bug 一直藏著。
|
|
455
|
-
|
|
456
|
-
**量到的事實**:同一台機器、同一個操作(一次 10 格滾輪 = 1000px):
|
|
457
|
-
|
|
458
|
-
| | main(沒有殼機制) | 修前的本分支 |
|
|
459
|
-
|---|---|---|
|
|
460
|
-
| 視窗內骨架列 | **0** | **14 / 14(全部)** |
|
|
461
|
-
| DOM 穩定時間 | 148ms | 124ms |
|
|
228
|
+
### 預掛緩衝(overscan)與捲動效能 → `data-table-scroll-performance.spec.md`(2026-09-27 抽出,獨立 SSOT)
|
|
462
229
|
|
|
463
|
-
|
|
230
|
+
「預掛緩衝(overscan)— 隨機器能力自適應」、「六之三、Runtime perf budget canonical」、捲動的結構不變條件、零空白不變條件、快速捲動的列殼、捲動中不量沒人看的東西、每列成本、出殼判準、同一時間最多一列 hover、指標底下那一列永遠有 hover 反應 —— 整段住在 `./data-table-scroll-performance.spec.md`;本檔只留這個指標。
|
|
464
231
|
|
|
465
|
-
|
|
466
|
-
costPerRow=2.1–4.6ms fixed=10.0ms budgetRows=4 slow=1 ahead=1
|
|
467
|
-
```
|
|
232
|
+
### 六之二之零 / 六之二之一、捲軸專項稽核與兩容器架構已知缺陷清單 → `data-table-known-defects.spec.md`(2026-09-27 抽出,獨立 SSOT)
|
|
468
233
|
|
|
469
|
-
|
|
470
|
-
1. `ahead`(緊急跳轉)門檻 = 位移 ≥ 一個視窗高(540px)—— **一次普通滾輪就是 1000px**,恆為真 → `budgetRows` 寫死 0 → 整窗全殼。
|
|
471
|
-
2. `budgeted` 門檻 = 位移 > 2 倍預掛緩衝(overscan 5 列 × 40px = 200px)—— 同樣恆為真。
|
|
472
|
-
而且 `fixedCost` 卡在它的上限 10ms,幀預算 12ms 扣掉只剩 2ms,一列 4ms → 算出 0 列 → 永遠掉進
|
|
473
|
-
「可見列 ÷ 4」的下限,也就是 **3/4 視窗固定變骨架**。
|
|
474
|
-
|
|
475
|
-
**修法**:加一條前提 —— **這一個視窗畫得完嗎**。
|
|
476
|
-
`視窗列數 × 每列成本 + commit 固定成本 ≤ SHELL_ENGAGE_VIEWPORT_MS(120ms)` → 畫得完就不出殼,直接畫完。
|
|
477
|
-
60 = 我們對標的 AG Grid 每幀建列預算。**cite 換成可驗證的來源**(2026-09-12):原本引
|
|
478
|
-
`ag-grid-community.js:34143`(打包後行號),但 AG Grid **不在本 repo 的依賴裡**,那個 cite 誰都驗不了;
|
|
479
|
-
改引原始碼 https://github.com/ag-grid/ag-grid/blob/latest/packages/ag-grid-community/src/misc/animationFrameService.ts
|
|
480
|
-
逐字 `const callback = this.executeFrame.bind(this, 60)`。原本取兩倍(120),該取捨已被實測推翻;
|
|
481
|
-
取兩倍的理由是「一個視窗的內容晚 120ms 出現,比先看到一片灰色骨架再換成真資料好」—— main 在同一台機器上就是花 148ms
|
|
482
|
-
一次畫完、全程沒有佔位。
|
|
483
|
-
|
|
484
|
-
**兩個常數必須分開**(我一度合併,被實測擋下):
|
|
485
|
-
- `SHELL_ENGAGE_VIEWPORT_MS = 120` —— 要不要出殼的量尺。
|
|
486
|
-
- `SHELL_FRAME_BUDGET_MS = 12` —— 既然要出殼,一次 commit 補幾列。把它拉到 60 會讓慢機器的 commit 變長:
|
|
487
|
-
4× 節流實測長工 132 → 261ms、最長連續空白 418 → **1156ms**。撤回。
|
|
488
|
-
|
|
489
|
-
**驗證**:user 的真實 Chrome,連續三次 1000px 捲動 → **骨架 0、空列 0**,DOM 89ms 穩定(main 148ms)。
|
|
490
|
-
慢機器保證沒破:4× 節流最長連續空白中位 452ms(修前 417ms)、骨架 10 幀(修前 11)、長工 131ms(修前 131)。
|
|
491
|
-
不節流全閘綠(空白中位 52ms)。新增斷言 `--assert-max-shell-frames=0`:**畫得動的機器不准出殼**,
|
|
492
|
-
對照組(修前建置)在同一條斷言下 11 幀、必紅。
|
|
493
|
-
|
|
494
|
-
**同一時間最多一列 hover(2026-09-16;user:「捲動之後很容易會出現一個畫面同時有兩筆 row 呈現 hover 的狀態」)**
|
|
495
|
-
|
|
496
|
-
不變式有兩半:**(a) 亮著的那一列是指標底下那一列**(下一段)、**(b) 同一時間最多一列亮**。(b) 以前沒有任何一行程式負責 ——
|
|
497
|
-
它只是「`mouseover` 加 / `mouseout` 減成對出現」的副作用,而捲動會把這一對拆散:捲動中瀏覽器不會每幀重算指標在哪一列
|
|
498
|
-
(命中點落後畫面 1–4 列),`syncHoverUnderPointer` 先把標記移到新列;瀏覽器遲來的 `mouseout` 帶著**舊**索引去刪一個早就被清掉的索引
|
|
499
|
-
(空轉),剛才那一列就成了孤兒;接著遲來的 `mouseover` 只加不清 → 兩列同時亮。而且不會自己好:補正的快速略過條件只看
|
|
500
|
-
「指標底下那列有沒有被標」,孤兒出現後永遠成立(實測等 10 秒、滑到別列、把指標移出表格都不會好,只有那列被虛擬捲動回收才消失)。
|
|
501
|
-
|
|
502
|
-
**修法(根因層)**:標記只有**一個寫入者** `setHoveredRow(idx)` —— 標新列的同一個動作把其餘所有列清掉;`mouseout` 依
|
|
503
|
-
**接下來該亮哪一列**(`relatedTarget` 所在列,沒有就是 null)來清,**不依瀏覽器記得的舊索引**;補正的略過條件加上
|
|
504
|
-
「被標記的**全部**都是這一列」;拖曳啟動的清除收尾時也把 ref 對回 DOM(ref 說 A、DOM 標著 B 正是病根)。
|
|
505
|
-
連帶修掉「指標從列上移進浮層(下拉選單 / tooltip)時什麼都不清」。
|
|
506
|
-
機械閘:`scripts/data-table-hover-exclusivity-invariant.mjs` —— **手勢必須是合成器驅動的平滑捲動**
|
|
507
|
-
(CDP `Input.synthesizeScrollGesture`);滾輪的離散事件製造不出那段落差,修前的 build 在它底下是綠的 = 假綠。
|
|
508
|
-
|
|
509
|
-
**指標底下那一列永遠有 hover 反應(2026-09-11;user:「游標明明到了,table row 的反應卻要等好一陣子」)**
|
|
510
|
-
|
|
511
|
-
這條跟「捲動快不快」是兩件事,先前每一輪都只在量捲動,所以一直沒抓到。根因有兩層,都用
|
|
512
|
-
`scripts/data-table-row-under-pointer-invariant.mjs`(含會紅的對照組)實測過:
|
|
513
|
-
|
|
514
|
-
1. **殼列沒有 hover 可供性**。殼列帶 `data-row-index`,所以 hover 代理**會**把 `data-hovered` 標上去 ——
|
|
515
|
-
但它的 class 裡沒有 `data-[hovered]:bg-neutral-hover`,標了也什麼都不顯示。指標停著不動、底下那列
|
|
516
|
-
在捲動中被套殼,看到的就是整列毫無反應。4× 節流實測:殼存活 505ms,期間 6 幀完全沒有 hover 反應。
|
|
517
|
-
**修法**:殼列補上跟真列同一條 hover 底色。殼是「內容還在路上」,不是「這裡沒有列」;
|
|
518
|
-
把手與動作鈕仍不畫(那兩個要有真資料才有意義)。對齊 Linear / Jira 的 skeleton 列仍是可 hover 表面。
|
|
519
|
-
2. **捲動中瀏覽器不重新派送 hover**。整段合成手勢期間,指標底下那一列**一次 `mouseover` 都沒有收到**,
|
|
520
|
-
`data-hovered` 還留在早就捲出視窗的舊列上。CSS `:hover` 沒有這個問題(瀏覽器每幀自己算),
|
|
521
|
-
AG Grid(`.ag-row:hover`)與 MUI X 都走 CSS;本表為了跨三個捲動區同步同一「邏輯列」才用 `data-hovered` 代理,
|
|
522
|
-
代價就是得自己補上瀏覽器免費提供的那一半。**修法**:`syncHoverUnderPointer` 在每次捲動 commit 之後,
|
|
523
|
-
用最後已知的指標座標做一次 `elementFromPoint`,把 `data-hovered` 對到真正在指標底下的那一列。
|
|
524
|
-
這個 effect 本來就已經讀過 `scrollTop`(版面算過了),所以不多逼出一次版面計算。
|
|
525
|
-
**一定要連 DOM 節點一起比,不能只比 row id** —— 殼列升級成真列時換了節點,新節點身上沒有 `data-hovered`,
|
|
526
|
-
只比 id 會直接 return,留下「真列在指標底下卻沒底色」的一幀空窗(實測就是 1 幀)。
|
|
527
|
-
|
|
528
|
-
同時,`decideShell` 與預排隊都把**指標底下那一列**加進「不套殼」的例外(原本只有拖曳中 / 編輯中 / 選取格所在列)。
|
|
529
|
-
不變式是同一條:**有使用者互動在上面的列不套殼**。成本:每幀最多多畫一列。
|
|
530
|
-
|
|
531
|
-
**驗證**:`data-table-row-under-pointer-invariant.mjs` —— 指標放到中央後完全不動,跑 6,000px/s 手勢,
|
|
532
|
-
逐幀用 `elementFromPoint` 取指標底下那一列。修前 6 幀沒有 hover 反應(最後一次在 935ms),修後 **0/140 幀**。
|
|
533
|
-
對照組把殼列那條 hover class 拿掉 → 必須量到沒反應(證明閘在該紅時會紅)。
|
|
234
|
+
2026-09-05 捲軸專項稽核的四條「看起來有做、其實沒作用」(α / β / γ / δ)、機械閘 I17 / I18 / I19、評估後不改的三條,與 2026-09-03 對抗式稽核的兩容器缺陷清單(A–U;含 T / I 兩列「登記待補」)整段住在 `./data-table-known-defects.spec.md`;本檔內凡寫「見缺陷 X」「見六之二之零」都指那份清單。
|
|
534
235
|
|
|
535
236
|
### 七、Column Type
|
|
536
237
|
|
|
@@ -545,7 +246,7 @@ select/multiSelect 的 `meta.options` 消費 Select 的完整 `SelectOption` sch
|
|
|
545
246
|
### 八、Row 狀態
|
|
546
247
|
|
|
547
248
|
- **不使用斑馬紋**——hover 狀態已足夠區分行,斑馬紋疊加會產生多種背景色組合,增加視覺雜訊
|
|
548
|
-
- **選取狀態僅由 row 內的 selection control(`multi`→Checkbox / `single`→Radio)呈現,不另加 selected-row 底色**——避免「勾選框 + row 底色」雙重冗餘指示(2026-05-31 user 決策:有 checkbox 就只用 checkbox 呈現狀態);hover 用 neutral-hover,與 selection
|
|
249
|
+
- **選取狀態僅由 row 內的 selection control(`multi`→Checkbox / `single`→Radio)呈現,不另加 selected-row 底色**——避免「勾選框 + row 底色」雙重冗餘指示(2026-05-31 user 決策:有 checkbox 就只用 checkbox 呈現狀態);hover 用 neutral-hover,與 selection 正交(純表示「正在看的」)。`spreadsheetMode` 的區間格被滑過時維持原色(滑過色只畫在該列的非區間格上),規則見「試算表模式」段。**指標在列或表頭裡的小按鈕上時(巢狀滑過)**:規則與 user 原話只住在 `../../tokens/color/color.spec.md`「Hover 換色配對總則」巢狀滑過段 —— 宿主保留自己的 hover,按鈕自己的滑過色疊在上面(沿同一把灰階往上一階):列上的動作鈕 / 巢狀展開鈕 / portal 出去的**列拖曳把手**時整列維持 `data-hovered` 底色;表頭 ⌄ 欄位選單上時排序區維持 `foreground` 字色(排序箭頭跟著),指到欄寬把手則不算。套到表格是 AI 推導,與 2026-09-04 user 回報「hover inline action 後整列底色消失」被當 bug 修掉同方向。列把手帶**專用**的 `data-hover-row-index` 讓 hover 代理認得它屬於哪一列(不重用 `data-row-index`,那個屬性的其他讀者只該找到列本身);⌄ 外層帶 `data-col-menu` 供排序區的兄弟選擇器認。
|
|
549
250
|
|
|
550
251
|
### 九、Row Actions
|
|
551
252
|
|
|
@@ -648,7 +349,12 @@ preserveSelectionOnFilter?: boolean // default false
|
|
|
648
349
|
- click checkbox → toggle 該 row
|
|
649
350
|
- **shift-click checkbox** → 從 anchor row 到當前 row 區間選(內部 track anchor)
|
|
650
351
|
- header checkbox click → toggle 全可見
|
|
651
|
-
- **整 cell 區可點擊**(canonical)
|
|
352
|
+
- **整 cell 區可點擊**(canonical):選取格的 padding 任何位置(不只視覺 checkbox 本體)點擊都觸發 toggle / select,表頭全選格同理。Disabled row 不觸發。**表頭全選格只在 `mode="multi"` 可點**:`single` 模式沒有「全選」這回事(RadioGroup 一次只能選一列),表頭選取格與「可選的列為 0」時一樣停用(`data-table.tsx` `isHeaderDisabled = selectableVisibleIds.length === 0 || mode !== 'multi'`;2026-09-27 補寫,此前只在程式裡)。實作:select cell 容器 div 的 `onClick` 委派到 `toggleRow` / `setSelection`,內部 checkbox / radio 用 `stopPropagation` 避免重複觸發。
|
|
353
|
+
- **理由已於 2026-09-24 換掉。** 舊理由寫的是「擴大 hit target 且不要求精準瞄準」—— 那是觸控論述,而 user 2026-09-24 裁示本 DS 以滑鼠指標的精度為前提,不拿觸控尺寸建議當依據。同日我一度依據另一條規則把 `onClick` 拿掉,**也是錯的,當天改回來**。
|
|
354
|
+
- **現行理由是世界級一手對照**:AG Grid / MUI X Data Grid / react-data-grid / Glide Data Grid 四家的 cell 都是點擊目標,而且**四家沒有任何一家讓選取格的空白處變成死區** —— AG Grid 聚焦該 cell(原始碼註解逐字 "we need to make sure the cell wrapping that checkbox is focused")、MUI X 該 cell 出現 focus outline、react-data-grid 該 cell 變 active cell、Glide 直接選列(整格無命中測試)。
|
|
355
|
+
- **「命中區 = 懸停回饋形狀」那條規則不適用於表格的格**:三家是「hover 畫在列、點擊目標卻是格」,形狀本來就不一致。那條規則的成立範圍是**控件層**(按鈕、行內動作),owner 與撤回紀錄見 `ds-canonical/references/hit-area-canonical.md`「適用範圍」節。
|
|
356
|
+
- **也不依「有沒有畫垂直格線」分流**:查無一手依據。真正切的那一刀是 `cellSelection` 這類 feature flag —— AG Grid 的 `columnBorder` 預設是透明色,同一份 DOM、同一份 JS,只差上不上色。
|
|
357
|
+
- **選取欄在格線模式下有自己的欄間線**(user 2026-09-24 拍板補回來)。它曾有過專用規則,2026-05-12 退役時說好「走 inlineEdit canonical」,但 tsx 的選取欄分支在套上 `dtCellGrid` 之前就 early-return —— 舊線拿掉、新線沒接到,**兩頭落空**。實測全表 325 個格有格線、選取格是唯一沒有的那一個,勾選框與第一個資料欄視覺上併成同一個盒。現在列身與表頭都套 `dtCellGrid`,本區最後一欄仍不畫(`data-dt-last-col`,凍結邊界線 / 外框接管)。**這條跟可點範圍無關** —— 上一條已明記「有格線 → 整格可點」查無一手依據;補線是視覺 bug 修復,兩件事不綁在一起。
|
|
652
358
|
|
|
653
359
|
### 五、Disabled rows
|
|
654
360
|
|
|
@@ -812,6 +518,20 @@ ValueShape ↔ DS picker 對照(canonical 2026-05-02):
|
|
|
812
518
|
3. **唯一 edit affordance = hover Pencil**(xs iconOnly tertiary,`opacity-0 group-hover/cell:opacity-100`,onClick `stopPropagation` + `onRequestEdit`)— 對齊 AG Grid 官方三件套(`suppressClickEdit` + 「including a button in your cell renderer」+ `startEditingCell()`,https://www.ag-grid.com/react-data-grid/cell-editing-start-stop/)+ Jira hover-pencil + Notion title cell hover-OPEN 鏡像;Atlaskit target-tag 分流(點 `<a>` 導航、點空白區編輯)為次選已評估不採(與 hover-outline 統一 affordance 衝突)
|
|
813
519
|
4. **edit 觸發後 = 一般 field 行為契約**:Pencil 只負責「進入 edit」一步;進入後 autoFocus 進輸入、Esc 取消、Enter/blur commit、驗證時機、focus 樣式全部回歸 field-controls edit-mode canonical,與其他型別 cell edit 零特例。若 edit 態控件與 form 場景不同(url 用 plain `<Input>` 非 LinkInput — LinkInput edit 預設顯 link 態,cell 需直接輸入),**必在上表該行寫明 documented 例外 + rationale**,禁只留 code 註解
|
|
814
520
|
|
|
521
|
+
### 試算表模式(`spreadsheetMode`):點格、焦點框、區間色(2026-09-26)
|
|
522
|
+
|
|
523
|
+
**來源**:user 2026-09-26 同意三條改法(待辦總帳 `governance/planning/2026-09-25-interaction-and-hover-remediation.md` X35 / L7)。三條都只作用在 `spreadsheetMode`;預設模式與勾選列模式不變。
|
|
524
|
+
|
|
525
|
+
| 情境 | 規則 | 依據 |
|
|
526
|
+
|---|---|---|
|
|
527
|
+
| **點任何格** | 格游標(藍框)移到被點的那一格 —— **唯讀格、開關格與連結格的空白處都一樣**。Shift+點 = 從起點延伸區間(藍框留在起點);再點一次已選的格 = 進編輯,**只限**「點格即編輯」的格,唯讀 / boolean / url 沒有可進的編輯,游標留在原格。格內自己處理點擊的控件照舊:勾選框切換值(commit 後游標本來就回到該格)、連結開連結、鉛筆鈕進編輯。**滑過樣式不變**:淺框提示與手形游標仍只給點格即編輯的格 | W3C APG grid 原文「In a grid, every cell contains a focusable element or is itself focusable, regardless of whether the cell content is editable or interactive.」([grid-pattern.html#L74](https://github.com/w3c/aria-practices/blob/3f094fde1c81b25dfa69162563bf28d093f854d4/content/patterns/grid/grid-pattern.html#L74));AG Grid 36.2.0 按下即 `focusCell`,不看能不能編輯([cellMouseListenerFeature.ts#L183-L196](https://github.com/ag-grid/ag-grid/blob/0fee5b7b1e839ae23fe860e404042448f3c1375d/packages/ag-grid-community/src/rendering/cell/cellMouseListenerFeature.ts#L183-L196));MUI X 9.14.0 放開時 `setCellFocus`([useGridFocus.ts#L390-L427](https://github.com/mui/mui-x/blob/c83b3dd6996f6913947b1be3e5d47655153ced60/packages/x-data-grid/src/hooks/features/focus/useGridFocus.ts#L390-L427));Handsontable 唯讀格「still allow navigation and copying of data」([read-only cells](https://handsontable.com/docs/javascript-data-grid/read-only-cells/))。DS 內部:方向鍵本來就走得到唯讀格(可走的欄只排除勾選欄),修前「滑鼠點不到、鍵盤走得到」自相矛盾 |
|
|
528
|
+
| **根節點與格游標只畫一個框** | 有格游標時,焦點框 = 那一格的 DS 焦點框(`focus-ring-inset`,2px `--ring` 往內),表格根節點**不畫**;此刻沒有格游標(例:按 Esc 清掉之後)才由根節點畫同一種內描邊。勾選列模式沒有格游標,仍由根節點畫。滑鼠點格與鍵盤移動看到的是同一個框;焦點離開格線區時格游標框收起(見表下第一條) | `ds-canonical/references/focus-canonical.md` A 類把「DataTable 根」列為「框畫在被指到的那一項上、容器抑制瀏覽器預設外框」,「框怎麼畫」只准三種幾何(原本的 1px `--primary` 是第四種,而且與根節點 2px 框同時出現 = 兩個焦點指示)。世界級(R21 實測)都只在格上畫、容器不畫:AG Grid「cells use a border only to indicate focus」([_general.css#L409-L420](https://github.com/ag-grid/ag-grid/blob/0fee5b7b1e839ae23fe860e404042448f3c1375d/packages/ag-grid-community/src/theming/core/css/_general.css#L409-L420))、MUI X 根 `outline: 'none'`([GridRootStyles.ts#L184](https://github.com/mui/mui-x/blob/c83b3dd6996f6913947b1be3e5d47655153ced60/packages/x-data-grid/src/components/containers/GridRootStyles.ts#L184))、APG `[role="gridcell"]:focus` 點線框([dataGrids.css#L72-L78](https://github.com/w3c/aria-practices/blob/3f094fde1c81b25dfa69162563bf28d093f854d4/content/patterns/grid/examples/css/dataGrids.css#L72-L78))。**框的粗細不是世界級共識**(AG Grid 1px `border: 1px solid`([_grid-layout.css#L281-L291](https://github.com/ag-grid/ag-grid/blob/0fee5b7b1e839ae23fe860e404042448f3c1375d/packages/ag-grid-community/src/theming/core/css/_grid-layout.css#L281-L291))、APG 3px 點線(同上 dataGrids.css))—— 2px 取自 DS 自己的焦點框幾何 |
|
|
529
|
+
| **區間格 × 列被滑過** | 區間格(`--primary-subtle`)在它那一列被滑過時**維持原色,淺深一致**;同一列的非區間格照常變 `--neutral-hover`,顏色與沒有區間的列相同。做法:含區間格的列被滑過時,滑過色不畫在列上、改畫在該列的非區間格上(`data-table.css`)。修前淺色不變(`--primary-subtle` 不透明)、深色 #1C304A → #243851(alpha 公式讓列的滑過色透上來) | 不新增 token(`../../tokens/README.md`「找不到現有 family 可鏡射 → 先質疑是否真需要」)、彩色底不疊層(`../../tokens/color/color.spec.md`「疊層只用在「底」」)、DS 內 `--primary-subtle` 被滑過從不換底(按下的 Button 只換字色)。世界級**兩派都有,不是共識**:MUI X 同列別格被滑過時選中格不變([GridRootStyles.ts#L142-L151](https://github.com/mui/mui-x/blob/c83b3dd6996f6913947b1be3e5d47655153ced60/packages/x-data-grid/src/components/containers/GridRootStyles.ts#L142-L151)),AG Grid 讓列滑過透進區間([_grid-layout.css#L375-L392](https://github.com/ag-grid/ag-grid/blob/0fee5b7b1e839ae23fe860e404042448f3c1375d/packages/ag-grid-community/src/theming/core/css/_grid-layout.css#L375-L392))。本段依上面三條 DS 既有規則選前者 |
|
|
530
|
+
|
|
531
|
+
- **焦點離開格線區時格游標框收起**(2026-09-26,**AI 依 (b) 推導 —— (b)「只留一個焦點框」為 AI 建議、09-26 user 未另提 → AI 判讀照建議做,方向來自 user 原話「一個藍色focus ring 就已經夠顯眼了」**):格游標框只在焦點在格線區裡時畫 —— 表格根節點、某一格裡(含格內控件,例:勾選框)、或浮層編輯器。焦點 Tab 到表頭控件(排序區、⌄ 欄位選單)或離開表格時,框收起、**游標位置保留**,Tab / Shift+Tab 回到表格時原格重現;區間底色不受影響(選取不是焦點)。否則那一刻是「表頭控件的焦點框 + 格游標框」兩個一模一樣的藍框。對齊資料表格派:格上的框只在格(或格內)有焦點時畫 —— AG Grid `.ag-cell-focus…:focus-within`([_grid-layout.css#L281-L291](https://github.com/ag-grid/ag-grid/blob/0fee5b7b1e839ae23fe860e404042448f3c1375d/packages/ag-grid-community/src/theming/core/css/_grid-layout.css#L281-L291))、MUI X `& .cell:focus`([GridRootStyles.ts#L266-L269](https://github.com/mui/mui-x/blob/c83b3dd6996f6913947b1be3e5d47655153ced60/packages/x-data-grid/src/components/containers/GridRootStyles.ts#L266-L269))、W3C APG `[role="gridcell"]:focus`([dataGrids.css#L72-L78](https://github.com/w3c/aria-practices/blob/3f094fde1c81b25dfa69162563bf28d093f854d4/content/patterns/grid/examples/css/dataGrids.css#L72-L78))。試算表產品(Excel / Google Sheets)作用格常駐的做法未驗證,不採。
|
|
532
|
+
- **Esc 行為不變**:未編輯時 Esc 清掉格游標與區間;焦點留在表格上,焦點框改由根節點畫(上表第二列)。
|
|
533
|
+
- **機械閘**:`scripts/data-table-invariants.mjs` I31(區間格兩主題釘住)/ I32(根節點與格游標框不同時出現、格游標 = DS 焦點框、Esc 後由根節點接手、Tab 到表頭控件時只剩那個控件的框、Shift+Tab 回來原格重現)/ I33(點唯讀格、開關格空白、連結格空白都移動格游標,且開關值不變、不開連結)。
|
|
534
|
+
|
|
815
535
|
### Nested rows — forward TanStack
|
|
816
536
|
|
|
817
537
|
```tsx
|
|
@@ -869,7 +589,7 @@ Row drag + column reorder + TreeView 共用 `lib/drag-visual.ts`:source `opacity
|
|
|
869
589
|
|
|
870
590
|
## Overlay + cell error SSOT(Phase 9)
|
|
871
591
|
|
|
872
|
-
**Overlay**:viewport `position:fixed inset:0` layer。`getCellRect()` 從 `getBoundingClientRect()` 取 float coords no rounding。Paint:hover
|
|
592
|
+
**Overlay**:viewport `position:fixed inset:0` layer。`getCellRect()` 從 `getBoundingClientRect()` 取 float coords no rounding。Paint:hover ring 1px `--border-hover` `outline outline-offset:-1px` in-place;selected ring(格游標)= DS 焦點框 `focus-ring-inset`(2px `--ring` 往內,只掛 class 不寫值,2026-09-26 起;見「試算表模式」段)(range outer ring 已 2026-05-10 retire — range 視覺只剩 cell-bg `--primary-subtle` `[data-range-cell]`,bg 已足以標示範圍、外框冗餘);active editor host portal opaque `<div>` z 3(cell 保持 view 態)。**Viewport clip**(Issue 6):body panel 加 `data-datatable-panel="left|center|right"`;`getCellGeometry()` return cell+panel rect;`<ClipMask>` panel rect `overflow:hidden`,內部 `toRelRect()` 轉 mask-relative(hover/selected ring 按 panel clip,不畫出 pin boundary)。Active editor host **不 clip**,因為 editor 必能越過 cell paint layer 接收互動。
|
|
873
593
|
|
|
874
594
|
**Cell errors**(Issue 9):`cellErrors?: Record<string, string|string[]>` prop key `${rowId}:${colId}`。Cell view 態渲 error 14px `text-error` 下方 gap-1;array→`<ul><li>`;single→`<span>`。`aria-describedby` + `aria-invalid` + `<span role="alert">`。`overflow:visible` 當有 error(搭 `autoRowHeight`)。**Per-row state SSOT** cell-render wrapper(`items-X` 等)必 consume `effectiveAutoRowForCell`,禁 global `autoRowHeight`(audit `audit-data-table-row-mode-ssot.mjs` 強制)。**Edit-clears-own-cell** 自動清視覺,consumer onCellCommit validate 後回填。**a11y caveat**:≥ 5 同時 `role="alert"` 第一次 paint AT 噪音 → consumer 可考 `role="status"` fallback,避免初次 paint 同時打斷多次。
|
|
875
595
|
|
|
@@ -934,14 +654,16 @@ DataTable 是 composite multi-section 元件,**不套 SizeMatrix / StateBehavior
|
|
|
934
654
|
|
|
935
655
|
**Keyboard 行為**(目前實作 — `tableKeyboardHandler`):
|
|
936
656
|
- ↑↓←→:cell-to-cell navigation **僅 `spreadsheetMode` opt-in 時生效**;selection 尚未建立時按方向鍵自動選取第一個 visible cell(鍵盤可直接進入 spreadsheet 導覽,無需滑鼠 click — 對齊 Excel / Google Sheets / AG Grid「focus grid → first cell active」,2026-07-05 D4 補);預設模式方向鍵無作用
|
|
937
|
-
-
|
|
657
|
+
- Shift+↑↓←→(2026-09-29,待辦總帳 N46;WCAG 2.1.1「Shift+點擊做得到的,鍵盤也要做得到」):從格游標(起點)往那個方向**擴大 / 縮小區間**——起點與藍框不動(同 Shift+點擊:起點永遠是 `selectedCellId`),動的是終點;第二下起從上一次的終點繼續;放開 Shift 再按方向鍵 = 一般移動、區間重設為單格。一手:AG Grid "Focusing a cell and then holding down ⇧ Shift and using the arrow keys will create a range starting from the focused cell."(<https://www.ag-grid.com/javascript-data-grid/cell-selection/>);MUI X "Use the arrow keys to focus on a cell, then hold Shift and navigate to another cell—if Shift is released and pressed again then the selection will restart from the last focused cell."(<https://mui.com/x/react-data-grid/cell-selection/>;本 DS 重設回**起點**而非「上一次的終點」,對齊本檔既有 Shift+點擊 canonical)。2026-09-29 前方向鍵一律重設起點、Shift 被忽略
|
|
658
|
+
- Enter / F2:spreadsheet 模式下進 cell editing(cell 可編輯 + 非 boolean/url 時);**Enter 確認後維持原格不下移**(2026-07-05 user 拍板;10 家實查:Excel 系 7 家下移、AG Grid 預設維持原格 — 採 AG Grid 派,數據 → `.claude/logs/deep-audit-2026-07-03/enter-commit-navigation-benchmark.json`;未來連續輸入需求可重議 opt-in);**edit 退出(commit / Esc)後 selection 還原至該 cell、焦點還給 table root**(editor unmount 後焦點掉到 body 才收回,不搶 user 點擊的新焦點 — 對齊 spreadsheet RFC Contract 11 + Excel / AG Grid,2026-07-05 D4 補);**編輯中再按 `F2` = 結算、回到格導覽**(2026-09-29,待辦總帳 N46;跨元件規則 `ds-canonical/references/keyboard-model-canonical.md`「`F2` 恆為進到格裡的控件,再按一次回到格導覽」,APG Grid 逐字 "A subsequent press of F2 restores grid navigation functions.";值留著 = commit,因為 APG 只把「還原」寫在 Escape 那一條,本元件的 Esc 已是取消編輯;只有文字型編輯器(string / number / url)吃這個鍵,浮層型(date / select / person)的出口仍是選值或點外面;實作 = `../Field/field-edit-keys.ts` `commitOnF2`,`cell-registry.tsx` makeKeyHandler 傳 true)
|
|
659
|
+
- **兩個鍵都給的理由是跨元件規則**(owner → `ds-canonical/references/keyboard-model-canonical.md`「進格用什麼鍵」):**`F2` 恆為進格;`Enter` 在該格的主要動作沒有佔走它時,也是進格**。本元件的檢視態儲存格沒有主要動作,所以兩個都給;`Calendar` 日期鈕的 `Enter` 被「選這一天」佔走,所以只給 `F2`。新元件照這條判,不要再逐案挑鍵。
|
|
938
660
|
- Cmd/Ctrl+A:`mode="multi"` selection 時選全可見列(扣 disabled)
|
|
939
|
-
- Esc:取消 editing(spreadsheet)/ 清 selection(selection mode);**IME 組字中的 Enter / Esc 不觸發 commit / cancel**(cell editor 帶 `isComposing` guard,2026-07-05 D4 補 — 中文選字 Enter 不誤提交半截組字)
|
|
661
|
+
- Esc:取消 editing(spreadsheet)/ 未在編輯時清格游標與區間(spreadsheet;焦點框改由表格根節點畫)/ 清 selection(selection mode);**IME 組字中的 Enter / Esc 不觸發 commit / cancel**(cell editor 帶 `isComposing` guard,2026-07-05 D4 補 — 中文選字 Enter 不誤提交半截組字)
|
|
940
662
|
- Tab:進入表格後操作排序與勾選;portal edit(`experimentalActiveEditorController`)中 Tab / Shift+Tab = commit 當前 draft + 移至下一個 editable cell 進 edit(2026-07-05 D4 補 commit — 原本換格丟 draft)
|
|
941
663
|
|
|
942
664
|
> APG grid full keyboard model(Home/End、Ctrl+Home/End、PageUp/PageDown、roving cell action)為 `role="grid"` future tier 目標,**尚未實作**。
|
|
943
665
|
|
|
944
|
-
**Focus**:table root `tabIndex=0` **僅在 selection enabled 或 `spreadsheetMode` 時**(否則 `undefined` = 不可 focus);cell 目前無 roving `tabindex=-1`
|
|
666
|
+
**Focus**:table root `tabIndex=0` **僅在 selection enabled 或 `spreadsheetMode` 時**(否則 `undefined` = 不可 focus);cell 目前無 roving `tabindex=-1` 機制。**根節點與格游標框不同時出現**(2026-09-26):`spreadsheetMode` 有格游標時畫在那一格(`focus-ring-inset`,只在焦點在格線區裡時畫;Tab 到表頭控件時收起、位置保留),根節點不畫;沒有格游標(Esc 之後)與勾選列模式由根節點畫 `focus-visible:focus-ring-inset`(規則與依據見「試算表模式」段)。互動元素(勾選框 / 排序 header / 展開鈕 / row action)各自 focusable + focus-visible ring(`outline: 2px solid var(--ring)`)。APG grid roving-tabindex focus model 為 future tier 目標,尚未實作 → 見 [WAI APG keyboard model](https://www.w3.org/WAI/ARIA/apg/patterns/grid/#keyboardinteraction)。
|
|
945
667
|
|
|
946
668
|
**驗證**:Storybook a11y addon panel 應 0 critical violation;鍵盤完整可操作(無需滑鼠)。WCAG AA contrast ≥ 4.5:1(text)/ 3:1(UI)。
|
|
947
669
|
|
|
@@ -952,6 +674,8 @@ DataTable 是 composite multi-section 元件,**不套 SizeMatrix / StateBehavior
|
|
|
952
674
|
- `bulk-action-bar.spec.md`
|
|
953
675
|
- `carousel.spec.md`
|
|
954
676
|
- `circular-progress.spec.md`
|
|
677
|
+
- `data-table-known-defects.spec.md`
|
|
678
|
+
- `data-table-scroll-performance.spec.md`
|
|
955
679
|
- `description-list.spec.md`
|
|
956
680
|
- `filter-operators.spec.md`
|
|
957
681
|
- `opacity.spec.md`
|