@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.
Files changed (576) hide show
  1. package/dist/components/Accordion/accordion.d.ts +1 -1
  2. package/dist/components/Accordion/accordion.d.ts.map +1 -1
  3. package/dist/components/Accordion/accordion.js +8 -3
  4. package/dist/components/Accordion/accordion.js.map +1 -1
  5. package/dist/components/AccountMenu/account-menu.d.ts.map +1 -1
  6. package/dist/components/AccountMenu/account-menu.js.map +1 -1
  7. package/dist/components/AgentPanel/agent-panel-fab.d.ts +7 -1
  8. package/dist/components/AgentPanel/agent-panel-fab.d.ts.map +1 -1
  9. package/dist/components/AgentPanel/agent-panel-fab.js +36 -14
  10. package/dist/components/AgentPanel/agent-panel-fab.js.map +1 -1
  11. package/dist/components/AgentPanel/agent-panel-logo.d.ts +3 -3
  12. package/dist/components/AgentPanel/agent-panel-logo.js.map +1 -1
  13. package/dist/components/AgentPanel/agent-panel.d.ts +1 -1
  14. package/dist/components/AgentPanel/agent-panel.d.ts.map +1 -1
  15. package/dist/components/AgentPanel/agent-panel.js +28 -22
  16. package/dist/components/AgentPanel/agent-panel.js.map +1 -1
  17. package/dist/components/AppShell/app-shell.d.ts +1 -1
  18. package/dist/components/AppShell/app-shell.js +1 -1
  19. package/dist/components/AppShell/app-shell.js.map +1 -1
  20. package/dist/components/Avatar/avatar.d.ts +95 -1
  21. package/dist/components/Avatar/avatar.d.ts.map +1 -1
  22. package/dist/components/Avatar/avatar.js +106 -15
  23. package/dist/components/Avatar/avatar.js.map +1 -1
  24. package/dist/components/Avatar/index.js +14 -2
  25. package/dist/components/Breadcrumb/breadcrumb.d.ts +4 -2
  26. package/dist/components/Breadcrumb/breadcrumb.d.ts.map +1 -1
  27. package/dist/components/Breadcrumb/breadcrumb.js +2 -2
  28. package/dist/components/Breadcrumb/breadcrumb.js.map +1 -1
  29. package/dist/components/BulkActionBar/bulk-action-bar.d.ts.map +1 -1
  30. package/dist/components/BulkActionBar/bulk-action-bar.js +27 -2
  31. package/dist/components/BulkActionBar/bulk-action-bar.js.map +1 -1
  32. package/dist/components/Button/button.d.ts +2 -2
  33. package/dist/components/Button/button.d.ts.map +1 -1
  34. package/dist/components/Button/button.js +118 -22
  35. package/dist/components/Button/button.js.map +1 -1
  36. package/dist/components/Calendar/calendar.d.ts +34 -10
  37. package/dist/components/Calendar/calendar.d.ts.map +1 -1
  38. package/dist/components/Calendar/calendar.js +210 -36
  39. package/dist/components/Calendar/calendar.js.map +1 -1
  40. package/dist/components/Carousel/carousel.d.ts +1 -1
  41. package/dist/components/Carousel/carousel.d.ts.map +1 -1
  42. package/dist/components/Carousel/carousel.js +3 -2
  43. package/dist/components/Carousel/carousel.js.map +1 -1
  44. package/dist/components/Chart/chart.d.ts +1 -0
  45. package/dist/components/Chart/chart.d.ts.map +1 -1
  46. package/dist/components/Chart/chart.js +8 -0
  47. package/dist/components/Chart/chart.js.map +1 -1
  48. package/dist/components/Checkbox/checkbox.d.ts +1 -1
  49. package/dist/components/Checkbox/checkbox.d.ts.map +1 -1
  50. package/dist/components/Checkbox/checkbox.js +10 -2
  51. package/dist/components/Checkbox/checkbox.js.map +1 -1
  52. package/dist/components/Chip/chip.d.ts +1 -1
  53. package/dist/components/Chip/chip.d.ts.map +1 -1
  54. package/dist/components/Chip/chip.js +4 -2
  55. package/dist/components/Chip/chip.js.map +1 -1
  56. package/dist/components/Combobox/combobox.d.ts +1 -1
  57. package/dist/components/Combobox/combobox.d.ts.map +1 -1
  58. package/dist/components/Combobox/combobox.js +37 -11
  59. package/dist/components/Combobox/combobox.js.map +1 -1
  60. package/dist/components/Command/command.d.ts +1 -1
  61. package/dist/components/Command/command.d.ts.map +1 -1
  62. package/dist/components/Command/command.js +106 -8
  63. package/dist/components/Command/command.js.map +1 -1
  64. package/dist/components/DataTable/cell-registry.d.ts.map +1 -1
  65. package/dist/components/DataTable/cell-registry.js +3 -2
  66. package/dist/components/DataTable/cell-registry.js.map +1 -1
  67. package/dist/components/DataTable/data-table-column-visibility-panel.d.ts.map +1 -1
  68. package/dist/components/DataTable/data-table-column-visibility-panel.js +24 -24
  69. package/dist/components/DataTable/data-table-column-visibility-panel.js.map +1 -1
  70. package/dist/components/DataTable/data-table-interaction-layer.d.ts +2 -1
  71. package/dist/components/DataTable/data-table-interaction-layer.d.ts.map +1 -1
  72. package/dist/components/DataTable/data-table-interaction-layer.js +3 -4
  73. package/dist/components/DataTable/data-table-interaction-layer.js.map +1 -1
  74. package/dist/components/DataTable/data-table-sort-manager.d.ts.map +1 -1
  75. package/dist/components/DataTable/data-table-sort-manager.js +20 -20
  76. package/dist/components/DataTable/data-table-sort-manager.js.map +1 -1
  77. package/dist/components/DataTable/data-table.d.ts +8 -5
  78. package/dist/components/DataTable/data-table.d.ts.map +1 -1
  79. package/dist/components/DataTable/data-table.js +197 -71
  80. package/dist/components/DataTable/data-table.js.map +1 -1
  81. package/dist/components/DateGrid/date-grid.d.ts +1 -1
  82. package/dist/components/DateGrid/date-grid.d.ts.map +1 -1
  83. package/dist/components/DateGrid/date-grid.js +358 -225
  84. package/dist/components/DateGrid/date-grid.js.map +1 -1
  85. package/dist/components/DatePicker/date-picker.d.ts.map +1 -1
  86. package/dist/components/DatePicker/date-picker.js +16 -1
  87. package/dist/components/DatePicker/date-picker.js.map +1 -1
  88. package/dist/components/Dialog/dialog.d.ts.map +1 -1
  89. package/dist/components/Dialog/dialog.js +55 -43
  90. package/dist/components/Dialog/dialog.js.map +1 -1
  91. package/dist/components/DropdownMenu/dropdown-menu-keyboard.d.ts +86 -0
  92. package/dist/components/DropdownMenu/dropdown-menu-keyboard.d.ts.map +1 -0
  93. package/dist/components/DropdownMenu/dropdown-menu-keyboard.js +172 -0
  94. package/dist/components/DropdownMenu/dropdown-menu-keyboard.js.map +1 -0
  95. package/dist/components/DropdownMenu/dropdown-menu.d.ts +6 -1
  96. package/dist/components/DropdownMenu/dropdown-menu.d.ts.map +1 -1
  97. package/dist/components/DropdownMenu/dropdown-menu.js +50 -39
  98. package/dist/components/DropdownMenu/dropdown-menu.js.map +1 -1
  99. package/dist/components/Field/field-context.d.ts +4 -0
  100. package/dist/components/Field/field-context.d.ts.map +1 -1
  101. package/dist/components/Field/field-context.js +1 -2
  102. package/dist/components/Field/field-context.js.map +1 -1
  103. package/dist/components/Field/field-edit-keys.d.ts +8 -0
  104. package/dist/components/Field/field-edit-keys.d.ts.map +1 -1
  105. package/dist/components/Field/field-edit-keys.js +3 -0
  106. package/dist/components/Field/field-edit-keys.js.map +1 -1
  107. package/dist/components/Field/field-wrapper.d.ts +18 -1
  108. package/dist/components/Field/field-wrapper.d.ts.map +1 -1
  109. package/dist/components/Field/field-wrapper.js +39 -10
  110. package/dist/components/Field/field-wrapper.js.map +1 -1
  111. package/dist/components/Field/field.d.ts +3 -2
  112. package/dist/components/Field/field.d.ts.map +1 -1
  113. package/dist/components/Field/field.js +2 -1
  114. package/dist/components/Field/field.js.map +1 -1
  115. package/dist/components/FileItem/file-item.d.ts +26 -2
  116. package/dist/components/FileItem/file-item.d.ts.map +1 -1
  117. package/dist/components/FileItem/file-item.js +82 -25
  118. package/dist/components/FileItem/file-item.js.map +1 -1
  119. package/dist/components/FileItem/index.js +4 -2
  120. package/dist/components/FileUpload/file-upload.d.ts +1 -1
  121. package/dist/components/FileUpload/file-upload.d.ts.map +1 -1
  122. package/dist/components/FileUpload/file-upload.js +132 -55
  123. package/dist/components/FileUpload/file-upload.js.map +1 -1
  124. package/dist/components/FileViewer/file-viewer.d.ts +1 -1
  125. package/dist/components/FileViewer/file-viewer.d.ts.map +1 -1
  126. package/dist/components/FileViewer/file-viewer.js +9 -4
  127. package/dist/components/FileViewer/file-viewer.js.map +1 -1
  128. package/dist/components/InlineEdit/inline-edit.d.ts.map +1 -1
  129. package/dist/components/InlineEdit/inline-edit.js +1 -1
  130. package/dist/components/InlineEdit/inline-edit.js.map +1 -1
  131. package/dist/components/Input/input.d.ts.map +1 -1
  132. package/dist/components/Input/input.js +6 -1
  133. package/dist/components/Input/input.js.map +1 -1
  134. package/dist/components/LinkInput/link-input.d.ts.map +1 -1
  135. package/dist/components/LinkInput/link-input.js +20 -4
  136. package/dist/components/LinkInput/link-input.js.map +1 -1
  137. package/dist/components/Notice/notice.js +1 -1
  138. package/dist/components/Notice/notice.js.map +1 -1
  139. package/dist/components/NumberInput/number-input.d.ts.map +1 -1
  140. package/dist/components/NumberInput/number-input.js +6 -1
  141. package/dist/components/NumberInput/number-input.js.map +1 -1
  142. package/dist/components/OverflowIndicator/index.js +2 -1
  143. package/dist/components/OverflowIndicator/overflow-indicator.d.ts +19 -1
  144. package/dist/components/OverflowIndicator/overflow-indicator.d.ts.map +1 -1
  145. package/dist/components/OverflowIndicator/overflow-indicator.js +47 -27
  146. package/dist/components/OverflowIndicator/overflow-indicator.js.map +1 -1
  147. package/dist/components/Pagination/pagination.d.ts +1 -1
  148. package/dist/components/Pagination/pagination.js +1 -1
  149. package/dist/components/Pagination/pagination.js.map +1 -1
  150. package/dist/components/PeoplePicker/avatar-stack-overflow.d.ts +7 -6
  151. package/dist/components/PeoplePicker/avatar-stack-overflow.d.ts.map +1 -1
  152. package/dist/components/PeoplePicker/avatar-stack-overflow.js +6 -7
  153. package/dist/components/PeoplePicker/avatar-stack-overflow.js.map +1 -1
  154. package/dist/components/PeoplePicker/people-picker-helpers.d.ts.map +1 -1
  155. package/dist/components/PeoplePicker/people-picker-helpers.js +1 -1
  156. package/dist/components/PeoplePicker/people-picker-helpers.js.map +1 -1
  157. package/dist/components/PeoplePicker/people-picker.d.ts.map +1 -1
  158. package/dist/components/PeoplePicker/people-picker.js +2 -2
  159. package/dist/components/PeoplePicker/people-picker.js.map +1 -1
  160. package/dist/components/PeoplePicker/person-display.d.ts.map +1 -1
  161. package/dist/components/PeoplePicker/person-display.js +26 -28
  162. package/dist/components/PeoplePicker/person-display.js.map +1 -1
  163. package/dist/components/ProfileCard/profile-card.d.ts.map +1 -1
  164. package/dist/components/ProfileCard/profile-card.js.map +1 -1
  165. package/dist/components/RadioGroup/radio-group.d.ts +1 -1
  166. package/dist/components/RadioGroup/radio-group.d.ts.map +1 -1
  167. package/dist/components/RadioGroup/radio-group.js +10 -4
  168. package/dist/components/RadioGroup/radio-group.js.map +1 -1
  169. package/dist/components/Rating/rating.d.ts +28 -6
  170. package/dist/components/Rating/rating.d.ts.map +1 -1
  171. package/dist/components/Rating/rating.js +97 -80
  172. package/dist/components/Rating/rating.js.map +1 -1
  173. package/dist/components/ScrollArea/scroll-area.d.ts +1 -1
  174. package/dist/components/ScrollArea/scroll-area.js +1 -1
  175. package/dist/components/ScrollArea/scroll-area.js.map +1 -1
  176. package/dist/components/SegmentedControl/segmented-control.d.ts +1 -1
  177. package/dist/components/SegmentedControl/segmented-control.d.ts.map +1 -1
  178. package/dist/components/SegmentedControl/segmented-control.js +3 -2
  179. package/dist/components/SegmentedControl/segmented-control.js.map +1 -1
  180. package/dist/components/Select/select.d.ts.map +1 -1
  181. package/dist/components/Select/select.js +15 -8
  182. package/dist/components/Select/select.js.map +1 -1
  183. package/dist/components/SelectMenu/index.js +2 -1
  184. package/dist/components/SelectMenu/index.js.map +1 -1
  185. package/dist/components/SelectMenu/select-menu-keyboard.d.ts +59 -0
  186. package/dist/components/SelectMenu/select-menu-keyboard.d.ts.map +1 -0
  187. package/dist/components/SelectMenu/select-menu-keyboard.js +107 -0
  188. package/dist/components/SelectMenu/select-menu-keyboard.js.map +1 -0
  189. package/dist/components/SelectMenu/select-menu.d.ts +1 -22
  190. package/dist/components/SelectMenu/select-menu.d.ts.map +1 -1
  191. package/dist/components/SelectMenu/select-menu.js +18 -34
  192. package/dist/components/SelectMenu/select-menu.js.map +1 -1
  193. package/dist/components/SelectionControl/selection-item.d.ts.map +1 -1
  194. package/dist/components/SelectionControl/selection-item.js +1 -0
  195. package/dist/components/SelectionControl/selection-item.js.map +1 -1
  196. package/dist/components/Sidebar/sidebar.d.ts +7 -9
  197. package/dist/components/Sidebar/sidebar.d.ts.map +1 -1
  198. package/dist/components/Sidebar/sidebar.js +171 -50
  199. package/dist/components/Sidebar/sidebar.js.map +1 -1
  200. package/dist/components/Slider/slider.d.ts +1 -1
  201. package/dist/components/Slider/slider.d.ts.map +1 -1
  202. package/dist/components/Slider/slider.js +9 -4
  203. package/dist/components/Slider/slider.js.map +1 -1
  204. package/dist/components/Steps/steps.d.ts.map +1 -1
  205. package/dist/components/Steps/steps.js +54 -26
  206. package/dist/components/Steps/steps.js.map +1 -1
  207. package/dist/components/Switch/switch.d.ts +1 -1
  208. package/dist/components/Switch/switch.d.ts.map +1 -1
  209. package/dist/components/Switch/switch.js +7 -2
  210. package/dist/components/Switch/switch.js.map +1 -1
  211. package/dist/components/Tabs/tabs.d.ts +1 -1
  212. package/dist/components/Tabs/tabs.d.ts.map +1 -1
  213. package/dist/components/Tabs/tabs.js +39 -4
  214. package/dist/components/Tabs/tabs.js.map +1 -1
  215. package/dist/components/Tag/tag.d.ts +1 -1
  216. package/dist/components/Tag/tag.d.ts.map +1 -1
  217. package/dist/components/Tag/tag.js +3 -2
  218. package/dist/components/Tag/tag.js.map +1 -1
  219. package/dist/components/Textarea/textarea.d.ts.map +1 -1
  220. package/dist/components/Textarea/textarea.js +8 -2
  221. package/dist/components/Textarea/textarea.js.map +1 -1
  222. package/dist/components/TimePicker/time-columns.d.ts.map +1 -1
  223. package/dist/components/TimePicker/time-columns.js +8 -4
  224. package/dist/components/TimePicker/time-columns.js.map +1 -1
  225. package/dist/components/TimePicker/time-picker.d.ts +1 -1
  226. package/dist/components/TimePicker/time-picker.js +1 -1
  227. package/dist/components/TimePicker/time-picker.js.map +1 -1
  228. package/dist/components/TreeView/tree-view.d.ts +9 -5
  229. package/dist/components/TreeView/tree-view.d.ts.map +1 -1
  230. package/dist/components/TreeView/tree-view.js +266 -214
  231. package/dist/components/TreeView/tree-view.js.map +1 -1
  232. package/dist/hooks/use-input-modality.d.ts +12 -17
  233. package/dist/hooks/use-input-modality.d.ts.map +1 -1
  234. package/dist/hooks/use-input-modality.js +4 -34
  235. package/dist/hooks/use-input-modality.js.map +1 -1
  236. package/dist/hooks/use-overflow-items.d.ts.map +1 -1
  237. package/dist/hooks/use-overflow-items.js +2 -0
  238. package/dist/hooks/use-overflow-items.js.map +1 -1
  239. package/dist/hooks/use-truncated.d.ts.map +1 -1
  240. package/dist/hooks/use-truncated.js.map +1 -1
  241. package/dist/index.d.ts +3 -0
  242. package/dist/index.d.ts.map +1 -1
  243. package/dist/index.js +23 -3
  244. package/dist/index.js.map +1 -1
  245. package/dist/lib/drag-announcements.d.ts +32 -0
  246. package/dist/lib/drag-announcements.d.ts.map +1 -1
  247. package/dist/lib/drag-announcements.js +39 -1
  248. package/dist/lib/drag-announcements.js.map +1 -1
  249. package/dist/lib/focus-after-trigger.d.ts +45 -0
  250. package/dist/lib/focus-after-trigger.d.ts.map +1 -0
  251. package/dist/lib/focus-after-trigger.js +66 -0
  252. package/dist/lib/focus-after-trigger.js.map +1 -0
  253. package/dist/lib/overlay-coexistence.js +1 -1
  254. package/dist/lib/roving-list-keyboard.d.ts +208 -0
  255. package/dist/lib/roving-list-keyboard.d.ts.map +1 -0
  256. package/dist/lib/roving-list-keyboard.js +174 -0
  257. package/dist/lib/roving-list-keyboard.js.map +1 -0
  258. package/dist/lib/utils.d.ts.map +1 -1
  259. package/dist/lib/utils.js +6 -1
  260. package/dist/lib/utils.js.map +1 -1
  261. package/dist/patterns/element-anatomy/item-anatomy.d.ts +4 -4
  262. package/dist/patterns/element-anatomy/item-anatomy.d.ts.map +1 -1
  263. package/dist/patterns/element-anatomy/item-anatomy.js +14 -4
  264. package/dist/patterns/element-anatomy/item-anatomy.js.map +1 -1
  265. package/dist/patterns/horizontal-overflow/horizontal-overflow.d.ts.map +1 -1
  266. package/dist/patterns/horizontal-overflow/horizontal-overflow.js +25 -0
  267. package/dist/patterns/horizontal-overflow/horizontal-overflow.js.map +1 -1
  268. package/dist/patterns/overlay-surface/overlay-surface.d.ts.map +1 -1
  269. package/dist/patterns/overlay-surface/overlay-surface.js.map +1 -1
  270. package/dist/tokens/categorical-color.d.ts +6 -1
  271. package/dist/tokens/categorical-color.d.ts.map +1 -1
  272. package/dist/tokens/categorical-color.js.map +1 -1
  273. package/dist/tokens/uiSize/icon-size.d.ts +1 -1
  274. package/dist/tokens/uiSize/stack-gap.d.ts +18 -0
  275. package/dist/tokens/uiSize/stack-gap.d.ts.map +1 -0
  276. package/dist/tokens/uiSize/stack-gap.js +5 -0
  277. package/dist/tokens/uiSize/stack-gap.js.map +1 -0
  278. package/ds-canonical/fork/codex/agents/skills/performance-audit/SKILL.md +1 -1
  279. package/ds-canonical/fork/codex/hooks.json +1 -1
  280. package/ds-canonical/fork/consumer/lock.json +17 -14
  281. package/ds-canonical/fork/consumer/managed-files/423729840acdd43c812f17c245b871716c6b60f76e4d3e94864d0e34a815bf8d.blob +9 -0
  282. package/ds-canonical/fork/consumer/managed-files/46f10bc55cdfdf834d0749eebff42ea0bb8576f361b36d2b6ba1851354c50150.blob +47 -9
  283. package/ds-canonical/fork/consumer/managed-files/5a0d134a2645114c8caac20458070b00a7f0fd55b3e8805b7325cb4cf52fa15e.blob +110 -0
  284. package/ds-canonical/fork/consumer/managed-files/5af0a46760af338d51fd0f7950675f8e97f7d2f09386b8818fcd7a58e6654e75.blob +790 -0
  285. package/ds-canonical/fork/consumer/managed-files/68bfcddff9c4a16388c248cd34fb66163e9e64b3d809a48ef3984f6ae4ff6d12.blob +211 -0
  286. package/ds-canonical/fork/consumer/managed-files/7eb6cc60abfdabaf6617c336058fad1454ccde08889295767bfd9b7fd153cc6e.blob +45 -6
  287. package/ds-canonical/fork/consumer/managed-files/92e4828cdcb96ff0d01daa16f6885f24045a8e72820665650f2a1890dc3a446c.blob +42 -2
  288. package/ds-canonical/fork/consumer/managed-files/ab072e7f2eca51f0e18a5e18c4b333260ee5d219a12cc6df5bb2d683f0b2734d.blob +27 -1
  289. package/ds-canonical/fork/consumer/managed-files/e71c65f45c6af7ce12b6b3c0d8028f185da5b719e2050c9b2a6b243d41ae4878.blob +62 -14
  290. package/ds-canonical/fork/governance.lock +41 -17
  291. package/ds-canonical/fork/hooks/check_consumer_app_invariants.sh +16 -4
  292. package/ds-canonical/fork/hooks/check_consumer_code_quality.sh +3 -1
  293. package/ds-canonical/fork/hooks/check_pattern_invariants.sh +13 -9
  294. package/ds-canonical/fork/launchers/settings-hooks.json +1 -1
  295. package/ds-canonical/fork/manifest.json +17 -11
  296. package/ds-canonical/fork/providers/claude/skills/performance-audit/SKILL.md +1 -1
  297. package/ds-canonical/fork/providers/codex/hook-config.json +1 -1
  298. package/ds-canonical/fork/providers/codex/skills/performance-audit/SKILL.md +1 -1
  299. package/ds-canonical/fork/skills/performance-audit/SKILL.md +1 -1
  300. package/ds-canonical/hooks/check_consumer_app_invariants.sh +16 -4
  301. package/ds-canonical/hooks/check_consumer_code_quality.sh +3 -1
  302. package/ds-canonical/hooks/check_datatable_invariants.sh +4 -1
  303. package/ds-canonical/hooks/check_pattern_invariants.sh +13 -9
  304. package/ds-canonical/hooks/check_plugin_fork_health.sh +8 -1
  305. package/ds-canonical/hooks/check_propose_discipline.sh +8 -1
  306. package/ds-canonical/hooks/check_storybook_addon_packaging.sh +8 -1
  307. package/ds-canonical/hooks/check_substantive_edit_approval_preflight.sh +64 -2
  308. package/ds-canonical/hooks/lib/_overlay_handcraft.sh +5 -2
  309. package/ds-canonical/hooks/registrations.json +12 -7
  310. package/ds-canonical/hooks/session_start_governance_check.sh +16 -0
  311. package/ds-canonical/hooks/tests/test_check_consumer_app_invariants.sh +25 -0
  312. package/ds-canonical/hooks/tests/test_check_consumer_code_quality.sh +4 -0
  313. package/ds-canonical/hooks/tests/test_check_consumer_ds_primitive_misuse.sh +14 -0
  314. package/ds-canonical/hooks/tests/test_check_datatable_invariants.sh +24 -0
  315. package/ds-canonical/hooks/tests/test_check_overlay_handcraft.sh +32 -0
  316. package/ds-canonical/hooks/tests/test_check_pattern_invariants.sh +59 -0
  317. package/ds-canonical/hooks/tests/test_check_plugin_fork_health.sh +32 -1
  318. package/ds-canonical/hooks/tests/test_check_propose_discipline.sh +21 -0
  319. package/ds-canonical/hooks/tests/test_check_storybook_addon_packaging.sh +21 -0
  320. package/ds-canonical/hooks/tests/test_check_substantive_edit_approval_preflight.sh +97 -0
  321. package/ds-canonical/hooks/tests/test_session_start_governance_check.sh +49 -0
  322. package/ds-canonical/references/README.md +12 -6
  323. package/ds-canonical/references/drag-canonical.md +3 -1
  324. package/ds-canonical/references/focus-canonical.md +26 -13
  325. package/ds-canonical/references/governance-audit-coverage.md +1 -1
  326. package/ds-canonical/references/hit-area-canonical.md +361 -0
  327. package/ds-canonical/references/item-anatomy-recipe.md +4 -2
  328. package/ds-canonical/references/keyboard-model-canonical.md +559 -0
  329. package/ds-canonical/rules/meta-patterns.md +4 -4
  330. package/ds-canonical/rules/self-verify.md +4 -4
  331. package/ds-canonical/skills/design-system-audit/references/audit-prompts.md +1 -1
  332. package/ds-canonical/skills/design-system-audit/references/historical-bugs.md +96 -0
  333. package/ds-canonical/skills/knowledge-prune/SKILL.md +4 -1
  334. package/ds-canonical/skills/performance-audit/SKILL.md +1 -1
  335. package/ds-story-manifest.json +19 -7
  336. package/llms-full.txt +2 -2
  337. package/llms.txt +2 -2
  338. package/package.json +3 -1
  339. package/src/components/Accordion/accordion.anatomy.stories.tsx +3 -3
  340. package/src/components/Accordion/accordion.spec.md +1 -1
  341. package/src/components/Accordion/accordion.tsx +8 -3
  342. package/src/components/AccountMenu/account-menu.spec.md +2 -3
  343. package/src/components/AccountMenu/account-menu.tsx +0 -1
  344. package/src/components/AgentPanel/agent-decision-card.spec.md +88 -0
  345. package/src/components/AgentPanel/agent-panel-fab.spec.md +151 -0
  346. package/src/components/AgentPanel/agent-panel-fab.tsx +67 -24
  347. package/src/components/AgentPanel/agent-panel-logo.spec.md +87 -0
  348. package/src/components/AgentPanel/agent-panel-logo.tsx +3 -3
  349. package/src/components/AgentPanel/agent-panel.anatomy.stories.tsx +1 -1
  350. package/src/components/AgentPanel/agent-panel.principles.stories.tsx +1 -1
  351. package/src/components/AgentPanel/agent-panel.spec.md +30 -275
  352. package/src/components/AgentPanel/agent-panel.stories.tsx +113 -8
  353. package/src/components/AgentPanel/agent-panel.tsx +59 -22
  354. package/src/components/Alert/alert.anatomy.stories.tsx +1 -1
  355. package/src/components/AppShell/app-shell.tsx +1 -1
  356. package/src/components/AspectRatio/aspect-ratio.anatomy.stories.tsx +10 -15
  357. package/src/components/Avatar/avatar.anatomy.stories.tsx +23 -26
  358. package/src/components/Avatar/avatar.principles.stories.tsx +10 -10
  359. package/src/components/Avatar/avatar.spec.md +45 -6
  360. package/src/components/Avatar/avatar.stories.tsx +8 -8
  361. package/src/components/Avatar/avatar.tsx +227 -21
  362. package/src/components/Badge/badge.anatomy.stories.tsx +16 -21
  363. package/src/components/Breadcrumb/breadcrumb.anatomy.stories.tsx +3 -3
  364. package/src/components/Breadcrumb/breadcrumb.spec.md +3 -3
  365. package/src/components/Breadcrumb/breadcrumb.tsx +5 -3
  366. package/src/components/BulkActionBar/bulk-action-bar.anatomy.stories.tsx +1 -1
  367. package/src/components/BulkActionBar/bulk-action-bar.principles.stories.tsx +1 -1
  368. package/src/components/BulkActionBar/bulk-action-bar.spec.md +1 -0
  369. package/src/components/BulkActionBar/bulk-action-bar.stories.tsx +57 -2
  370. package/src/components/BulkActionBar/bulk-action-bar.tsx +32 -2
  371. package/src/components/Button/button.anatomy.stories.tsx +174 -33
  372. package/src/components/Button/button.spec.md +23 -8
  373. package/src/components/Button/button.tsx +116 -21
  374. package/src/components/Calendar/calendar.anatomy.stories.tsx +34 -11
  375. package/src/components/Calendar/calendar.principles.stories.tsx +44 -1
  376. package/src/components/Calendar/calendar.spec.md +97 -20
  377. package/src/components/Calendar/calendar.stories.tsx +101 -4
  378. package/src/components/Calendar/calendar.tsx +379 -74
  379. package/src/components/Carousel/carousel.anatomy.stories.tsx +16 -17
  380. package/src/components/Carousel/carousel.spec.md +48 -2
  381. package/src/components/Carousel/carousel.tsx +9 -3
  382. package/src/components/Chart/chart.anatomy.stories.tsx +7 -5
  383. package/src/components/Chart/chart.spec.md +32 -7
  384. package/src/components/Chart/chart.tsx +9 -0
  385. package/src/components/Checkbox/checkbox.anatomy.stories.tsx +21 -28
  386. package/src/components/Checkbox/checkbox.spec.md +4 -2
  387. package/src/components/Checkbox/checkbox.tsx +10 -2
  388. package/src/components/Chip/chip.anatomy.stories.tsx +1 -1
  389. package/src/components/Chip/chip.spec.md +2 -1
  390. package/src/components/Chip/chip.tsx +4 -2
  391. package/src/components/Combobox/combobox.anatomy.stories.tsx +30 -37
  392. package/src/components/Combobox/combobox.spec.md +11 -3
  393. package/src/components/Combobox/combobox.tsx +43 -12
  394. package/src/components/Command/command.spec.md +11 -0
  395. package/src/components/Command/command.tsx +143 -10
  396. package/src/components/DataTable/cell-registry.tsx +5 -1
  397. package/src/components/DataTable/data-table-column-visibility-panel.tsx +5 -6
  398. package/src/components/DataTable/data-table-interaction-layer.tsx +27 -14
  399. package/src/components/DataTable/data-table-known-defects.spec.md +72 -0
  400. package/src/components/DataTable/data-table-scroll-performance.spec.md +295 -0
  401. package/src/components/DataTable/data-table-sort-manager.tsx +5 -6
  402. package/src/components/DataTable/data-table.anatomy.stories.tsx +1 -1
  403. package/src/components/DataTable/data-table.css +64 -0
  404. package/src/components/DataTable/data-table.spec.md +70 -346
  405. package/src/components/DataTable/data-table.stories.tsx +56 -9
  406. package/src/components/DataTable/data-table.tsx +392 -103
  407. package/src/components/DateGrid/date-grid.anatomy.stories.tsx +2 -2
  408. package/src/components/DateGrid/date-grid.spec.md +163 -1
  409. package/src/components/DateGrid/date-grid.tsx +270 -7
  410. package/src/components/DatePicker/date-picker.anatomy.stories.tsx +18 -27
  411. package/src/components/DatePicker/date-picker.spec.md +6 -2
  412. package/src/components/DatePicker/date-picker.tsx +20 -1
  413. package/src/components/Dialog/dialog.spec.md +6 -2
  414. package/src/components/Dialog/dialog.stories.tsx +18 -9
  415. package/src/components/Dialog/dialog.tsx +83 -48
  416. package/src/components/DropdownMenu/dropdown-menu-keyboard.ts +255 -0
  417. package/src/components/DropdownMenu/dropdown-menu.anatomy.stories.tsx +32 -33
  418. package/src/components/DropdownMenu/dropdown-menu.spec.md +18 -5
  419. package/src/components/DropdownMenu/dropdown-menu.stories.tsx +58 -1
  420. package/src/components/DropdownMenu/dropdown-menu.tsx +70 -33
  421. package/src/components/Empty/empty.spec.md +1 -2
  422. package/src/components/Field/field-context.ts +9 -4
  423. package/src/components/Field/field-controls.spec.md +5 -2
  424. package/src/components/Field/field-edit-keys.ts +12 -0
  425. package/src/components/Field/field-wrapper.tsx +76 -10
  426. package/src/components/Field/field.anatomy.stories.tsx +1 -1
  427. package/src/components/Field/field.spec.md +11 -6
  428. package/src/components/Field/field.stories.tsx +24 -1
  429. package/src/components/Field/field.tsx +10 -4
  430. package/src/components/FileItem/file-item.anatomy.stories.tsx +20 -10
  431. package/src/components/FileItem/file-item.principles.stories.tsx +14 -7
  432. package/src/components/FileItem/file-item.spec.md +50 -48
  433. package/src/components/FileItem/file-item.stories.tsx +41 -28
  434. package/src/components/FileItem/file-item.tsx +184 -50
  435. package/src/components/FileUpload/file-upload.anatomy.stories.tsx +11 -15
  436. package/src/components/FileUpload/file-upload.spec.md +27 -1
  437. package/src/components/FileUpload/file-upload.stories.tsx +33 -0
  438. package/src/components/FileUpload/file-upload.tsx +173 -43
  439. package/src/components/FileViewer/file-viewer.tsx +9 -4
  440. package/src/components/HoverCard/hover-card.stories.tsx +2 -2
  441. package/src/components/InlineEdit/inline-edit.spec.md +2 -0
  442. package/src/components/InlineEdit/inline-edit.tsx +8 -3
  443. package/src/components/Input/input.anatomy.stories.tsx +23 -31
  444. package/src/components/Input/input.spec.md +11 -0
  445. package/src/components/Input/input.tsx +6 -1
  446. package/src/components/LinkInput/link-input.anatomy.stories.tsx +18 -21
  447. package/src/components/LinkInput/link-input.principles.stories.tsx +4 -4
  448. package/src/components/LinkInput/link-input.spec.md +12 -1
  449. package/src/components/LinkInput/link-input.tsx +37 -4
  450. package/src/components/Menu/menu-item.anatomy.stories.tsx +50 -56
  451. package/src/components/Menu/menu-item.spec.md +1 -1
  452. package/src/components/Menu/menu-item.stories.tsx +48 -42
  453. package/src/components/Notice/notice.anatomy.stories.tsx +8 -16
  454. package/src/components/Notice/notice.spec.md +1 -1
  455. package/src/components/Notice/notice.tsx +1 -1
  456. package/src/components/NumberInput/number-input.anatomy.stories.tsx +21 -28
  457. package/src/components/NumberInput/number-input.spec.md +2 -0
  458. package/src/components/NumberInput/number-input.tsx +8 -1
  459. package/src/components/OverflowIndicator/overflow-indicator.anatomy.stories.tsx +28 -48
  460. package/src/components/OverflowIndicator/overflow-indicator.spec.md +13 -1
  461. package/src/components/OverflowIndicator/overflow-indicator.stories.tsx +12 -9
  462. package/src/components/OverflowIndicator/overflow-indicator.tsx +32 -12
  463. package/src/components/Pagination/pagination.spec.md +1 -0
  464. package/src/components/Pagination/pagination.tsx +1 -1
  465. package/src/components/PeoplePicker/avatar-stack-overflow.ts +14 -13
  466. package/src/components/PeoplePicker/people-picker-helpers.ts +5 -2
  467. package/src/components/PeoplePicker/people-picker.anatomy.stories.tsx +1 -1
  468. package/src/components/PeoplePicker/people-picker.spec.md +4 -2
  469. package/src/components/PeoplePicker/people-picker.tsx +1 -4
  470. package/src/components/PeoplePicker/person-display.tsx +45 -28
  471. package/src/components/Popover/popover.anatomy.stories.tsx +1 -1
  472. package/src/components/Popover/popover.principles.stories.tsx +3 -12
  473. package/src/components/Popover/popover.spec.md +7 -3
  474. package/src/components/ProfileCard/profile-card.anatomy.stories.tsx +1 -1
  475. package/src/components/ProfileCard/profile-card.spec.md +1 -1
  476. package/src/components/ProfileCard/profile-card.tsx +0 -3
  477. package/src/components/ProgressBar/progress-bar.anatomy.stories.tsx +11 -21
  478. package/src/components/ProgressBar/progress-bar.spec.md +1 -1
  479. package/src/components/RadioGroup/radio-group.anatomy.stories.tsx +17 -24
  480. package/src/components/RadioGroup/radio-group.spec.md +1 -1
  481. package/src/components/RadioGroup/radio-group.tsx +22 -4
  482. package/src/components/Rating/rating.anatomy.stories.tsx +131 -124
  483. package/src/components/Rating/rating.principles.stories.tsx +69 -43
  484. package/src/components/Rating/rating.spec.md +142 -63
  485. package/src/components/Rating/rating.stories.tsx +11 -15
  486. package/src/components/Rating/rating.tsx +131 -117
  487. package/src/components/ScrollArea/scroll-area.anatomy.stories.tsx +1 -1
  488. package/src/components/ScrollArea/scroll-area.principles.stories.tsx +6 -3
  489. package/src/components/ScrollArea/scroll-area.spec.md +3 -1
  490. package/src/components/ScrollArea/scroll-area.stories.tsx +5 -2
  491. package/src/components/ScrollArea/scroll-area.tsx +1 -1
  492. package/src/components/SegmentedControl/segmented-control.anatomy.stories.tsx +1 -1
  493. package/src/components/SegmentedControl/segmented-control.spec.md +2 -2
  494. package/src/components/SegmentedControl/segmented-control.tsx +3 -2
  495. package/src/components/Select/select.anatomy.stories.tsx +26 -35
  496. package/src/components/Select/select.spec.md +4 -3
  497. package/src/components/Select/select.tsx +11 -4
  498. package/src/components/SelectMenu/select-menu-keyboard.ts +218 -0
  499. package/src/components/SelectMenu/select-menu.anatomy.stories.tsx +6 -17
  500. package/src/components/SelectMenu/select-menu.spec.md +12 -4
  501. package/src/components/SelectMenu/select-menu.tsx +21 -56
  502. package/src/components/SelectionControl/selection-item.anatomy.stories.tsx +9 -33
  503. package/src/components/SelectionControl/selection-item.spec.md +2 -2
  504. package/src/components/SelectionControl/selection-item.tsx +3 -1
  505. package/src/components/Separator/separator.stories.tsx +4 -4
  506. package/src/components/Sheet/sheet.principles.stories.tsx +31 -13
  507. package/src/components/Sidebar/sidebar.anatomy.stories.tsx +1 -1
  508. package/src/components/Sidebar/sidebar.spec.md +73 -6
  509. package/src/components/Sidebar/sidebar.stories.tsx +82 -4
  510. package/src/components/Sidebar/sidebar.tsx +281 -64
  511. package/src/components/Skeleton/skeleton.spec.md +1 -1
  512. package/src/components/Slider/slider.anatomy.stories.tsx +1 -1
  513. package/src/components/Slider/slider.spec.md +6 -5
  514. package/src/components/Slider/slider.tsx +13 -8
  515. package/src/components/Steps/steps-state-visuals.spec.md +157 -0
  516. package/src/components/Steps/steps.anatomy.stories.tsx +1 -0
  517. package/src/components/Steps/steps.spec.md +59 -142
  518. package/src/components/Steps/steps.stories.tsx +1 -1
  519. package/src/components/Steps/steps.tsx +125 -33
  520. package/src/components/Switch/switch.spec.md +3 -3
  521. package/src/components/Switch/switch.tsx +6 -1
  522. package/src/components/Tabs/tabs.spec.md +5 -1
  523. package/src/components/Tabs/tabs.tsx +42 -3
  524. package/src/components/Tag/tag.anatomy.stories.tsx +24 -28
  525. package/src/components/Tag/tag.spec.md +10 -11
  526. package/src/components/Tag/tag.tsx +3 -3
  527. package/src/components/Textarea/textarea.tsx +7 -1
  528. package/src/components/TimePicker/time-columns.tsx +9 -4
  529. package/src/components/TimePicker/time-picker.spec.md +4 -1
  530. package/src/components/TimePicker/time-picker.tsx +1 -1
  531. package/src/components/Tooltip/tooltip.anatomy.stories.tsx +6 -15
  532. package/src/components/Tooltip/tooltip.principles.stories.tsx +4 -16
  533. package/src/components/Tooltip/tooltip.spec.md +2 -1
  534. package/src/components/TreeView/tree-view.anatomy.stories.tsx +35 -13
  535. package/src/components/TreeView/tree-view.spec.md +67 -38
  536. package/src/components/TreeView/tree-view.stories.tsx +221 -12
  537. package/src/components/TreeView/tree-view.tsx +463 -356
  538. package/src/hooks/use-input-modality.ts +17 -46
  539. package/src/hooks/use-overflow-items.ts +4 -0
  540. package/src/hooks/use-truncated.ts +3 -2
  541. package/src/index.ts +17 -0
  542. package/src/lib/README.md +2 -0
  543. package/src/lib/drag-announcements.ts +78 -0
  544. package/src/lib/focus-after-trigger.ts +102 -0
  545. package/src/lib/roving-list-keyboard.ts +400 -0
  546. package/src/lib/utils.ts +5 -0
  547. package/src/patterns/action-bar/action-bar.spec.md +2 -0
  548. package/src/patterns/element-anatomy/inline-action.spec.md +16 -12
  549. package/src/patterns/element-anatomy/item-anatomy.spec.md +98 -6
  550. package/src/patterns/element-anatomy/item-anatomy.stories.tsx +75 -70
  551. package/src/patterns/element-anatomy/item-anatomy.tsx +17 -6
  552. package/src/patterns/element-anatomy/truncated-text.spec.md +1 -0
  553. package/src/patterns/header-canonical/header-canonical.spec.md +3 -2
  554. package/src/patterns/horizontal-overflow/horizontal-overflow.spec.md +2 -0
  555. package/src/patterns/horizontal-overflow/horizontal-overflow.stories.tsx +36 -0
  556. package/src/patterns/horizontal-overflow/horizontal-overflow.tsx +28 -0
  557. package/src/patterns/overlay-surface/overlay-chrome-sizing.spec.md +180 -0
  558. package/src/patterns/overlay-surface/overlay-surface.spec.md +8 -162
  559. package/src/patterns/overlay-surface/overlay-surface.tsx +5 -3
  560. package/src/patterns/resize-handle/resize-handle.spec.md +3 -3
  561. package/src/styles/base.css +1 -1
  562. package/src/tokens/categorical-color.ts +6 -1
  563. package/src/tokens/color/color.spec.md +91 -24
  564. package/src/tokens/color/color.stories.tsx +50 -2
  565. package/src/tokens/color/primitives.css +41 -38
  566. package/src/tokens/color/semantic.css +105 -36
  567. package/src/tokens/layoutSpace/layoutSpace.spec.md +2 -0
  568. package/src/tokens/motion/motion.spec.md +31 -8
  569. package/src/tokens/orphan-tokens.spec.md +1 -1
  570. package/src/tokens/uiSize/icon-size.ts +1 -1
  571. package/src/tokens/uiSize/stack-gap.ts +17 -0
  572. package/src/tokens/uiSize/uiSize.css +12 -0
  573. package/src/tokens/uiSize/uiSize.spec.md +17 -16
  574. package/src/tokens/uiSize/uiSize.stories.tsx +1 -1
  575. package/dist/node_modules/aria-hidden/dist/es2015/index.js +0 -147
  576. package/dist/node_modules/aria-hidden/dist/es2015/index.js.map +0 -1
@@ -0,0 +1,255 @@
1
+ /**
2
+ * @internal — DropdownMenu 的鍵盤接線:選單開著按 Tab、子選單裡按 Esc(2026-09-25 user 拍板,待辦總帳 B10 / B11)。
3
+ * 不進 barrel(只有 dropdown-menu.tsx 消費)。
4
+ *
5
+ * 2026-09-26 從 dropdown-menu.tsx 拆出(該檔 879 行,超過 `scripts/code-quality-audit.mjs` 的 800 行上限;
6
+ * 待辦總帳〇節「dropdown-menu.tsx 879 行超過 800 行上限,拆檔」)。拆法:dropdown-menu.tsx 只留視覺層,
7
+ * 開關狀態 / 觸發點登記 / Tab / Esc 全部住這裡;「從觸發點算下一站」合併進 `lib/focus-after-trigger.ts`(與 SelectMenu 共用一份)。
8
+ *
9
+ * 規則住所 = `ds-canonical/references/keyboard-model-canonical.md`「彈出框開著時的 Tab 與 Esc」,本檔只做機械落地:
10
+ * Tab / Shift+Tab:收起全部層,從「開啟者」往下 / 往上走一站(= 在開啟者上按 Tab)。
11
+ * Radix 預設把 Tab 擋掉(@radix-ui/react-menu@2.1.16 dist/index.mjs:305;上游 menu.tsx:570-572
12
+ * https://github.com/radix-ui/primitives/blob/f7ecd5ab16f5e1e820eb5786a1419a98a2d594ae/packages/react/menu/src/menu.tsx#L570-L572)。
13
+ * 子選單 Esc:只關這一層、焦點回上一層那一項(= ←)。Radix 預設關整棵(menu.tsx:1262-1266
14
+ * https://github.com/radix-ui/primitives/blob/f7ecd5ab16f5e1e820eb5786a1419a98a2d594ae/packages/react/menu/src/menu.tsx#L1262-L1266)。
15
+ * user 原話(逐字,附條件同意):選單 Tab「你確定這個符合我們一致的設計語言且不違背世界級的設計就這樣做」;
16
+ * 子選單 Esc「你確定這符合我們一致的設計語言且不違背世界級的設計就這樣做」。
17
+ */
18
+ import * as React from 'react'
19
+ import { flushSync } from 'react-dom'
20
+ import { focusFromTrigger } from '@/design-system/lib/focus-after-trigger'
21
+
22
+ /** 能拿焦點的元素(判斷「觸發點本身能不能當開啟者」用) */
23
+ const FOCUSABLE = 'a[href], area[href], button, input, select, textarea, iframe, summary, [contenteditable]:not([contenteditable="false"]), [tabindex]'
24
+
25
+ /** 選單各層(含收合動畫中還掛著的那層)不是頁面上的一站 */
26
+ const isInsideMenu = (el: HTMLElement) => !!el.closest('[data-radix-menu-content]')
27
+
28
+ function assignRef<T>(ref: React.ForwardedRef<T>, value: T | null) {
29
+ if (typeof ref === 'function') ref(value)
30
+ else if (ref) ref.current = value
31
+ }
32
+
33
+ /**
34
+ * 選單的「開啟者」—— Tab 離開時從它算下一站。
35
+ * 觸發點自己能聚焦 → 就是它(一般情況)。觸發點只是透明錨點(AgentFab 右鍵選單:錨點是蓋在按鈕上、aria-hidden 的 span)→
36
+ * 改找「宣告 `aria-controls` 指向本選單」的可聚焦元素(ARIA 的 menu button 關係本來就是這樣寫);再不行才找包住觸發點的可聚焦祖先。
37
+ */
38
+ function menuOpener(trigger: HTMLElement | null, content: HTMLElement | null): HTMLElement | null {
39
+ if (!trigger) return null
40
+ if (trigger.matches(FOCUSABLE) && !trigger.matches(':disabled')) return trigger
41
+ const id = content?.id
42
+ if (id) {
43
+ const owner = Array.from(trigger.ownerDocument.querySelectorAll<HTMLElement>('[aria-controls]')).find(
44
+ (el) => el !== trigger && (el.getAttribute('aria-controls') ?? '').split(/\s+/).includes(id) && el.matches(FOCUSABLE),
45
+ )
46
+ if (owner) return owner
47
+ }
48
+ return trigger.parentElement?.closest<HTMLElement>(FOCUSABLE) ?? null
49
+ }
50
+
51
+ /** 整個選單(根)共用的鍵盤狀態:誰是觸發點、根內容是哪個元素、這次關閉是不是 Tab 走掉的 */
52
+ export interface DropdownMenuKeyboard {
53
+ closeAll: () => void
54
+ triggerRef: React.MutableRefObject<HTMLElement | null>
55
+ contentRef: React.MutableRefObject<HTMLElement | null>
56
+ /** 這次關閉是 Tab 離開 → 焦點已由使用者帶走,關閉時不再自動聚焦(不還觸發點、也不呼叫 consumer 的 onCloseAutoFocus) */
57
+ tabbedOutRef: React.MutableRefObject<boolean>
58
+ }
59
+ const DropdownMenuKeyboardContext = React.createContext<DropdownMenuKeyboard | null>(null)
60
+
61
+ /** 每一層(根 / 每個子選單)各一份:「關掉這一層」。子選單已在收合動畫中時,再按 Esc 交給上一層。 */
62
+ export interface DropdownMenuLevel {
63
+ closeLevel: () => void
64
+ triggerRef: React.MutableRefObject<HTMLElement | null>
65
+ }
66
+ const DropdownMenuLevelContext = React.createContext<DropdownMenuLevel | null>(null)
67
+
68
+ /**
69
+ * 選單開著按 Tab(B11):收起全部層,焦點從開啟者往下 / 往上走一站;頁面上已沒有下一站 → 留在開啟者(X20)。
70
+ * 只處理「焦點就在這一層」按下的 Tab;子層的 Tab 由子層處理,冒泡到上層時已 defaultPrevented。
71
+ */
72
+ function handleMenuTab(event: React.KeyboardEvent<HTMLElement>, keyboard: DropdownMenuKeyboard | null) {
73
+ if (!keyboard || event.key !== 'Tab' || event.defaultPrevented) return
74
+ if (event.altKey || event.ctrlKey || event.metaKey) return
75
+ if ((event.target as Element).closest('[data-radix-menu-content]') !== event.currentTarget) return
76
+ const opener = menuOpener(keyboard.triggerRef.current, keyboard.contentRef.current)
77
+ if (!opener) return // 找不到開啟者 → 留給 Radix 預設(不動),總比把焦點丟到頁尾好
78
+ event.preventDefault()
79
+ keyboard.tabbedOutRef.current = true
80
+ // 先同步關掉:modal 選單的 FocusScope 焦點鎖要先解除,否則 focus() 會被拉回選單裡
81
+ flushSync(() => keyboard.closeAll())
82
+ focusFromTrigger(opener, event.shiftKey, isInsideMenu)
83
+ }
84
+
85
+ /** 開關狀態由 DS 這一層擁有、受控地交給 Radix:Tab 離開(B11)與子選單 Esc 傳到根(B10)都要從 DS 這一側關掉選單 */
86
+ function useOwnedOpenState(open: boolean | undefined, defaultOpen: boolean | undefined, onOpenChange?: (open: boolean) => void) {
87
+ const [uncontrolledOpen, setUncontrolledOpen] = React.useState(defaultOpen ?? false)
88
+ const resolvedOpen = open ?? uncontrolledOpen
89
+ const setOpen = React.useCallback(
90
+ (nextOpen: boolean) => {
91
+ if (open === undefined) setUncontrolledOpen(nextOpen)
92
+ onOpenChange?.(nextOpen)
93
+ },
94
+ [onOpenChange, open],
95
+ )
96
+ return [resolvedOpen, setOpen] as const
97
+ }
98
+
99
+ export interface OpenStateProps {
100
+ open?: boolean
101
+ defaultOpen?: boolean
102
+ onOpenChange?: (open: boolean) => void
103
+ }
104
+
105
+ /** 根:開關狀態 + 鍵盤狀態 + 根這一層的「關掉這一層」(交給 `DropdownMenuKeyboardProvider` 包住 Radix Root)。 */
106
+ export function useDropdownMenuRootKeyboard({ open, defaultOpen, onOpenChange }: OpenStateProps) {
107
+ const [resolvedOpen, setOpen] = useOwnedOpenState(open, defaultOpen, onOpenChange)
108
+ const triggerRef = React.useRef<HTMLElement | null>(null)
109
+ const contentRef = React.useRef<HTMLElement | null>(null)
110
+ const tabbedOutRef = React.useRef(false)
111
+ const handleOpenChange = React.useCallback(
112
+ (nextOpen: boolean) => {
113
+ if (nextOpen) tabbedOutRef.current = false
114
+ setOpen(nextOpen)
115
+ },
116
+ [setOpen],
117
+ )
118
+ const keyboard = React.useMemo<DropdownMenuKeyboard>(
119
+ () => ({ closeAll: () => handleOpenChange(false), triggerRef, contentRef, tabbedOutRef }),
120
+ [handleOpenChange],
121
+ )
122
+ const level = React.useMemo<DropdownMenuLevel>(
123
+ () => ({ closeLevel: () => handleOpenChange(false), triggerRef }),
124
+ [handleOpenChange],
125
+ )
126
+ return { resolvedOpen, handleOpenChange, keyboard, level }
127
+ }
128
+
129
+ /** 子選單:開關狀態 + 「關掉這一層」(B10:Esc 只關最內層、焦點回上一層那一項)。 */
130
+ export function useDropdownMenuSubKeyboard({ open, defaultOpen, onOpenChange }: OpenStateProps) {
131
+ const [resolvedOpen, handleOpenChange] = useOwnedOpenState(open, defaultOpen, onOpenChange)
132
+ const parent = React.useContext(DropdownMenuLevelContext)
133
+ const triggerRef = React.useRef<HTMLElement | null>(null)
134
+ const level = React.useMemo<DropdownMenuLevel>(
135
+ () => ({
136
+ closeLevel: () => {
137
+ // 這一層已經在收合動畫中(Radix 的 DismissableLayer 還掛著、仍是最上層)→ 這次 Esc 屬於上一層
138
+ if (!resolvedOpen) {
139
+ parent?.closeLevel()
140
+ return
141
+ }
142
+ // 與 Radix 子選單的 ← 同一組動作(上游 menu.tsx:1271-1274 onOpenChange(false) + trigger.focus())
143
+ handleOpenChange(false)
144
+ triggerRef.current?.focus()
145
+ },
146
+ triggerRef,
147
+ }),
148
+ [handleOpenChange, parent, resolvedOpen],
149
+ )
150
+ return { resolvedOpen, handleOpenChange, level }
151
+ }
152
+
153
+ /** 根用:鍵盤狀態 + 根這一層(`useDropdownMenuRootKeyboard` 的回傳值原樣傳進來) */
154
+ export function DropdownMenuKeyboardProvider({ keyboard, level, children }: {
155
+ keyboard: DropdownMenuKeyboard
156
+ level: DropdownMenuLevel
157
+ children: React.ReactNode
158
+ }) {
159
+ return React.createElement(
160
+ DropdownMenuKeyboardContext.Provider,
161
+ { value: keyboard },
162
+ React.createElement(DropdownMenuLevelContext.Provider, { value: level }, children),
163
+ )
164
+ }
165
+
166
+ /** 子選單用:這一層(`useDropdownMenuSubKeyboard` 的回傳值原樣傳進來) */
167
+ export function DropdownMenuLevelProvider({ level, children }: { level: DropdownMenuLevel; children: React.ReactNode }) {
168
+ return React.createElement(DropdownMenuLevelContext.Provider, { value: level }, children)
169
+ }
170
+
171
+ /** 觸發點:記下它,選單開著按 Tab 時從它算下一站(B11) */
172
+ export function useMenuTriggerRef<T extends HTMLElement>(ref: React.ForwardedRef<T>) {
173
+ const keyboard = React.useContext(DropdownMenuKeyboardContext)
174
+ return React.useCallback(
175
+ (node: T | null) => {
176
+ if (keyboard) keyboard.triggerRef.current = node
177
+ assignRef(ref, node)
178
+ },
179
+ [keyboard, ref],
180
+ )
181
+ }
182
+
183
+ /** 子選單觸發項:記下它,子選單按 Esc 只關這一層時焦點回到它(B10) */
184
+ export function useSubMenuTriggerRef<T extends HTMLElement>(ref: React.ForwardedRef<T>) {
185
+ const level = React.useContext(DropdownMenuLevelContext)
186
+ return React.useCallback(
187
+ (node: T | null) => {
188
+ if (level) level.triggerRef.current = node
189
+ assignRef(ref, node)
190
+ },
191
+ [level, ref],
192
+ )
193
+ }
194
+
195
+ /**
196
+ * 根內容:記下它(觸發點是透明錨點時靠它的 id 找開啟者)、接上 Tab、Tab 離開時擋掉關閉後的自動聚焦。
197
+ * Focus return on close 的其餘情形不 override —— 用 Radix 內建 default(close 時 focus 還 trigger;
198
+ * outside-interaction 例外由 Radix `hasInteractedOutsideRef` 自管;W3C APG menubar「Escape: …return focus to the element…
199
+ * from which the menu was opened」)。唯一例外是 Tab 離開:焦點已由使用者帶到下一站,沒有「關閉後自動聚焦」這回事 →
200
+ * 擋掉 Radix 的還觸發點,也不轉給 consumer 的 onCloseAutoFocus(否則像「還焦點給編輯器」的 consumer 會把焦點拉回去,X21)。
201
+ */
202
+ export function useMenuContentKeyboard<T extends HTMLElement>(
203
+ ref: React.ForwardedRef<T>,
204
+ onKeyDown: React.KeyboardEventHandler<T> | undefined,
205
+ onCloseAutoFocus: ((event: Event) => void) | undefined,
206
+ ) {
207
+ const keyboard = React.useContext(DropdownMenuKeyboardContext)
208
+ const setRef = React.useCallback(
209
+ (node: T | null) => {
210
+ if (keyboard) keyboard.contentRef.current = node
211
+ assignRef(ref, node)
212
+ },
213
+ [keyboard, ref],
214
+ )
215
+ return {
216
+ ref: setRef,
217
+ onKeyDown: (event: React.KeyboardEvent<T>) => {
218
+ onKeyDown?.(event)
219
+ handleMenuTab(event, keyboard)
220
+ },
221
+ onCloseAutoFocus: (event: Event) => {
222
+ if (keyboard?.tabbedOutRef.current) {
223
+ keyboard.tabbedOutRef.current = false
224
+ event.preventDefault()
225
+ return
226
+ }
227
+ onCloseAutoFocus?.(event)
228
+ },
229
+ }
230
+ }
231
+
232
+ /**
233
+ * 子選單內容:Tab 同樣收起全部層、從根的開啟者往下走(W3C「close all menus and submenus」);
234
+ * Esc 只關這一層、焦點回上一層那一項(B10)。preventDefault 讓 Radix 的 rootContext.onClose() 不跑
235
+ *(composeEventHandlers 見 defaultPrevented 就跳過內建 handler);consumer 自己 preventDefault = 不關,照 Radix 慣例。
236
+ */
237
+ export function useSubMenuContentKeyboard<T extends HTMLElement>(
238
+ onKeyDown: React.KeyboardEventHandler<T> | undefined,
239
+ onEscapeKeyDown: ((event: KeyboardEvent) => void) | undefined,
240
+ ) {
241
+ const keyboard = React.useContext(DropdownMenuKeyboardContext)
242
+ const level = React.useContext(DropdownMenuLevelContext)
243
+ return {
244
+ onKeyDown: (event: React.KeyboardEvent<T>) => {
245
+ onKeyDown?.(event)
246
+ handleMenuTab(event, keyboard)
247
+ },
248
+ onEscapeKeyDown: (event: KeyboardEvent) => {
249
+ onEscapeKeyDown?.(event)
250
+ if (event.defaultPrevented || !level) return
251
+ event.preventDefault()
252
+ level.closeLevel()
253
+ },
254
+ }
255
+ }
@@ -8,6 +8,7 @@ import {
8
8
  DropdownMenuSub, DropdownMenuSubTrigger, DropdownMenuSubContent,
9
9
  } from './dropdown-menu'
10
10
  import { Button } from '@/design-system/components/Button/button'
11
+ import { SegmentedControl, SegmentedControlItem } from '@/design-system/components/SegmentedControl/segmented-control'
11
12
 
12
13
  const meta: Meta = {
13
14
  title: 'Design System/Components/DropdownMenu/設計規格',
@@ -123,18 +124,6 @@ const TokenAnnotation = ({ colors }: { colors: ColorSpec }) => (
123
124
  </div>
124
125
  )
125
126
 
126
- const Tab = ({ active, onClick, disabled, children }: { active: boolean; onClick: () => void; disabled?: boolean; children: React.ReactNode }) => {
127
- if (disabled) return <span className="px-2.5 py-1 text-[12px] font-mono rounded-md text-fg-disabled bg-neutral-hover cursor-not-allowed">{children}</span>
128
- return (
129
- <button type="button" onClick={onClick}
130
- className={`px-2.5 py-1 text-[12px] font-mono rounded-md cursor-pointer transition-colors ${
131
- active ? 'bg-primary text-white font-semibold' : 'bg-neutral-hover text-fg-secondary hover:bg-neutral-active'
132
- }`}>
133
- {children}
134
- </button>
135
- )
136
- }
137
-
138
127
  const PropRow = ({ label, dot, children }: { label: string; dot?: string; children: React.ReactNode }) => (
139
128
  <div className="flex items-start gap-3 py-2 border-b border-divider last:border-b-0">
140
129
  <span className="text-[11px] text-fg-muted font-medium w-[72px] shrink-0 pt-0.5 flex items-center gap-1.5">
@@ -316,33 +305,35 @@ const InspectorInner = () => {
316
305
 
317
306
  return (
318
307
  <div className="flex flex-col gap-6">
319
- {/* Controls */}
308
+ {/* Controls — 互斥切換消費 SegmentedControl(segmented-control.spec.md「何時用」2–5 個互斥選項;
309
+ 取代手刻 Tab:它靜止借 neutral-hover、hover 借 neutral-active,是 color.spec.md 成對 token 的錯配) */}
320
310
  <div className="flex flex-col gap-2.5">
321
311
  <div className="flex items-center gap-2">
322
312
  <span className="text-[11px] text-fg-muted w-16 shrink-0">Size</span>
323
- <div className="flex gap-1.5">
324
- {(['sm', 'md', 'lg'] as const).map((sz) => <Tab key={sz} active={size === sz} onClick={() => setSize(sz)}>{sz}</Tab>)}
325
- </div>
313
+ <SegmentedControl size="sm" aria-label="Size" value={size} onValueChange={(v) => setSize(v as SizeKey)}>
314
+ {(['sm', 'md', 'lg'] as const).map((sz) => <SegmentedControlItem key={sz} value={sz}>{sz}</SegmentedControlItem>)}
315
+ </SegmentedControl>
326
316
  </div>
327
317
  <div className="flex items-center gap-2">
328
318
  <span className="text-[11px] text-fg-muted w-16 shrink-0">Item</span>
329
- <div className="flex gap-1.5">
330
- {(['item', 'checkbox', 'subTrigger'] as const).map((m) => <Tab key={m} active={itemMode === m} onClick={() => setItemMode(m)}>{m}</Tab>)}
331
- </div>
319
+ <SegmentedControl size="sm" aria-label="Item" value={itemMode} onValueChange={(v) => setItemMode(v as ItemMode)}>
320
+ {(['item', 'checkbox', 'subTrigger'] as const).map((m) => <SegmentedControlItem key={m} value={m}>{m}</SegmentedControlItem>)}
321
+ </SegmentedControl>
332
322
  </div>
333
323
  <div className="flex items-center gap-2">
334
324
  <span className="text-[11px] text-fg-muted w-16 shrink-0">Danger</span>
335
- <div className="flex gap-1.5">
336
- <Tab active={!danger} onClick={() => setDanger(false)}>off</Tab>
337
- <Tab active={danger && canDanger} onClick={() => setDanger(true)} disabled={!canDanger}>on</Tab>
338
- </div>
325
+ {/* 停用項用元件自身 disabled(segmented-control.spec.md「disabled」);停用項不得是當前值 → value 取 hasDanger */}
326
+ <SegmentedControl size="sm" aria-label="Danger" value={hasDanger ? 'on' : 'off'} onValueChange={(v) => setDanger(v === 'on')}>
327
+ <SegmentedControlItem value="off">off</SegmentedControlItem>
328
+ <SegmentedControlItem value="on" disabled={!canDanger}>on</SegmentedControlItem>
329
+ </SegmentedControl>
339
330
  {!canDanger && <span className="text-[11px] text-fg-muted">僅限 Item</span>}
340
331
  </div>
341
332
  <div className="flex items-center gap-2">
342
333
  <span className="text-[11px] text-fg-muted w-16 shrink-0">State</span>
343
- <div className="flex gap-1.5">
344
- {STATES.map((st) => <Tab key={st} active={state === st} onClick={() => setState(st)}>{st}</Tab>)}
345
- </div>
334
+ <SegmentedControl size="sm" aria-label="State" value={state} onValueChange={(v) => setState(v as StateKey)}>
335
+ {STATES.map((st) => <SegmentedControlItem key={st} value={st}>{st}</SegmentedControlItem>)}
336
+ </SegmentedControl>
346
337
  </div>
347
338
  </div>
348
339
 
@@ -486,7 +477,9 @@ const ItemPreview = ({ size, danger, state, icon: Icon, label, shortcut }: {
486
477
  const textClass = danger ? 'text-error' : state === 'disabled' ? 'text-fg-disabled' : ''
487
478
  const fontClass = size === 'lg' ? 'text-body-lg' : 'text-body'
488
479
  return (
480
+ // aria-disabled:靜態預覽畫的是「停用中的項目」,明告 inactive 才不會被 axe 當一般文字誤報 color-contrast(2026-09-29,待辦總帳 OE21;三個 Preview 同修)
489
481
  <div className={`flex items-start gap-2 px-3 ${fontClass} leading-compact ${bgClass} ${textClass}`}
482
+ aria-disabled={state === 'disabled' || undefined}
490
483
  style={{ paddingTop: `calc((var(--field-height-${size}) - 1lh) / 2)`, paddingBottom: `calc((var(--field-height-${size}) - 1lh) / 2)` }}>
491
484
  <div className="h-[1lh] flex items-center shrink-0"><Icon size={iconPx} /></div>
492
485
  <span>{label}</span>
@@ -505,6 +498,7 @@ const SubTriggerPreview = ({ size, state, icon: Icon, label, triggerValue }: {
505
498
  const fontClass = size === 'lg' ? 'text-body-lg' : 'text-body'
506
499
  return (
507
500
  <div className={`flex items-start gap-2 px-3 ${fontClass} leading-compact ${bgClass} ${textClass}`}
501
+ aria-disabled={state === 'disabled' || undefined}
508
502
  style={{ paddingTop: `calc((var(--field-height-${size}) - 1lh) / 2)`, paddingBottom: `calc((var(--field-height-${size}) - 1lh) / 2)` }}>
509
503
  <div className="h-[1lh] flex items-center shrink-0"><Icon size={iconPx} /></div>
510
504
  <span>{label}</span>
@@ -527,6 +521,7 @@ const CheckboxPreview = ({ size, label, checked, state }: {
527
521
  const checkboxPx = SIZE_SPECS[size].icon
528
522
  return (
529
523
  <div className={`flex items-start gap-2 px-3 ${fontClass} leading-compact ${bgClass} ${textClass}`}
524
+ aria-disabled={state === 'disabled' || undefined}
530
525
  style={{ paddingTop: `calc((var(--field-height-${size}) - 1lh) / 2)`, paddingBottom: `calc((var(--field-height-${size}) - 1lh) / 2)` }}>
531
526
  <div className="h-[1lh] flex items-center shrink-0">
532
527
  <div className={`rounded border flex items-center justify-center shrink-0 ${isCheckedVisual ? 'bg-primary border-primary' : 'border-border'}`}
@@ -577,7 +572,9 @@ export const ColorMatrix = {
577
572
  const isCheckboxSelected = type === 'checkbox' && st === 'active/selected'
578
573
  return (
579
574
  <td key={st} className="p-3 border-b border-divider align-top min-w-[160px]">
575
+ {/* aria-disabled:disabled 那一格畫的是停用中的項目,明告 inactive 才不會被 axe 當一般文字誤報 color-contrast(2026-09-29,待辦總帳 OE21) */}
580
576
  <div className="rounded px-3 py-1.5 text-body leading-compact border border-border flex items-center gap-2"
577
+ aria-disabled={st === 'disabled' || undefined}
581
578
  style={{
582
579
  backgroundColor: `var(${ITEM_TOKEN_MAP[type][st].bg})`,
583
580
  color: `var(${ITEM_TOKEN_MAP[type][st].text})`,
@@ -619,6 +616,7 @@ export const ColorMatrix = {
619
616
  {STATES.map((st) => (
620
617
  <td key={st} className="p-3 border-b border-divider align-top min-w-[160px]">
621
618
  <div className="rounded px-3 py-1.5 text-body leading-compact border border-border flex items-center gap-2"
619
+ aria-disabled={st === 'disabled' || undefined}
622
620
  style={{
623
621
  backgroundColor: `var(${DANGER_ITEM_MAP[st].bg})`,
624
622
  color: `var(${DANGER_ITEM_MAP[st].text})`,
@@ -847,8 +845,8 @@ const StateBehaviorInner = () => {
847
845
  <AnimationFrame label="closing" state="closing" desc="animate-out,~150ms duration" />
848
846
  </div>
849
847
  <div className="text-caption text-fg-muted pt-2 border-t border-divider">
850
- 觸發 `close` 的路徑:(a) 點擊 item(auto close)/ (b) Esc / (c) 外部點擊(onPointerDownOutside)/ (d) blur。
851
- Trigger 會回到 focused 狀態,保留鍵盤流程。
848
+ 觸發 `close` 的路徑:(a) 點擊 item(auto close)/ (b) Esc / (c) 外部點擊(onPointerDownOutside)/ (d) blur / (e) Tab / Shift+Tab。
849
+ (a)(b) 焦點回到 Trigger,保留鍵盤流程;(c) 焦點留在點的地方;(e) 焦點從 Trigger 往下 / 往上走一站(2026-09-25,spec「鍵盤操作」)。
852
850
  </div>
853
851
  </div>
854
852
 
@@ -856,7 +854,7 @@ const StateBehaviorInner = () => {
856
854
  <div className="flex flex-col gap-3">
857
855
  <span className="text-caption font-medium text-fg-secondary">行為 2:Submenu 展開(hover/focus 延遲 + sideways slide)</span>
858
856
  <Desc>
859
- SubTrigger hover 約 100ms 自動展開 submenu 向右側滑入。Submenu 是獨立 floating layer——與 parent 共存,parent 不 close。鍵盤用 ArrowRight 展開 / ArrowLeft 收起;本 DS 為 LTR-only。
857
+ SubTrigger hover 約 100ms 自動展開 submenu 向右側滑入。Submenu 是獨立 floating layer——與 parent 共存,parent 不 close。鍵盤用 ArrowRight 展開 / ArrowLeft 或 Esc 收起這一層(2026-09-25 起 Esc 只關一層);本 DS 為 LTR-only。
860
858
  </Desc>
861
859
  <div className="flex gap-6 items-start">
862
860
  <div className="flex items-center justify-center px-6 py-8 rounded-lg bg-canvas border border-divider min-w-[280px]">
@@ -885,7 +883,7 @@ const StateBehaviorInner = () => {
885
883
  ChevronRight suffix 是 SubTrigger 的視覺 affordance(消費 item-anatomy row 主檔,不自刻)。
886
884
  </div>
887
885
  <div className="pt-1 border-t border-divider">
888
- 鍵盤:焦點在 SubTrigger 時 ArrowRight 展開、ArrowLeft 回 parent。
886
+ 鍵盤:焦點在 SubTrigger 時 ArrowRight 展開;在子選單裡 ArrowLeft 或 Esc 只收這一層、焦點回 SubTrigger。
889
887
  </div>
890
888
  </div>
891
889
  </div>
@@ -943,9 +941,10 @@ const StateBehaviorInner = () => {
943
941
  <li>`DropdownMenuItem` 點擊即 close——這是「action 觸發即完成」語意。</li>
944
942
  <li>`DropdownMenuCheckboxItem` 點擊不 close——每次勾選即時生效(onCheckedChange 即時 fire),menu 保持開啟只為讓使用者連續多選。</li>
945
943
  <li>`DropdownMenuSub` 永遠右側滑入,parent menu 保持展開——提供 breadcrumb 式認知流,使用者知道自己在哪層。</li>
946
- <li>Escape close 整個選單(含所有展開子層;Radix root close)。ArrowLeft 才逐層收合(焦點回 SubTrigger)。</li>
944
+ <li>Escape 一次只關焦點所在的那一層:子選單 → 焦點回上一層那一項(與 ArrowLeft 同);主選單 → 焦點回 trigger。每按一次少一層(2026-09-25 起;原本 Radix 預設一次全關,待辦總帳 B10)。</li>
945
+ <li>選單開著按 Tab / Shift+Tab:一次收起全部層,焦點從 trigger 往下 / 往上走一站(2026-09-25 起;原本 Radix 預設擋掉 Tab,待辦總帳 B11)。</li>
947
946
  <li>Portal render 到 body,`z-50` 確保不被 parent overflow 截斷。</li>
948
- <li>Focus trap:menu open 時鍵盤焦點進入 menu,close 時自動 restore 到 trigger。</li>
947
+ <li>焦點不鎖在選單裡(預設非 modal):open 時鍵盤焦點進入 menu;Esc / 選了項目 → 回 trigger,點外面 → 留在點的地方,Tab → 下一站。</li>
949
948
  </ul>
950
949
  </div>
951
950
  </div>
@@ -964,7 +963,7 @@ export const Accessibility = {
964
963
  render: () => (
965
964
  <div className="max-w-3xl text-body text-fg-secondary">
966
965
  <h3 className="text-h5 text-foreground mb-2">無障礙設計</h3>
967
- <p className="whitespace-pre-line">{"無障礙能力沿用 Radix 選單元件的內建支援,涵蓋語意角色、選單導覽與焦點管理,設計師與工程師不需額外設定。\n\n鍵盤操作:\n\n- Tab — 將焦點移到觸發按鈕\n- Enter / Space / 向下鍵 — 開啟選單\n- 向上鍵 / 向下鍵 — 在選項之間移動\n- Home / End — 跳到第一項 / 最後一項(PageUp / PageDown 同)\n- 輸入首字 — 跳到標籤符合的選項\n- Enter / Space — 選擇目前的選項\n- Esc — 關閉整個選單(含子選單;逐層收合用左方向鍵)\n\n焦點管理:選單開啟時焦點會進入選單,關閉後自動回到原本的觸發按鈕;目前導覽到的選項會以明顯的 highlight 底色標示,鍵盤使用者隨時看得到自己在哪一項。\n\n驗證:鍵盤可完整操作(不需滑鼠),文字對比達 WCAG AA;在 Storybook 無障礙檢查面板上應為零項嚴重問題。"}</p>
966
+ <p className="whitespace-pre-line">{"無障礙能力沿用 Radix 選單元件的內建支援,涵蓋語意角色、選單導覽與焦點管理(Tab 與子選單的 Esc 兩鍵由本元件改寫,見下),設計師與工程師不需額外設定。\n\n鍵盤操作:\n\n- Tab — 將焦點移到觸發按鈕\n- Enter / Space / 向下鍵 — 開啟選單\n- 向上鍵 / 向下鍵 — 在選項之間移動\n- Home / End — 跳到第一項 / 最後一項(PageUp / PageDown 同)\n- 輸入首字 — 跳到標籤符合的選項\n- Enter / Space — 選擇目前的選項\n- Esc — 只關目前所在的那一層:在子選單裡回到上一層打開它的那一項(同左方向鍵),在主選單裡回到觸發按鈕\n- Tab / Shift+Tab(選單開著時)— 一次收起全部層,焦點從觸發按鈕往下 / 往上走一站\n\n焦點管理:選單開啟時焦點會進入選單;按 Esc 或選了項目,焦點回到觸發按鈕;點外面,焦點留在點的地方;按 Tab,焦點到下一站。鍵盤導覽到的選項畫焦點框(滑鼠移過的選項才上底色),鍵盤使用者隨時看得到自己在哪一項。\n\n驗證:鍵盤可完整操作(不需滑鼠),文字對比達 WCAG AA;在 Storybook 無障礙檢查面板上應為零項嚴重問題。"}</p>
968
967
  <p className="mt-4 text-fg-muted">完整 ARIA 細節參考 <a className="text-primary hover:text-primary-hover" href="https://www.radix-ui.com/primitives/docs/components/dropdown-menu#accessibility" target="_blank" rel="noreferrer">Radix Accessibility 文件</a>。</p>
969
968
  </div>
970
969
  ),
@@ -176,7 +176,7 @@ DropdownMenu 是掃描模式——使用者快速瀏覽選項,所有尺寸使
176
176
 
177
177
  ## 鍵盤操作
178
178
 
179
- 由 Radix DropdownMenu 提供:
179
+ 由 Radix DropdownMenu 提供;**`Tab` / `Shift+Tab` 與子選單裡的 `Escape` 兩處由本元件改寫**(2026-09-25,來歷見下一節):
180
180
 
181
181
  | 按鍵 | 行為 |
182
182
  |---|---|
@@ -184,10 +184,22 @@ DropdownMenu 是掃描模式——使用者快速瀏覽選項,所有尺寸使
184
184
  | `Home` / `End` | 跳到第一/最後一個 item(`PageUp` / `PageDown` 同) |
185
185
  | 首字 typeahead | 打字元自動跳到 **label 符合**的 item——children 為字串時元件自動 forward `textValue`,description / badge / shortcut 不進比對;複雜 children(非純字串)由 consumer 顯式給 `textValue` |
186
186
  | `Enter` / `Space` | 執行目前焦點的動作 |
187
- | `Escape` | 關閉整個選單(含所有展開子層;Radix root close)。逐層收合用 `←` |
187
+ | `Tab` / `Shift+Tab` | **收起全部層(含子選單),焦點從開啟選單的那顆鈕往下 / 往上走一站** —— 效果等於在觸發鈕上按 `Tab` / `Shift+Tab`。選單內容掛在頁面最末端,所以下一站一律從觸發鈕算,不從選單項算。觸發鈕在對話框或面板裡時,照那個框的規則在框內繞圈;頁面上已經沒有下一站時,焦點留在觸發鈕 |
188
+ | `Escape` | **只關焦點所在的那一層**:在主選單 → 關選單、焦點回觸發鈕;在子選單 → 只關這一層、焦點回上一層打開它的那一項(與 `←` 同效果)。**每按一次少一層** |
188
189
  | `→` | 展開子選單 |
189
190
  | `←` | 收合子選單,焦點回到 SubTrigger |
190
191
 
192
+ **關閉之後焦點去哪**(全 DS 規則住在 `ds-canonical/references/keyboard-model-canonical.md`「彈出框開著時的 Tab 與 Esc」,本表只是本元件的落地):
193
+ `Escape` 或選了項目 → 回觸發鈕;點外面 → 留在點的地方(非 modal 預設,Radix `hasInteractedOutsideRef`);`Tab` → 已經走到下一站,不再拉回觸發鈕,也不呼叫 consumer 的 `onCloseAutoFocus`。
194
+
195
+ ### `Tab` 與子選單 `Escape` 的來歷(2026-09-25)
196
+
197
+ **user 原話(逐字,附條件同意)**:選單 Tab:「你確定這個符合我們一致的設計語言且不違背世界級的設計就這樣做」;子選單 Esc:「你確定這符合我們一致的設計語言且不違背世界級的設計就這樣做」;追問:「看最後是怎樣定義,選單類應該要一致吧?這樣才符合世界級的設計?」。條件(世界級 + DS 一致)的查證是 **AI 研究**(待辦總帳 `governance/planning/2026-09-25-interaction-and-hover-remediation.md` B10 / B11);Tab 那一題 user 可在預覽否決。
198
+
199
+ - **子選單 `Escape` 只關一層**:W3C「Close the menu that contains focus and return focus to the element or context, e.g., menu button or parent `menuitem`, from which the menu was opened」(https://github.com/w3c/aria-practices/blob/3f094fde1c81b25dfa69162563bf28d093f854d4/content/patterns/menubar/menu-and-menubar-pattern.html#L153 );同派:Adobe React Aria(https://github.com/adobe/react-spectrum/blob/956ecbcb8803f0d0d5d5d973bb169d70c52aea02/packages/react-aria/src/menu/useSubmenuTrigger.ts#L161-L171 )、Fluent(https://github.com/microsoft/fluentui/blob/d27922755bebae866d9ffe86b7da44c27ec801ee/packages/react-components/react-menu/library/src/components/Menu/Menu.cy.tsx#L952-L964 )、GitHub Primer 測試「closes top menu on escape or left arrow key press」(https://github.com/primer/react/blob/f2c075a5d4d0b51a279c39effa18226ad909929d/packages/react/src/ActionMenu/ActionMenu.test.tsx#L626-L649 )、VS Code(https://github.com/microsoft/vscode/blob/2ec783d855253a817b5787fb48bc6c3d8d31c0c5/src/vs/base/browser/ui/menu/menu.ts#L953-L957 )、Windows 桌面選單(https://github.com/microsoft/microsoft-ui-xaml/blob/258a2e9b0852b69c98162e6daf9906ec8fe5161a/dxaml/xcp/dxaml/lib/MenuFlyoutPresenter_Partial.cpp#L232-L235 )、Chrome(https://github.com/chromium/chromium/blob/c53fe9e04b2856967d1f2f716b99b0911251e521/ui/views/controls/menu/menu_controller.cc#L2155-L2167 )、Firefox「Pressing Escape hides one level of menus only.」(https://github.com/mozilla-firefox/firefox/blob/0a5c5bffebd796cedba281419efd2f8f3cc7c85d/layout/xul/nsXULPopupManager.cpp#L2642-L2648 )。有一手資料的 9 家裡唯一例外是本元件的底層 Radix(子選單 Esc 呼叫 `rootContext.onClose()` 關整棵,https://github.com/radix-ui/primitives/blob/f7ecd5ab16f5e1e820eb5786a1419a98a2d594ae/packages/react/menu/src/menu.tsx#L1262-L1266 ),本元件改寫它。DS 內部:對話框、側滑面板、AI 面板早已寫明 Esc 只關最上層 / 最內層(`../Dialog/dialog.spec.md`、`../Sheet/sheet.spec.md`、`../AgentPanel/agent-panel.spec.md`),子選單原本是全 DS 唯一例外。
200
+ - **`Tab` 收起並往下走**:W3C「When focus is on a `menuitem` in a `menu` or `menubar`, move focus out of the `menu` or `menubar`, and close all menus and submenus.」(https://github.com/w3c/aria-practices/blob/3f094fde1c81b25dfa69162563bf28d093f854d4/content/patterns/menubar/menu-and-menubar-pattern.html#L82 );Fluent 測試「should be able to tab to next element after the root trigger」(https://github.com/microsoft/fluentui/blob/d27922755bebae866d9ffe86b7da44c27ec801ee/packages/react-components/react-menu/library/src/components/Menu/Menu.cy.tsx#L579 );Primer「When Tab or Shift+Tab is pressed, the menu should close and the focus should naturally move to the next item」(https://github.com/primer/react/blob/f2c075a5d4d0b51a279c39effa18226ad909929d/packages/react/src/hooks/useMenuKeyboardNavigation.ts#L29-L30 )。**世界級在這題不一致**:Radix(「menus should not be navigated using tab key so we prevent it」,https://github.com/radix-ui/primitives/blob/f7ecd5ab16f5e1e820eb5786a1419a98a2d594ae/packages/react/menu/src/menu.tsx#L570-L572 )、Adobe React Aria(測試「contains focus within the menu」,https://github.com/adobe/react-spectrum/blob/956ecbcb8803f0d0d5d5d973bb169d70c52aea02/packages/react-aria-components/test/Menu.test.tsx#L1842-L1877 )、VS Code(「Stop tab navigation of menus」,https://github.com/microsoft/vscode/blob/2ec783d855253a817b5787fb48bc6c3d8d31c0c5/src/vs/base/browser/ui/menu/menu.ts#L132-L139 )是「按了不動」那一派。本 DS 採 W3C 這一派,理由是 DS 自己的鍵盤分工「Tab 換區、方向鍵在區裡走、Esc 退一步」(`ds-canonical/references/keyboard-model-canonical.md`)——選單是一個區,Tab 就該離開它(AI 推導,條件查證見待辦總帳 B11)。
201
+ - **實作**:`dropdown-menu-keyboard.ts` 的 `handleMenuTab`(`Content` / `SubContent` 的 `onKeyDown`)與子選單的 `closeLevel`(`SubContent` 的 `onEscapeKeyDown`);2026-09-26 從 `dropdown-menu.tsx` 拆出(該檔 879 行超過 800 行上限),`dropdown-menu.tsx` 只留視覺層。「從觸發點算下一站」與 SelectMenu 共用一份 `../../lib/focus-after-trigger.ts`(2026-09-26 兩份合一,待辦總帳〇節「按鍵規則合併」)。觸發點是透明錨點時(AgentFab 右鍵選單),以「宣告 `aria-controls` 指向本選單的可聚焦元素」為開啟者。觸發點本身不在 Tab 路上時(例:樹 / 側欄列上 `tabIndex=-1` 的選單鈕),以它在文件裡的位置為起點往下 / 往上走 —— 從列上選單鈕打開的選單按 `Tab`,會直接離開那一整串。
202
+
191
203
  ---
192
204
 
193
205
  ## 破壞性動作
@@ -237,14 +249,15 @@ Item-level default / hover / focused / selected / disabled **色彩**由 MenuIte
237
249
 
238
250
  **Keyboard 行為**:
239
251
 
240
- - Tab — focus trigger
252
+ - Tab — 選單關著:把焦點移到觸發鈕(觸發鈕本身是一站)
241
253
  - Enter / Space / ↓ — 開啟
242
254
  - ↑/↓ — 導覽 items(Home/End 跳首尾項,PageUp/PageDown 同)
243
255
  - 首字 typeahead — 打字元自動跳到 label 符合的 item(比對 `textValue`,見「鍵盤操作」表)
244
256
  - Enter / Space — 選擇目前焦點的選項
245
- - Esc — 關閉
257
+ - Tab / Shift+Tab — 選單開著:收起全部層,焦點從觸發鈕往下 / 往上走一站(2026-09-25 起,見「鍵盤操作」)
258
+ - Esc — 只關焦點所在的那一層(子選單 → 回上一層那一項;主選單 → 回觸發鈕)
246
259
 
247
- **Focus**:Radix primitive 自管 focus trap / restoration。反白(Radix `data-highlighted`)是**唯一的游標**,依**反白來歷**分流(owner = `ds-canonical/references/focus-canonical.md` 規則一「兩類元件」+ 規則二,user 2026-09-09 拍板「都要畫框,不上底色」):**滑鼠移過搬走反白** = hover → `bg-neutral-hover`、無框;**鍵盤搬走反白** = 游標 → 畫框 `focus-ring-inset`(內描邊,列撐滿選單寬)、不上底色,而且滑鼠停留列的 hover 底色一起消失(四種項目上都沒有 `hover:` 樣式;滑鼠停著不算搶,2026-09-09 下午 user 三問、Radix Menu 原始碼證實 `onPointerMove → item.focus()` 就是搶 —— https://github.com/radix-ui/primitives/blob/main/packages/react/menu/src/menu.tsx)。來歷由 `hooks/use-input-modality.ts` 的 `useCursorMover` + 各項目 `onPointerMoveCapture → markPointerGrab` 判(本元件刻意用 `data-highlighted` + 模態而非瀏覽器 `:focus-visible`,因 Radix 對 hover 做程式化 focus、各瀏覽器 `:focus-visible` 行為不一 — 見 dropdown-menu.tsx docblock「Hover / highlight canonical」)。2026-09-09 之前鍵盤導覽到的未選中項只有底色不畫框 —— AI 推導自 Radix 慣例,已撤回。**Trigger focus 指示**:Trigger 走全域 `:focus-visible` 外描邊(`styles/base.css`,幾何 SSOT = focus-canonical「框怎麼畫」);asChild 場景由子元件(通常 Button)同樣走全域外描邊。
260
+ **Focus**:預設非 modal —— 焦點不鎖在選單裡(`Tab` 會離開,見上)。開啟時焦點進選單;關閉時 `Esc` / 選了項目 → 回觸發鈕,點外面 → 留在點的地方,`Tab` → 下一站(本元件 `onCloseAutoFocus` 在 Tab 離開時擋掉還觸發鈕)。反白(Radix `data-highlighted`)是**唯一的游標**,依**反白來歷**分流(owner = `ds-canonical/references/focus-canonical.md` 規則一「兩類元件」+ 規則二,user 2026-09-09 拍板「都要畫框,不上底色」):**滑鼠移過搬走反白** = hover → `bg-neutral-hover`、無框;**鍵盤搬走反白** = 游標 → 畫框 `focus-ring-inset`(內描邊,列撐滿選單寬)、不上底色,而且滑鼠停留列的 hover 底色一起消失(四種項目上都沒有 `hover:` 樣式;滑鼠停著不算搶,2026-09-09 下午 user 三問、Radix Menu 原始碼證實 `onPointerMove → item.focus()` 就是搶 —— https://github.com/radix-ui/primitives/blob/main/packages/react/menu/src/menu.tsx)。來歷由 `hooks/use-input-modality.ts` 的 `useCursorMover` + 各項目 `onPointerMoveCapture → markPointerGrab` 判(本元件刻意用 `data-highlighted` + 模態而非瀏覽器 `:focus-visible`,因 Radix 對 hover 做程式化 focus、各瀏覽器 `:focus-visible` 行為不一 — 見 dropdown-menu.tsx docblock「Hover / highlight canonical」)。2026-09-09 之前鍵盤導覽到的未選中項只有底色不畫框 —— AI 推導自 Radix 慣例,已撤回。**Trigger focus 指示**:Trigger 走全域 `:focus-visible` 外描邊(`styles/base.css`,幾何 SSOT = focus-canonical「框怎麼畫」);asChild 場景由子元件(通常 Button)同樣走全域外描邊。
248
261
 
249
262
  **ARIA composite scroll wrapper**:`DropdownMenuContent` 的 viewport-aware body 仍消費 `ScrollArea`,但 viewport 傳 `viewportTabIndex={null}` 完全省略 generic wrapper 的 `tabindex`。`role="menu"` 內的 focus owner 是 Radix roving-focus `menuitem*` family;額外 `div[tabindex]` 會破壞 required-children 結構。群組 Label 是正常可讀文字,使用 `fg-secondary`(非低對比的 `fg-muted`)。
250
263
 
@@ -1,7 +1,7 @@
1
1
  // @story-history: isOverlay OpenSnapshot / hasInteractiveStates Disabled 由 anatomy.stories.tsx StateBehavior + Inspector auto-compile owns(2026-05-15 F-migration);showcase 展示真實多選 / 角色切換 / Checkbox 整合情境。
2
2
  import type { Meta, StoryObj } from '@storybook/react'
3
3
  import { useState } from 'react'
4
- import { Mail, Settings, User, LogOut, Trash2, Copy, Pencil, ExternalLink, Moon, Sun, Monitor, ChevronDown, FileText } from 'lucide-react'
4
+ import { Mail, Settings, User, LogOut, Trash2, Copy, Pencil, ExternalLink, Moon, Sun, Monitor, ChevronDown, FileText, FolderInput, Link, Share2, UserPlus } from 'lucide-react'
5
5
  import {
6
6
  DropdownMenu, DropdownMenuTrigger, DropdownMenuContent, DropdownMenuItem,
7
7
  DropdownMenuCheckboxItem, DropdownMenuRadioGroup, DropdownMenuRadioItem,
@@ -145,6 +145,63 @@ export const SubMenu: StoryObj = {
145
145
  render: () => <SubMenuDemo />,
146
146
  }
147
147
 
148
+ // ── 多層子選單:Esc 一次只關一層、Tab 收起全部往下走(2026-09-25,待辦總帳 B10 / B11)──
149
+ // 情境:工作項目頁首的工具列「指派給我 / 更多動作 / 分享」,更多動作 → 移動到 → 專案 → 欄位,共三層。
150
+ // 觸發鈕前後各一顆真按鈕,Tab / Shift+Tab 的落點看得見也量得到(dropdown-menu-keyboard-invariant 的量測對象)。
151
+ // @overlay-open-skip: 本則教的是鍵盤開關行為(每按一次 Esc 少一層、Tab 收起往下走),要從「關著」開始操作;
152
+ // 浮層開著的視覺快照由同檔 OpenSnapshot(defaultOpen)負責。
153
+ const MOVE_TARGETS = [
154
+ { project: '行動 App 改版', columns: ['待辦', '進行中', '完成'] },
155
+ { project: '官網重構', columns: ['待辦', '設計審查', '完成'] },
156
+ ] as const
157
+
158
+ const NestedSubMenuDemo = () => (
159
+ <div className="flex items-center gap-2">
160
+ <Button variant="tertiary" startIcon={UserPlus}>指派給我</Button>
161
+ <DropdownMenu>
162
+ <DropdownMenuTrigger asChild>
163
+ <Button variant="tertiary" endIcon={ChevronDown}>更多動作</Button>
164
+ </DropdownMenuTrigger>
165
+ <DropdownMenuContent>
166
+ <DropdownMenuItem startIcon={Link}>複製連結</DropdownMenuItem>
167
+ <DropdownMenuSub>
168
+ <DropdownMenuSubTrigger startIcon={FolderInput}>移動到</DropdownMenuSubTrigger>
169
+ <DropdownMenuSubContent>
170
+ {MOVE_TARGETS.map(({ project, columns }) => (
171
+ <DropdownMenuSub key={project}>
172
+ <DropdownMenuSubTrigger>{project}</DropdownMenuSubTrigger>
173
+ <DropdownMenuSubContent>
174
+ {columns.map((column) => (
175
+ <DropdownMenuItem key={column}>{column}</DropdownMenuItem>
176
+ ))}
177
+ </DropdownMenuSubContent>
178
+ </DropdownMenuSub>
179
+ ))}
180
+ </DropdownMenuSubContent>
181
+ </DropdownMenuSub>
182
+ <DropdownMenuSeparator />
183
+ <DropdownMenuItem startIcon={Trash2} className="text-error">刪除</DropdownMenuItem>
184
+ </DropdownMenuContent>
185
+ </DropdownMenu>
186
+ <Button variant="tertiary" startIcon={Share2}>分享</Button>
187
+ </div>
188
+ )
189
+
190
+ export const NestedSubMenu: StoryObj = {
191
+ name: '多層子選單',
192
+ parameters: {
193
+ docs: {
194
+ description: {
195
+ story:
196
+ '用鍵盤從「更多動作」一路按 → 開到第三層(移動到 → 專案 → 欄位)。' +
197
+ '按 Esc 每次只關最內層的那一層,焦點回到上一層打開它的那一項(跟 ← 一樣);三層要按三次才全部關掉、回到「更多動作」。' +
198
+ '選單開著時按 Tab,不論在第幾層都一次收起全部,焦點落在「分享」;按 Shift+Tab 則落在「指派給我」。',
199
+ },
200
+ },
201
+ },
202
+ render: () => <NestedSubMenuDemo />,
203
+ }
204
+
148
205
  // ── Checkbox Items ──
149
206
 
150
207
  const CheckboxDemo = () => {