@qijenchen/design-system 0.1.0-beta.10

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 (507) hide show
  1. package/README.md +163 -0
  2. package/dist/components/Accordion/accordion.d.ts +37 -0
  3. package/dist/components/Accordion/accordion.d.ts.map +1 -0
  4. package/dist/components/Accordion/accordion.js +78 -0
  5. package/dist/components/Accordion/accordion.js.map +1 -0
  6. package/dist/components/Alert/alert.d.ts +47 -0
  7. package/dist/components/Alert/alert.d.ts.map +1 -0
  8. package/dist/components/Alert/alert.js +132 -0
  9. package/dist/components/Alert/alert.js.map +1 -0
  10. package/dist/components/AppShell/_demo-helpers.d.ts +49 -0
  11. package/dist/components/AppShell/_demo-helpers.d.ts.map +1 -0
  12. package/dist/components/AppShell/app-shell.d.ts +76 -0
  13. package/dist/components/AppShell/app-shell.d.ts.map +1 -0
  14. package/dist/components/AppShell/app-shell.js +214 -0
  15. package/dist/components/AppShell/app-shell.js.map +1 -0
  16. package/dist/components/AspectRatio/aspect-ratio.d.ts +40 -0
  17. package/dist/components/AspectRatio/aspect-ratio.d.ts.map +1 -0
  18. package/dist/components/AspectRatio/aspect-ratio.js +23 -0
  19. package/dist/components/AspectRatio/aspect-ratio.js.map +1 -0
  20. package/dist/components/Avatar/avatar.d.ts +85 -0
  21. package/dist/components/Avatar/avatar.d.ts.map +1 -0
  22. package/dist/components/Avatar/avatar.js +195 -0
  23. package/dist/components/Avatar/avatar.js.map +1 -0
  24. package/dist/components/Badge/badge.d.ts +43 -0
  25. package/dist/components/Badge/badge.d.ts.map +1 -0
  26. package/dist/components/Badge/badge.js +69 -0
  27. package/dist/components/Badge/badge.js.map +1 -0
  28. package/dist/components/Breadcrumb/breadcrumb.d.ts +163 -0
  29. package/dist/components/Breadcrumb/breadcrumb.d.ts.map +1 -0
  30. package/dist/components/Breadcrumb/breadcrumb.js +300 -0
  31. package/dist/components/Breadcrumb/breadcrumb.js.map +1 -0
  32. package/dist/components/BulkActionBar/bulk-action-bar.d.ts +46 -0
  33. package/dist/components/BulkActionBar/bulk-action-bar.d.ts.map +1 -0
  34. package/dist/components/BulkActionBar/bulk-action-bar.js +78 -0
  35. package/dist/components/BulkActionBar/bulk-action-bar.js.map +1 -0
  36. package/dist/components/Button/button-group.d.ts +49 -0
  37. package/dist/components/Button/button-group.d.ts.map +1 -0
  38. package/dist/components/Button/button-group.js +46 -0
  39. package/dist/components/Button/button-group.js.map +1 -0
  40. package/dist/components/Button/button.d.ts +203 -0
  41. package/dist/components/Button/button.d.ts.map +1 -0
  42. package/dist/components/Button/button.js +309 -0
  43. package/dist/components/Button/button.js.map +1 -0
  44. package/dist/components/Calendar/calendar.d.ts +81 -0
  45. package/dist/components/Calendar/calendar.d.ts.map +1 -0
  46. package/dist/components/Calendar/calendar.js +282 -0
  47. package/dist/components/Calendar/calendar.js.map +1 -0
  48. package/dist/components/Carousel/carousel.d.ts +61 -0
  49. package/dist/components/Carousel/carousel.d.ts.map +1 -0
  50. package/dist/components/Carousel/carousel.js +276 -0
  51. package/dist/components/Carousel/carousel.js.map +1 -0
  52. package/dist/components/Chart/chart.d.ts +94 -0
  53. package/dist/components/Chart/chart.d.ts.map +1 -0
  54. package/dist/components/Chart/chart.js +233 -0
  55. package/dist/components/Chart/chart.js.map +1 -0
  56. package/dist/components/Checkbox/checkbox-group.d.ts +58 -0
  57. package/dist/components/Checkbox/checkbox-group.d.ts.map +1 -0
  58. package/dist/components/Checkbox/checkbox-group.js +28 -0
  59. package/dist/components/Checkbox/checkbox-group.js.map +1 -0
  60. package/dist/components/Checkbox/checkbox.d.ts +73 -0
  61. package/dist/components/Checkbox/checkbox.d.ts.map +1 -0
  62. package/dist/components/Checkbox/checkbox.js +125 -0
  63. package/dist/components/Checkbox/checkbox.js.map +1 -0
  64. package/dist/components/Chip/chip.d.ts +54 -0
  65. package/dist/components/Chip/chip.d.ts.map +1 -0
  66. package/dist/components/Chip/chip.js +224 -0
  67. package/dist/components/Chip/chip.js.map +1 -0
  68. package/dist/components/CircularProgress/circular-progress.d.ts +40 -0
  69. package/dist/components/CircularProgress/circular-progress.d.ts.map +1 -0
  70. package/dist/components/CircularProgress/circular-progress.js +118 -0
  71. package/dist/components/CircularProgress/circular-progress.js.map +1 -0
  72. package/dist/components/Coachmark/coachmark.d.ts +100 -0
  73. package/dist/components/Coachmark/coachmark.d.ts.map +1 -0
  74. package/dist/components/Coachmark/coachmark.js +107 -0
  75. package/dist/components/Coachmark/coachmark.js.map +1 -0
  76. package/dist/components/Combobox/combobox.d.ts +150 -0
  77. package/dist/components/Combobox/combobox.d.ts.map +1 -0
  78. package/dist/components/Combobox/combobox.js +595 -0
  79. package/dist/components/Combobox/combobox.js.map +1 -0
  80. package/dist/components/Command/command.d.ts +106 -0
  81. package/dist/components/Command/command.d.ts.map +1 -0
  82. package/dist/components/Command/command.js +123 -0
  83. package/dist/components/Command/command.js.map +1 -0
  84. package/dist/components/DataTable/active-editor-controller.d.ts +66 -0
  85. package/dist/components/DataTable/active-editor-controller.d.ts.map +1 -0
  86. package/dist/components/DataTable/cell-registry.d.ts +37 -0
  87. package/dist/components/DataTable/cell-registry.d.ts.map +1 -0
  88. package/dist/components/DataTable/cell-registry.js +377 -0
  89. package/dist/components/DataTable/cell-registry.js.map +1 -0
  90. package/dist/components/DataTable/column-types.d.ts +145 -0
  91. package/dist/components/DataTable/column-types.d.ts.map +1 -0
  92. package/dist/components/DataTable/column-types.js +17 -0
  93. package/dist/components/DataTable/column-types.js.map +1 -0
  94. package/dist/components/DataTable/data-table-column-visibility-panel.d.ts +49 -0
  95. package/dist/components/DataTable/data-table-column-visibility-panel.d.ts.map +1 -0
  96. package/dist/components/DataTable/data-table-filter-panel.d.ts +30 -0
  97. package/dist/components/DataTable/data-table-filter-panel.d.ts.map +1 -0
  98. package/dist/components/DataTable/data-table-interaction-layer.d.ts +78 -0
  99. package/dist/components/DataTable/data-table-interaction-layer.d.ts.map +1 -0
  100. package/dist/components/DataTable/data-table-interaction-layer.js +220 -0
  101. package/dist/components/DataTable/data-table-interaction-layer.js.map +1 -0
  102. package/dist/components/DataTable/data-table-sort-manager.d.ts +19 -0
  103. package/dist/components/DataTable/data-table-sort-manager.d.ts.map +1 -0
  104. package/dist/components/DataTable/data-table.d.ts +181 -0
  105. package/dist/components/DataTable/data-table.d.ts.map +1 -0
  106. package/dist/components/DataTable/data-table.js +1851 -0
  107. package/dist/components/DataTable/data-table.js.map +1 -0
  108. package/dist/components/DataTable/filter-operators.d.ts +116 -0
  109. package/dist/components/DataTable/filter-operators.d.ts.map +1 -0
  110. package/dist/components/DataTable/filter-tree.d.ts +66 -0
  111. package/dist/components/DataTable/filter-tree.d.ts.map +1 -0
  112. package/dist/components/DataTable/lib/column-meta.d.ts +49 -0
  113. package/dist/components/DataTable/lib/column-meta.d.ts.map +1 -0
  114. package/dist/components/DateGrid/date-grid.d.ts +61 -0
  115. package/dist/components/DateGrid/date-grid.d.ts.map +1 -0
  116. package/dist/components/DateGrid/date-grid.js +168 -0
  117. package/dist/components/DateGrid/date-grid.js.map +1 -0
  118. package/dist/components/DatePicker/date-picker.d.ts +119 -0
  119. package/dist/components/DatePicker/date-picker.d.ts.map +1 -0
  120. package/dist/components/DatePicker/date-picker.js +743 -0
  121. package/dist/components/DatePicker/date-picker.js.map +1 -0
  122. package/dist/components/DescriptionList/description-list.d.ts +60 -0
  123. package/dist/components/DescriptionList/description-list.d.ts.map +1 -0
  124. package/dist/components/DescriptionList/description-list.js +77 -0
  125. package/dist/components/DescriptionList/description-list.js.map +1 -0
  126. package/dist/components/Dialog/dialog.d.ts +54 -0
  127. package/dist/components/Dialog/dialog.d.ts.map +1 -0
  128. package/dist/components/Dialog/dialog.js +151 -0
  129. package/dist/components/Dialog/dialog.js.map +1 -0
  130. package/dist/components/DropdownMenu/dropdown-menu.d.ts +111 -0
  131. package/dist/components/DropdownMenu/dropdown-menu.d.ts.map +1 -0
  132. package/dist/components/DropdownMenu/dropdown-menu.js +288 -0
  133. package/dist/components/DropdownMenu/dropdown-menu.js.map +1 -0
  134. package/dist/components/Empty/empty.d.ts +40 -0
  135. package/dist/components/Empty/empty.d.ts.map +1 -0
  136. package/dist/components/Empty/empty.js +66 -0
  137. package/dist/components/Empty/empty.js.map +1 -0
  138. package/dist/components/Field/field-context.d.ts +77 -0
  139. package/dist/components/Field/field-context.d.ts.map +1 -0
  140. package/dist/components/Field/field-context.js +37 -0
  141. package/dist/components/Field/field-context.js.map +1 -0
  142. package/dist/components/Field/field-types.d.ts +5 -0
  143. package/dist/components/Field/field-types.d.ts.map +1 -0
  144. package/dist/components/Field/field-types.js +13 -0
  145. package/dist/components/Field/field-types.js.map +1 -0
  146. package/dist/components/Field/field-wrapper.d.ts +17 -0
  147. package/dist/components/Field/field-wrapper.d.ts.map +1 -0
  148. package/dist/components/Field/field-wrapper.js +252 -0
  149. package/dist/components/Field/field-wrapper.js.map +1 -0
  150. package/dist/components/Field/field.d.ts +127 -0
  151. package/dist/components/Field/field.d.ts.map +1 -0
  152. package/dist/components/Field/field.js +295 -0
  153. package/dist/components/Field/field.js.map +1 -0
  154. package/dist/components/FieldControlGroup/field-control-group.d.ts +74 -0
  155. package/dist/components/FieldControlGroup/field-control-group.d.ts.map +1 -0
  156. package/dist/components/FieldControlGroup/field-control-group.js +62 -0
  157. package/dist/components/FieldControlGroup/field-control-group.js.map +1 -0
  158. package/dist/components/FileItem/file-item.d.ts +44 -0
  159. package/dist/components/FileItem/file-item.d.ts.map +1 -0
  160. package/dist/components/FileItem/file-item.js +202 -0
  161. package/dist/components/FileItem/file-item.js.map +1 -0
  162. package/dist/components/FileUpload/file-upload.d.ts +97 -0
  163. package/dist/components/FileUpload/file-upload.d.ts.map +1 -0
  164. package/dist/components/FileUpload/file-upload.js +231 -0
  165. package/dist/components/FileUpload/file-upload.js.map +1 -0
  166. package/dist/components/FileViewer/file-viewer-types.d.ts +73 -0
  167. package/dist/components/FileViewer/file-viewer-types.d.ts.map +1 -0
  168. package/dist/components/FileViewer/file-viewer.d.ts +82 -0
  169. package/dist/components/FileViewer/file-viewer.d.ts.map +1 -0
  170. package/dist/components/FileViewer/file-viewer.js +752 -0
  171. package/dist/components/FileViewer/file-viewer.js.map +1 -0
  172. package/dist/components/FileViewer/image-renderer.d.ts +9 -0
  173. package/dist/components/FileViewer/image-renderer.d.ts.map +1 -0
  174. package/dist/components/FileViewer/image-renderer.js +165 -0
  175. package/dist/components/FileViewer/image-renderer.js.map +1 -0
  176. package/dist/components/HoverCard/hover-card.d.ts +30 -0
  177. package/dist/components/HoverCard/hover-card.d.ts.map +1 -0
  178. package/dist/components/HoverCard/hover-card.js +61 -0
  179. package/dist/components/HoverCard/hover-card.js.map +1 -0
  180. package/dist/components/Input/input.d.ts +72 -0
  181. package/dist/components/Input/input.d.ts.map +1 -0
  182. package/dist/components/Input/input.js +148 -0
  183. package/dist/components/Input/input.js.map +1 -0
  184. package/dist/components/LinkInput/link-input.d.ts +46 -0
  185. package/dist/components/LinkInput/link-input.d.ts.map +1 -0
  186. package/dist/components/LinkInput/link-input.js +215 -0
  187. package/dist/components/LinkInput/link-input.js.map +1 -0
  188. package/dist/components/Menu/menu-item.d.ts +83 -0
  189. package/dist/components/Menu/menu-item.d.ts.map +1 -0
  190. package/dist/components/Menu/menu-item.js +209 -0
  191. package/dist/components/Menu/menu-item.js.map +1 -0
  192. package/dist/components/NameCard/name-card.d.ts +85 -0
  193. package/dist/components/NameCard/name-card.d.ts.map +1 -0
  194. package/dist/components/NameCard/name-card.js +153 -0
  195. package/dist/components/NameCard/name-card.js.map +1 -0
  196. package/dist/components/Notice/notice.d.ts +69 -0
  197. package/dist/components/Notice/notice.d.ts.map +1 -0
  198. package/dist/components/Notice/notice.js +121 -0
  199. package/dist/components/Notice/notice.js.map +1 -0
  200. package/dist/components/NumberInput/number-input.d.ts +57 -0
  201. package/dist/components/NumberInput/number-input.d.ts.map +1 -0
  202. package/dist/components/NumberInput/number-input.js +131 -0
  203. package/dist/components/NumberInput/number-input.js.map +1 -0
  204. package/dist/components/OverflowIndicator/overflow-indicator.d.ts +23 -0
  205. package/dist/components/OverflowIndicator/overflow-indicator.d.ts.map +1 -0
  206. package/dist/components/OverflowIndicator/overflow-indicator.js +111 -0
  207. package/dist/components/OverflowIndicator/overflow-indicator.js.map +1 -0
  208. package/dist/components/PeoplePicker/avatar-stack-overflow.d.ts +57 -0
  209. package/dist/components/PeoplePicker/avatar-stack-overflow.d.ts.map +1 -0
  210. package/dist/components/PeoplePicker/avatar-stack-overflow.js +35 -0
  211. package/dist/components/PeoplePicker/avatar-stack-overflow.js.map +1 -0
  212. package/dist/components/PeoplePicker/people-picker-helpers.d.ts +7 -0
  213. package/dist/components/PeoplePicker/people-picker-helpers.d.ts.map +1 -0
  214. package/dist/components/PeoplePicker/people-picker-helpers.js +25 -0
  215. package/dist/components/PeoplePicker/people-picker-helpers.js.map +1 -0
  216. package/dist/components/PeoplePicker/people-picker.d.ts +77 -0
  217. package/dist/components/PeoplePicker/people-picker.d.ts.map +1 -0
  218. package/dist/components/PeoplePicker/people-picker.js +263 -0
  219. package/dist/components/PeoplePicker/people-picker.js.map +1 -0
  220. package/dist/components/PeoplePicker/person-display.d.ts +66 -0
  221. package/dist/components/PeoplePicker/person-display.d.ts.map +1 -0
  222. package/dist/components/PeoplePicker/person-display.js +203 -0
  223. package/dist/components/PeoplePicker/person-display.js.map +1 -0
  224. package/dist/components/Popover/popover.d.ts +50 -0
  225. package/dist/components/Popover/popover.d.ts.map +1 -0
  226. package/dist/components/Popover/popover.js +113 -0
  227. package/dist/components/Popover/popover.js.map +1 -0
  228. package/dist/components/ProgressBar/progress-bar.d.ts +37 -0
  229. package/dist/components/ProgressBar/progress-bar.d.ts.map +1 -0
  230. package/dist/components/ProgressBar/progress-bar.js +86 -0
  231. package/dist/components/ProgressBar/progress-bar.js.map +1 -0
  232. package/dist/components/RadioGroup/radio-group.d.ts +78 -0
  233. package/dist/components/RadioGroup/radio-group.d.ts.map +1 -0
  234. package/dist/components/RadioGroup/radio-group.js +153 -0
  235. package/dist/components/RadioGroup/radio-group.js.map +1 -0
  236. package/dist/components/Rating/rating.d.ts +46 -0
  237. package/dist/components/Rating/rating.d.ts.map +1 -0
  238. package/dist/components/Rating/rating.js +179 -0
  239. package/dist/components/Rating/rating.js.map +1 -0
  240. package/dist/components/ScrollArea/scroll-area.d.ts +45 -0
  241. package/dist/components/ScrollArea/scroll-area.d.ts.map +1 -0
  242. package/dist/components/ScrollArea/scroll-area.js +65 -0
  243. package/dist/components/ScrollArea/scroll-area.js.map +1 -0
  244. package/dist/components/SegmentedControl/segmented-control.d.ts +102 -0
  245. package/dist/components/SegmentedControl/segmented-control.d.ts.map +1 -0
  246. package/dist/components/SegmentedControl/segmented-control.js +171 -0
  247. package/dist/components/SegmentedControl/segmented-control.js.map +1 -0
  248. package/dist/components/Select/select.d.ts +102 -0
  249. package/dist/components/Select/select.d.ts.map +1 -0
  250. package/dist/components/Select/select.js +435 -0
  251. package/dist/components/Select/select.js.map +1 -0
  252. package/dist/components/SelectMenu/select-menu.d.ts +103 -0
  253. package/dist/components/SelectMenu/select-menu.d.ts.map +1 -0
  254. package/dist/components/SelectMenu/select-menu.js +239 -0
  255. package/dist/components/SelectMenu/select-menu.js.map +1 -0
  256. package/dist/components/SelectionControl/selection-item.d.ts +69 -0
  257. package/dist/components/SelectionControl/selection-item.d.ts.map +1 -0
  258. package/dist/components/SelectionControl/selection-item.js +142 -0
  259. package/dist/components/SelectionControl/selection-item.js.map +1 -0
  260. package/dist/components/Separator/separator.d.ts +17 -0
  261. package/dist/components/Separator/separator.d.ts.map +1 -0
  262. package/dist/components/Separator/separator.js +39 -0
  263. package/dist/components/Separator/separator.js.map +1 -0
  264. package/dist/components/Sheet/sheet.d.ts +56 -0
  265. package/dist/components/Sheet/sheet.d.ts.map +1 -0
  266. package/dist/components/Sheet/sheet.js +145 -0
  267. package/dist/components/Sheet/sheet.js.map +1 -0
  268. package/dist/components/Sidebar/sidebar.d.ts +195 -0
  269. package/dist/components/Sidebar/sidebar.d.ts.map +1 -0
  270. package/dist/components/Sidebar/sidebar.js +826 -0
  271. package/dist/components/Sidebar/sidebar.js.map +1 -0
  272. package/dist/components/Skeleton/skeleton.d.ts +16 -0
  273. package/dist/components/Skeleton/skeleton.d.ts.map +1 -0
  274. package/dist/components/Skeleton/skeleton.js +30 -0
  275. package/dist/components/Skeleton/skeleton.js.map +1 -0
  276. package/dist/components/Slider/slider.d.ts +48 -0
  277. package/dist/components/Slider/slider.d.ts.map +1 -0
  278. package/dist/components/Slider/slider.js +108 -0
  279. package/dist/components/Slider/slider.js.map +1 -0
  280. package/dist/components/Steps/steps.d.ts +71 -0
  281. package/dist/components/Steps/steps.d.ts.map +1 -0
  282. package/dist/components/Steps/steps.js +583 -0
  283. package/dist/components/Steps/steps.js.map +1 -0
  284. package/dist/components/Switch/switch.d.ts +112 -0
  285. package/dist/components/Switch/switch.d.ts.map +1 -0
  286. package/dist/components/Switch/switch.js +179 -0
  287. package/dist/components/Switch/switch.js.map +1 -0
  288. package/dist/components/Tabs/tabs.d.ts +104 -0
  289. package/dist/components/Tabs/tabs.d.ts.map +1 -0
  290. package/dist/components/Tabs/tabs.js +316 -0
  291. package/dist/components/Tabs/tabs.js.map +1 -0
  292. package/dist/components/Tag/tag.d.ts +86 -0
  293. package/dist/components/Tag/tag.d.ts.map +1 -0
  294. package/dist/components/Tag/tag.js +172 -0
  295. package/dist/components/Tag/tag.js.map +1 -0
  296. package/dist/components/Textarea/textarea.d.ts +74 -0
  297. package/dist/components/Textarea/textarea.d.ts.map +1 -0
  298. package/dist/components/Textarea/textarea.js +224 -0
  299. package/dist/components/Textarea/textarea.js.map +1 -0
  300. package/dist/components/TimePicker/time-columns.d.ts +46 -0
  301. package/dist/components/TimePicker/time-columns.d.ts.map +1 -0
  302. package/dist/components/TimePicker/time-columns.js +173 -0
  303. package/dist/components/TimePicker/time-columns.js.map +1 -0
  304. package/dist/components/TimePicker/time-picker.d.ts +94 -0
  305. package/dist/components/TimePicker/time-picker.d.ts.map +1 -0
  306. package/dist/components/TimePicker/time-picker.js +253 -0
  307. package/dist/components/TimePicker/time-picker.js.map +1 -0
  308. package/dist/components/Toast/toast.d.ts +61 -0
  309. package/dist/components/Toast/toast.d.ts.map +1 -0
  310. package/dist/components/Toast/toast.js +76 -0
  311. package/dist/components/Toast/toast.js.map +1 -0
  312. package/dist/components/Tooltip/tooltip.d.ts +20 -0
  313. package/dist/components/Tooltip/tooltip.d.ts.map +1 -0
  314. package/dist/components/Tooltip/tooltip.js +53 -0
  315. package/dist/components/Tooltip/tooltip.js.map +1 -0
  316. package/dist/components/TreeView/tree-view.d.ts +166 -0
  317. package/dist/components/TreeView/tree-view.d.ts.map +1 -0
  318. package/dist/components/TreeView/tree-view.js +617 -0
  319. package/dist/components/TreeView/tree-view.js.map +1 -0
  320. package/dist/hooks/use-controllable.d.ts +16 -0
  321. package/dist/hooks/use-controllable.d.ts.map +1 -0
  322. package/dist/hooks/use-controllable.js +26 -0
  323. package/dist/hooks/use-controllable.js.map +1 -0
  324. package/dist/hooks/use-is-narrow-viewport.d.ts +2 -0
  325. package/dist/hooks/use-is-narrow-viewport.d.ts.map +1 -0
  326. package/dist/hooks/use-is-narrow-viewport.js +19 -0
  327. package/dist/hooks/use-is-narrow-viewport.js.map +1 -0
  328. package/dist/hooks/use-is-touch-device.d.ts +8 -0
  329. package/dist/hooks/use-is-touch-device.d.ts.map +1 -0
  330. package/dist/hooks/use-is-touch-device.js +16 -0
  331. package/dist/hooks/use-is-touch-device.js.map +1 -0
  332. package/dist/hooks/use-overflow-items.d.ts +124 -0
  333. package/dist/hooks/use-overflow-items.d.ts.map +1 -0
  334. package/dist/hooks/use-overflow-items.js +97 -0
  335. package/dist/hooks/use-overflow-items.js.map +1 -0
  336. package/dist/index.d.ts +74 -0
  337. package/dist/index.d.ts.map +1 -0
  338. package/dist/index.js +371 -0
  339. package/dist/index.js.map +1 -0
  340. package/dist/lib/drag-visual.d.ts +158 -0
  341. package/dist/lib/drag-visual.d.ts.map +1 -0
  342. package/dist/lib/drag-visual.js +96 -0
  343. package/dist/lib/drag-visual.js.map +1 -0
  344. package/dist/lib/i18n/i18n-context.d.ts +105 -0
  345. package/dist/lib/i18n/i18n-context.d.ts.map +1 -0
  346. package/dist/lib/multi-select-ordering.d.ts +54 -0
  347. package/dist/lib/multi-select-ordering.d.ts.map +1 -0
  348. package/dist/lib/multi-select-ordering.js +13 -0
  349. package/dist/lib/multi-select-ordering.js.map +1 -0
  350. package/dist/lib/utils.d.ts +12 -0
  351. package/dist/lib/utils.d.ts.map +1 -0
  352. package/dist/lib/utils.js +79 -0
  353. package/dist/lib/utils.js.map +1 -0
  354. package/dist/patterns/element-anatomy/item-anatomy.d.ts +370 -0
  355. package/dist/patterns/element-anatomy/item-anatomy.d.ts.map +1 -0
  356. package/dist/patterns/element-anatomy/item-anatomy.js +272 -0
  357. package/dist/patterns/element-anatomy/item-anatomy.js.map +1 -0
  358. package/dist/patterns/header-canonical/chrome-header.d.ts +80 -0
  359. package/dist/patterns/header-canonical/chrome-header.d.ts.map +1 -0
  360. package/dist/patterns/header-canonical/chrome-header.js +75 -0
  361. package/dist/patterns/header-canonical/chrome-header.js.map +1 -0
  362. package/dist/patterns/horizontal-overflow/horizontal-overflow.d.ts +101 -0
  363. package/dist/patterns/horizontal-overflow/horizontal-overflow.d.ts.map +1 -0
  364. package/dist/patterns/horizontal-overflow/horizontal-overflow.js +105 -0
  365. package/dist/patterns/horizontal-overflow/horizontal-overflow.js.map +1 -0
  366. package/dist/patterns/overlay-surface/overlay-surface.d.ts +28 -0
  367. package/dist/patterns/overlay-surface/overlay-surface.d.ts.map +1 -0
  368. package/dist/patterns/overlay-surface/overlay-surface.js +85 -0
  369. package/dist/patterns/overlay-surface/overlay-surface.js.map +1 -0
  370. package/dist/patterns/resize-handle/resize-handle.d.ts +102 -0
  371. package/dist/patterns/resize-handle/resize-handle.d.ts.map +1 -0
  372. package/dist/patterns/resize-handle/resize-handle.js +74 -0
  373. package/dist/patterns/resize-handle/resize-handle.js.map +1 -0
  374. package/dist/react-day-picker.css +457 -0
  375. package/dist/stories-helpers/anatomy/anatomy-utils.d.ts +40 -0
  376. package/dist/stories-helpers/anatomy/anatomy-utils.d.ts.map +1 -0
  377. package/dist/tokens/elevation/overlay-geometry.d.ts +12 -0
  378. package/dist/tokens/elevation/overlay-geometry.d.ts.map +1 -0
  379. package/dist/tokens/elevation/overlay-geometry.js +7 -0
  380. package/dist/tokens/elevation/overlay-geometry.js.map +1 -0
  381. package/dist/tokens/motion/motion.d.ts +15 -0
  382. package/dist/tokens/motion/motion.d.ts.map +1 -0
  383. package/dist/tokens/motion/motion.js +9 -0
  384. package/dist/tokens/motion/motion.js.map +1 -0
  385. package/dist/tokens/uiSize/icon-size.d.ts +53 -0
  386. package/dist/tokens/uiSize/icon-size.d.ts.map +1 -0
  387. package/package.json +92 -0
  388. package/src/README.md +32 -0
  389. package/src/components/Accordion/accordion.tsx +104 -0
  390. package/src/components/Alert/alert.tsx +188 -0
  391. package/src/components/AppShell/_demo-helpers.tsx +198 -0
  392. package/src/components/AppShell/app-shell.tsx +364 -0
  393. package/src/components/AspectRatio/aspect-ratio.tsx +58 -0
  394. package/src/components/Avatar/avatar.tsx +368 -0
  395. package/src/components/Badge/badge.tsx +104 -0
  396. package/src/components/Breadcrumb/breadcrumb.tsx +619 -0
  397. package/src/components/BulkActionBar/bulk-action-bar.tsx +156 -0
  398. package/src/components/Button/button-group.tsx +96 -0
  399. package/src/components/Button/button.tsx +539 -0
  400. package/src/components/Calendar/calendar.tsx +411 -0
  401. package/src/components/Carousel/carousel.tsx +371 -0
  402. package/src/components/Chart/chart.tsx +376 -0
  403. package/src/components/Checkbox/checkbox-group.tsx +94 -0
  404. package/src/components/Checkbox/checkbox.tsx +237 -0
  405. package/src/components/Chip/chip.tsx +359 -0
  406. package/src/components/CircularProgress/circular-progress.tsx +204 -0
  407. package/src/components/Coachmark/coachmark.tsx +255 -0
  408. package/src/components/Combobox/combobox.tsx +826 -0
  409. package/src/components/Command/command.tsx +187 -0
  410. package/src/components/DataTable/active-editor-controller.ts +72 -0
  411. package/src/components/DataTable/cell-registry.tsx +520 -0
  412. package/src/components/DataTable/column-types.ts +180 -0
  413. package/src/components/DataTable/data-table-column-visibility-panel.tsx +261 -0
  414. package/src/components/DataTable/data-table-filter-panel.tsx +813 -0
  415. package/src/components/DataTable/data-table-interaction-layer.tsx +483 -0
  416. package/src/components/DataTable/data-table-sort-manager.tsx +210 -0
  417. package/src/components/DataTable/data-table.css +165 -0
  418. package/src/components/DataTable/data-table.tsx +2924 -0
  419. package/src/components/DataTable/filter-operators.ts +225 -0
  420. package/src/components/DataTable/filter-tree.ts +313 -0
  421. package/src/components/DataTable/lib/column-meta.ts +79 -0
  422. package/src/components/DateGrid/date-grid.tsx +209 -0
  423. package/src/components/DatePicker/date-picker.tsx +1114 -0
  424. package/src/components/DescriptionList/description-list.tsx +141 -0
  425. package/src/components/Dialog/dialog.tsx +267 -0
  426. package/src/components/DropdownMenu/dropdown-menu.tsx +475 -0
  427. package/src/components/Empty/empty.tsx +108 -0
  428. package/src/components/Field/field-context.ts +136 -0
  429. package/src/components/Field/field-types.ts +52 -0
  430. package/src/components/Field/field-wrapper.tsx +348 -0
  431. package/src/components/Field/field.tsx +535 -0
  432. package/src/components/FieldControlGroup/field-control-group.tsx +136 -0
  433. package/src/components/FileItem/file-item.tsx +322 -0
  434. package/src/components/FileUpload/file-upload.tsx +326 -0
  435. package/src/components/FileViewer/file-viewer-types.ts +76 -0
  436. package/src/components/FileViewer/file-viewer.tsx +1065 -0
  437. package/src/components/FileViewer/image-renderer.tsx +256 -0
  438. package/src/components/HoverCard/hover-card.tsx +79 -0
  439. package/src/components/Input/input.tsx +233 -0
  440. package/src/components/LinkInput/link-input.tsx +304 -0
  441. package/src/components/Menu/menu-item.tsx +334 -0
  442. package/src/components/NameCard/name-card.tsx +319 -0
  443. package/src/components/Notice/notice.tsx +196 -0
  444. package/src/components/NumberInput/number-input.tsx +203 -0
  445. package/src/components/OverflowIndicator/overflow-indicator.tsx +156 -0
  446. package/src/components/PeoplePicker/avatar-stack-overflow.ts +100 -0
  447. package/src/components/PeoplePicker/people-picker-helpers.ts +76 -0
  448. package/src/components/PeoplePicker/people-picker.tsx +455 -0
  449. package/src/components/PeoplePicker/person-display.tsx +358 -0
  450. package/src/components/Popover/popover.tsx +183 -0
  451. package/src/components/ProgressBar/progress-bar.tsx +157 -0
  452. package/src/components/README.md +58 -0
  453. package/src/components/RadioGroup/radio-group.tsx +261 -0
  454. package/src/components/Rating/rating.tsx +295 -0
  455. package/src/components/ScrollArea/scroll-area.tsx +110 -0
  456. package/src/components/SegmentedControl/segmented-control.tsx +304 -0
  457. package/src/components/Select/select.tsx +658 -0
  458. package/src/components/SelectMenu/select-menu.tsx +430 -0
  459. package/src/components/SelectionControl/selection-item.tsx +261 -0
  460. package/src/components/Separator/separator.tsx +48 -0
  461. package/src/components/Sheet/sheet.tsx +240 -0
  462. package/src/components/Sidebar/sidebar.tsx +1280 -0
  463. package/src/components/Skeleton/skeleton.tsx +35 -0
  464. package/src/components/Slider/slider.tsx +158 -0
  465. package/src/components/Steps/steps.tsx +850 -0
  466. package/src/components/Switch/switch.tsx +285 -0
  467. package/src/components/Tabs/tabs.tsx +515 -0
  468. package/src/components/Tag/tag.tsx +246 -0
  469. package/src/components/Textarea/textarea.tsx +280 -0
  470. package/src/components/TimePicker/time-columns.tsx +260 -0
  471. package/src/components/TimePicker/time-picker.tsx +419 -0
  472. package/src/components/Toast/toast.tsx +129 -0
  473. package/src/components/Tooltip/tooltip.tsx +68 -0
  474. package/src/components/TreeView/tree-view.tsx +1031 -0
  475. package/src/hooks/use-controllable.ts +40 -0
  476. package/src/hooks/use-is-narrow-viewport.ts +19 -0
  477. package/src/hooks/use-is-touch-device.ts +21 -0
  478. package/src/hooks/use-overflow-items.ts +256 -0
  479. package/src/index.ts +85 -0
  480. package/src/lib/README.md +82 -0
  481. package/src/lib/drag-visual.ts +272 -0
  482. package/src/lib/i18n/README.md +60 -0
  483. package/src/lib/i18n/i18n-context.tsx +129 -0
  484. package/src/lib/multi-select-ordering.ts +61 -0
  485. package/src/lib/utils.ts +93 -0
  486. package/src/patterns/README.md +67 -0
  487. package/src/patterns/element-anatomy/item-anatomy.tsx +744 -0
  488. package/src/patterns/header-canonical/chrome-header.tsx +175 -0
  489. package/src/patterns/header-canonical/header-canonical.css +27 -0
  490. package/src/patterns/horizontal-overflow/horizontal-overflow.tsx +217 -0
  491. package/src/patterns/overlay-surface/overlay-surface.tsx +191 -0
  492. package/src/patterns/resize-handle/resize-handle.tsx +188 -0
  493. package/src/stories-helpers/anatomy/anatomy-utils.tsx +64 -0
  494. package/src/styles/preset.css +31 -0
  495. package/src/styles/tokens.css +35 -0
  496. package/src/tokens/README.md +53 -0
  497. package/src/tokens/color/primitives.css +429 -0
  498. package/src/tokens/color/semantic.css +539 -0
  499. package/src/tokens/elevation/overlay-geometry.ts +13 -0
  500. package/src/tokens/layoutSpace/layoutSpace.css +36 -0
  501. package/src/tokens/motion/motion.css +30 -0
  502. package/src/tokens/motion/motion.ts +17 -0
  503. package/src/tokens/opacity/opacity.css +23 -0
  504. package/src/tokens/radius/radius.css +19 -0
  505. package/src/tokens/typography/typography.css +118 -0
  506. package/src/tokens/uiSize/icon-size.ts +52 -0
  507. package/src/tokens/uiSize/uiSize.css +125 -0
@@ -0,0 +1,1280 @@
1
+ // @benchmark-unverified-blanket: file-level retraction per M22 (d) — claims herein not individually URL-cited; treat as unverified visual/usage rumor unless retrofit per-claim. Hook escape preserved.
2
+ // code-quality-allow: file-size — foundational composite(SidebarProvider / Sidebar / SidebarMenu / SidebarGroup / SidebarMenuButton 等 10+ primitives 共享 context + 20+ sidebar-specific features;已是 foundational SSOT spec cap 800)
3
+ import * as React from "react"
4
+ import { Slot } from "@radix-ui/react-slot"
5
+ import * as CollapsiblePrimitive from "@radix-ui/react-collapsible"
6
+ import { cva, type VariantProps } from "class-variance-authority"
7
+ import type { LucideIcon } from "lucide-react"
8
+ import { ChevronDown, PanelLeft } from "lucide-react"
9
+
10
+ import { useIsNarrowViewport } from "@/design-system/hooks/use-is-narrow-viewport"
11
+ import { cn } from "@/lib/utils"
12
+ import { Badge } from "@/design-system/components/Badge/badge"
13
+ import { Button } from "@/design-system/components/Button/button"
14
+ import { Input } from "@/design-system/components/Input/input"
15
+ import { ScrollArea } from "@/design-system/components/ScrollArea/scroll-area"
16
+ import { Separator } from "@/design-system/components/Separator/separator"
17
+ import {
18
+ Sheet,
19
+ SheetContent,
20
+ SheetDescription,
21
+ SheetHeader,
22
+ SheetTitle,
23
+ } from "@/design-system/components/Sheet/sheet"
24
+ import { Skeleton } from "@/design-system/components/Skeleton/skeleton"
25
+ import { ChromeHeader } from "@/design-system/patterns/header-canonical/chrome-header"
26
+ import {
27
+ Tooltip,
28
+ TooltipContent,
29
+ TooltipTrigger,
30
+ } from "@/design-system/components/Tooltip/tooltip"
31
+ // Row primitive 共用常數與 helpers——單一 source of truth
32
+ import {
33
+ AVATAR_SIZE,
34
+ ICON_SIZE,
35
+ ItemIcon,
36
+ ItemLabel,
37
+ ItemInlineAction,
38
+ ItemInlineActionButton,
39
+ RowSizeProvider,
40
+ getUniformPrefixSlotStyle,
41
+ ROW_PADDING_BY_SIZE,
42
+ type RowSize,
43
+ type InlineActionConfig,
44
+ } from "@/design-system/patterns/element-anatomy/item-anatomy"
45
+
46
+ /**
47
+ * Sidebar
48
+ * ─────────────────────────────────────────────────────────────
49
+ * 本元件的 item / label 視覺規格刻意對齊 MenuItem(menuItemVariants)
50
+ * ——sidebar、dropdown-menu、select-menu 共用同一條 item-layout 公式:
51
+ *
52
+ * px = var(--layout-space-loose) (sidebar 脈絡;md=16 / lg=24)
53
+ * py = calc((--field-height-md - 1lh) / 2)
54
+ * font: text-body leading-compact font-medium
55
+ * hover / selected: bg-neutral-hover / bg-neutral-selected
56
+ *
57
+ * 這確保 SidebarGroupLabel(header 模式)和 SidebarMenuButton(互動模式)
58
+ * 擁有完全相同的 row height,消除「label 和 items 之間高度落差」的問題。
59
+ */
60
+
61
+ const SIDEBAR_COOKIE_NAME = "sidebar_state"
62
+ const SIDEBAR_COOKIE_MAX_AGE = 60 * 60 * 24 * 7
63
+ const SIDEBAR_KEYBOARD_SHORTCUT = "b"
64
+
65
+ // ── Context ────────────────────────────────────────────────────────────────
66
+
67
+ // Sidebar 的 size 直接用 item-layout pattern 的 RowSize type(sm/md/lg),
68
+ // 跟所有 row primitives 保持同一套 size 語意
69
+ type SidebarSize = RowSize
70
+
71
+ type SidebarContextProps = {
72
+ state: "expanded" | "collapsed"
73
+ open: boolean
74
+ setOpen: (open: boolean) => void
75
+ openMobile: boolean
76
+ setOpenMobile: (open: boolean) => void
77
+ isMobile: boolean
78
+ toggleSidebar: () => void
79
+ // 全 sidebar 共用的 size(sm/md/lg)——透過 SidebarProvider 一次設定,
80
+ // 下游所有 SidebarMenuButton / SidebarGroupLabel / SidebarMenuSkeleton 自動繼承。
81
+ size: SidebarSize
82
+ // Single-selection state:整個 sidebar 同時只有一個 active item。
83
+ // SidebarMenuButton 傳 `id` prop,自動從這裡算 isActive、自動 onClick 時 setActiveId。
84
+ // Consumer 可 controlled(傳 activeId + onActiveChange)或 uncontrolled(僅傳
85
+ // defaultActiveId)。Router-driven 的 sidebar 通常 controlled(URL → activeId)。
86
+ activeId: string | undefined
87
+ setActiveId: (id: string) => void
88
+ }
89
+
90
+ const SidebarContext = React.createContext<SidebarContextProps | null>(null)
91
+
92
+ function useSidebar() {
93
+ const context = React.useContext(SidebarContext)
94
+ if (!context) {
95
+ throw new Error("useSidebar must be used within a SidebarProvider.")
96
+ }
97
+ return context
98
+ }
99
+
100
+ // ── Provider ───────────────────────────────────────────────────────────────
101
+
102
+ // code-quality-allow: long-function — foundational composite main body — 拆 sub-fn 會複雜化 local state / ref / context binding
103
+ const SidebarProvider = React.forwardRef<
104
+ HTMLDivElement,
105
+ React.ComponentProps<"div"> & {
106
+ defaultOpen?: boolean
107
+ open?: boolean
108
+ onOpenChange?: (open: boolean) => void
109
+ /** Sidebar row 元件的預設尺寸(sm/md/lg),propagate 給所有 children。Default: "md" */
110
+ size?: SidebarSize
111
+ /** 當前 active item 的 id(controlled)——router-driven sidebar 從 URL 算出來傳進。 */
112
+ activeId?: string
113
+ /** 初始 active id(uncontrolled)。 */
114
+ defaultActiveId?: string
115
+ /** Active id 改變時的 callback(controlled 必傳)。 */
116
+ onActiveChange?: (id: string) => void
117
+ /**
118
+ * 全域 prefix 對齊。**預設 `false`**——只在「sidebar 內有大量 brand logo 跟一般 icon
119
+ * 混用,期待 label 齊左掃視」時 opt-in `true`。
120
+ *
121
+ * **典型 use case**(should opt-in):
122
+ * - Linear / Raycast 風格的 integration 清單:Home / Inbox(lucide icon)
123
+ * + GitHub / Slack / Figma(brand logo,24px)混在同一個 menu
124
+ * - App launcher / workspace switcher / connected apps,brand logo 為主體
125
+ *
126
+ * **不該 opt-in**:
127
+ * - 全 icon 主導覽 + 全 avatar user footer(語意不同層級,該分 group 不該強迫對齊)
128
+ * - 沒有真實混用情境只想要「視覺整齊」(預設行為已經對)
129
+ *
130
+ * 開啟後機制:CSS `:has()` 偵測 sidebar 子樹同時存在 `data-prefix-type="icon"` 和
131
+ * `"avatar"`(由 `<ItemIcon>` / `<ItemAvatar>` 自動標記)時,套用固定 24px 槽,
132
+ * 跨 menu / 跨 group 全域 label 對齊。不混用時零成本。
133
+ *
134
+ * 為什麼預設關閉而非 always-on auto:explicit-over-implicit——sidebar 排版行為
135
+ * 應該從寫的 prop 一眼看出,不藏 CSS 魔法。詳見 `sidebar.spec.md`。
136
+ */
137
+ uniformPrefix?: boolean
138
+ }
139
+ >(
140
+ // code-quality-allow: long-function — SidebarProvider 整合 dual-state(open + activeId)+ mobile sync + keyboard handler + context memo + style binding。拆 sub-fn 會打斷 React state binding chain + Context provider 順序語意(M21 prop variant test:拆分降低可讀性,單 fn 內 state lifecycle 一目了然)。當前 97 < cap 200。
141
+ (
142
+ {
143
+ defaultOpen = true,
144
+ open: openProp,
145
+ onOpenChange: setOpenProp,
146
+ size = "md",
147
+ activeId: activeIdProp,
148
+ defaultActiveId,
149
+ onActiveChange,
150
+ uniformPrefix = false,
151
+ className,
152
+ style,
153
+ children,
154
+ ...props
155
+ },
156
+ ref
157
+ ) => {
158
+ const [_activeId, _setActiveId] = React.useState<string | undefined>(defaultActiveId)
159
+ const activeId = activeIdProp ?? _activeId
160
+ const setActiveId = React.useCallback(
161
+ (id: string) => {
162
+ if (activeIdProp === undefined) _setActiveId(id)
163
+ onActiveChange?.(id)
164
+ },
165
+ [activeIdProp, onActiveChange]
166
+ )
167
+ const isMobile = useIsNarrowViewport()
168
+ const [openMobile, setOpenMobile] = React.useState(false)
169
+
170
+ const [_open, _setOpen] = React.useState(defaultOpen)
171
+ const open = openProp ?? _open
172
+ const setOpen = React.useCallback(
173
+ (value: boolean | ((value: boolean) => boolean)) => {
174
+ const openState = typeof value === "function" ? value(open) : value
175
+ if (setOpenProp) {
176
+ setOpenProp(openState)
177
+ } else {
178
+ _setOpen(openState)
179
+ }
180
+ document.cookie = `${SIDEBAR_COOKIE_NAME}=${openState}; path=/; max-age=${SIDEBAR_COOKIE_MAX_AGE}`
181
+ },
182
+ [setOpenProp, open]
183
+ )
184
+
185
+ const toggleSidebar = React.useCallback(() => {
186
+ return isMobile
187
+ ? setOpenMobile((o) => !o)
188
+ : setOpen((o) => !o)
189
+ }, [isMobile, setOpen, setOpenMobile])
190
+
191
+ React.useEffect(() => {
192
+ const handleKeyDown = (event: KeyboardEvent) => {
193
+ if (
194
+ event.key === SIDEBAR_KEYBOARD_SHORTCUT &&
195
+ (event.metaKey || event.ctrlKey)
196
+ ) {
197
+ event.preventDefault()
198
+ toggleSidebar()
199
+ }
200
+ }
201
+ window.addEventListener("keydown", handleKeyDown)
202
+ return () => window.removeEventListener("keydown", handleKeyDown)
203
+ }, [toggleSidebar])
204
+
205
+ // code-quality-allow: long-function — SidebarProvider 內 dual-state(open + activeId)+ mobile + handler refs + useMemo context 集中一處;拆 sub-fn 會打斷 useMemo dep stability + Context provider 順序。當前 97 < cap 200。Script naive heuristic 把 `const state = ?` 三元當 fn 起點誤報。
206
+ const state = open ? "expanded" : "collapsed"
207
+
208
+ const contextValue = React.useMemo<SidebarContextProps>(
209
+ () => ({
210
+ state,
211
+ open,
212
+ setOpen,
213
+ isMobile,
214
+ openMobile,
215
+ setOpenMobile,
216
+ toggleSidebar,
217
+ size,
218
+ activeId,
219
+ setActiveId,
220
+ }),
221
+ [state, open, setOpen, isMobile, openMobile, setOpenMobile, toggleSidebar, size, activeId, setActiveId]
222
+ )
223
+
224
+ // 不在此處包 TooltipProvider——shadcn 原版預設 delayDuration=0,會覆蓋
225
+ // 應用層的 delay 設定,造成 sidebar 內部 tooltip 行為跟其他地方不一致。
226
+ // TooltipProvider 應由應用層(Storybook preview.tsx、app root)統一設定。
227
+ // RowSizeProvider 讓整個 sidebar 子樹的 <ItemIcon> / <ItemAvatar> 都能 auto-size,
228
+ // asChild consumer 不需要再手動查 ICON_SIZE / AVATAR_SIZE。
229
+ //
230
+ // ── 全域 prefix 對齊(預設關閉,explicit opt-in)──
231
+ // false(預設):school A,各 menu 自然 prefix 寬度,跨 menu 不對齊
232
+ // true:opt-in CSS `:has()` auto-detect,混用時套用、不混用零成本(school B,Notion 慣例)
233
+ const slotStyle = getUniformPrefixSlotStyle(size)
234
+ const slotValue = slotStyle["--item-prefix-slot" as keyof typeof slotStyle]
235
+ // 2026-05-22 Approach 9 — per-row prefix size mirror(JS const → CSS var):
236
+ // `--item-icon-size` ← ICON_SIZE[size] (sm/md=16, lg=20)
237
+ // `--item-avatar-size` ← AVATAR_SIZE.inline[size] (sm=20, md/lg=24)
238
+ // 供 SidebarMenuButton collapsed pl 公式消費,讓所有 prefix 中心(icon/avatar)鎖回
239
+ // rail center = --sidebar-width-icon/2(per user mental model:rail anchor =
240
+ // GlobalHeader toggle geometry,sidebar 收合所有元素 cx 對齊此 rail center)。
241
+ // Rail anchor 自身不動(--sidebar-menu-icon-size 維持 1rem 固定,per globals.css :root)。
242
+ const wrapperStyle = React.useMemo<React.CSSProperties>(
243
+ () =>
244
+ ({
245
+ "--item-icon-size": `${ICON_SIZE[size]}px`,
246
+ "--item-avatar-size": `${AVATAR_SIZE.inline[size]}px`,
247
+ ...(uniformPrefix ? { "--mixed-prefix-slot": slotValue } : {}),
248
+ ...style,
249
+ } as React.CSSProperties),
250
+ [size, uniformPrefix, slotValue, style]
251
+ )
252
+
253
+ return (
254
+ <SidebarContext.Provider value={contextValue}>
255
+ <RowSizeProvider value={size}>
256
+ <div
257
+ style={wrapperStyle}
258
+ className={cn(
259
+ "group/sidebar-wrapper flex min-h-svh w-full",
260
+ // CSS :has() 偵測 — 只在 uniformPrefix=true(預設)時掛
261
+ // mixing detected → 同時把 --item-icon-size cascade 到 slot 寬(`!` important 蓋
262
+ // 過 wrapperStyle inline,讓 collapsed pl 公式知道「ItemPrefix wrapper 被撐到
263
+ // 24,effective prefix width = 24 非 16」)。
264
+ uniformPrefix &&
265
+ "has-[[data-prefix-type=icon]]:has-[[data-prefix-type=avatar]]:[--item-prefix-slot:var(--mixed-prefix-slot)] has-[[data-prefix-type=icon]]:has-[[data-prefix-type=avatar]]:![--item-icon-size:var(--mixed-prefix-slot)]",
266
+ className
267
+ )}
268
+ ref={ref}
269
+ {...props}
270
+ >
271
+ {children}
272
+ </div>
273
+ </RowSizeProvider>
274
+ </SidebarContext.Provider>
275
+ )
276
+ }
277
+ )
278
+ SidebarProvider.displayName = "SidebarProvider"
279
+
280
+ // ── Sidebar container ──────────────────────────────────────────────────────
281
+
282
+ const Sidebar = React.forwardRef<
283
+ HTMLDivElement,
284
+ React.ComponentProps<"div"> & {
285
+ side?: "left" | "right"
286
+ collapsible?: "offcanvas" | "icon" | "none"
287
+ /**
288
+ * Viewport top inset(2026-05-20 ship per AppShell `primary-header` unblock)。
289
+ * 預設 undefined = sidebar 從 viewport top 起算(`top:0 / h:svh`,當前 default)。
290
+ * 提供 CSS value(eg. `'var(--chrome-header-height)'` 或 `'48px'`)時,sidebar 改
291
+ * 從該值起算(`top: viewportInsetTop / height: calc(100svh - viewportInsetTop)`),
292
+ * 讓 global header 不被覆蓋(AppShell primary-header mode 必傳)。
293
+ * 對齊 Mantine `layout="default"` navbar 高度扣 header 慣例。
294
+ */
295
+ viewportInsetTop?: string
296
+ }
297
+ >(
298
+ (
299
+ {
300
+ side = "left",
301
+ collapsible = "offcanvas",
302
+ viewportInsetTop,
303
+ className,
304
+ children,
305
+ ...props
306
+ },
307
+ ref
308
+ ) => {
309
+ const { isMobile, state, openMobile, setOpenMobile } = useSidebar()
310
+ const insetStyle: React.CSSProperties | undefined = viewportInsetTop
311
+ ? { top: viewportInsetTop, height: `calc(100svh - ${viewportInsetTop})` }
312
+ : undefined
313
+
314
+ if (collapsible === "none") {
315
+ return (
316
+ <div
317
+ className={cn(
318
+ "flex h-full w-[var(--sidebar-width)] flex-col bg-surface text-foreground",
319
+ className
320
+ )}
321
+ ref={ref}
322
+ {...props}
323
+ >
324
+ {children}
325
+ </div>
326
+ )
327
+ }
328
+
329
+ if (isMobile) {
330
+ return (
331
+ <Sheet open={openMobile} onOpenChange={setOpenMobile} {...props}>
332
+ <SheetContent
333
+ data-sidebar="sidebar"
334
+ data-mobile="true"
335
+ className="w-[var(--sidebar-width)] bg-surface p-0 text-foreground [&>button]:hidden"
336
+ style={
337
+ {
338
+ "--sidebar-width": "var(--sidebar-width-mobile)",
339
+ } as React.CSSProperties
340
+ }
341
+ side={side}
342
+ >
343
+ <SheetHeader className="sr-only">
344
+ <SheetTitle>Sidebar</SheetTitle>
345
+ <SheetDescription>Displays the mobile sidebar.</SheetDescription>
346
+ </SheetHeader>
347
+ <div className="flex h-full w-full flex-col">{children}</div>
348
+ </SheetContent>
349
+ </Sheet>
350
+ )
351
+ }
352
+
353
+ return (
354
+ <div
355
+ ref={ref}
356
+ className="group peer hidden shrink-0 text-foreground md:block"
357
+ data-state={state}
358
+ data-collapsible={state === "collapsed" ? collapsible : ""}
359
+ data-side={side}
360
+ >
361
+ {/* Gap div:佔據 sidebar 實際寬度,推開主內容 */}
362
+ <div
363
+ className={cn(
364
+ "relative w-[var(--sidebar-width)] min-w-[var(--sidebar-width-min)] bg-transparent transition-[width,min-width] duration-200 ease-linear motion-reduce:duration-0",
365
+ "group-data-[collapsible=offcanvas]:!w-0 group-data-[collapsible=offcanvas]:!min-w-0",
366
+ "group-data-[side=right]:rotate-180",
367
+ "group-data-[collapsible=icon]:!w-[var(--sidebar-width-icon)] group-data-[collapsible=icon]:!min-w-0"
368
+ )}
369
+ />
370
+ <div
371
+ style={insetStyle}
372
+ className={cn(
373
+ // 2026-05-20 v3:`inset-y-0 h-svh` → 可被 viewportInsetTop prop override(per AppShell primary-header)
374
+ !viewportInsetTop && "inset-y-0 h-svh",
375
+ // 2026-05-21 v9 fix(per user 「右側分隔線跑去哪 + sidebar 寬度沒正確變化」):
376
+ // 補 `overflow-x-hidden` — C* refactor 主機制(outer narrows to sidebar-width-icon
377
+ // 時 clip inner 272px overflow,visual right border 顯示,主內容不被遮)。原 C*
378
+ // commit 漏加 className 只 comment 提及。
379
+ "overflow-x-hidden",
380
+ "fixed z-10 hidden w-[var(--sidebar-width)] min-w-[var(--sidebar-width-min)] transition-[left,right,width,min-width] duration-200 ease-linear motion-reduce:duration-0 md:flex",
381
+ "group-data-[collapsible=icon]:!min-w-0",
382
+ side === "left"
383
+ ? "left-0 group-data-[collapsible=offcanvas]:left-[calc(var(--sidebar-width)*-1)]"
384
+ : "right-0 group-data-[collapsible=offcanvas]:right-[calc(var(--sidebar-width)*-1)]",
385
+ "group-data-[collapsible=icon]:w-[var(--sidebar-width-icon)]",
386
+ "group-data-[side=left]:border-r group-data-[side=left]:border-divider",
387
+ "group-data-[side=right]:border-l group-data-[side=right]:border-divider",
388
+ className
389
+ )}
390
+ {...props}
391
+ >
392
+ {/* SidebarInner:永遠 full sidebar-width via inline style(Tailwind JIT 沒 compile
393
+ `min-w-[var(--sidebar-width)]` arbitrary class — 只 generated min-w-sidebar-width-min)。
394
+ Inline style 繞 JIT 確保 272px。Collapsed mode 只是 outer overflow-x:hidden clip 出
395
+ 左側 sidebar-width-icon = visible icon rail。 */}
396
+ <div
397
+ data-sidebar="sidebar"
398
+ style={{ width: 'var(--sidebar-width)', minWidth: 'var(--sidebar-width)' }}
399
+ className="flex h-full shrink-0 flex-col bg-surface"
400
+ >
401
+ {children}
402
+ </div>
403
+ </div>
404
+ </div>
405
+ )
406
+ }
407
+ )
408
+ Sidebar.displayName = "Sidebar"
409
+
410
+ // ── Trigger / Rail / Input ─────────────────────────────────────────────────
411
+
412
+ // SidebarTrigger — 用 Button 原生的 size="sm" iconOnly,
413
+ // 高度自動跟 density 走(field-height-sm @ md=28 / lg=32)
414
+ // 不 override size,避免 density 切換時 trigger 不變
415
+ const SidebarTrigger = React.forwardRef<
416
+ React.ElementRef<typeof Button>,
417
+ React.ComponentProps<typeof Button>
418
+ >(({ className, onClick, ...props }, ref) => {
419
+ const { toggleSidebar } = useSidebar()
420
+
421
+ return (
422
+ <Button
423
+ ref={ref}
424
+ data-sidebar="trigger"
425
+ variant="text"
426
+ size="sm"
427
+ iconOnly
428
+ startIcon={PanelLeft}
429
+ aria-label="Toggle Sidebar"
430
+ className={className}
431
+ onClick={(event) => {
432
+ onClick?.(event)
433
+ toggleSidebar()
434
+ }}
435
+ {...props}
436
+ />
437
+ )
438
+ })
439
+ SidebarTrigger.displayName = "SidebarTrigger"
440
+
441
+ // SidebarInput — 用 Input 元件原生高度(跟 density 自動走),不 override
442
+ const SidebarInput = React.forwardRef<
443
+ React.ElementRef<typeof Input>,
444
+ React.ComponentProps<typeof Input>
445
+ >(({ className, ...props }, ref) => {
446
+ return (
447
+ <Input
448
+ ref={ref}
449
+ data-sidebar="input"
450
+ className={cn("w-full", className)}
451
+ {...props}
452
+ />
453
+ )
454
+ })
455
+ SidebarInput.displayName = "SidebarInput"
456
+
457
+ // ── Shell regions ──────────────────────────────────────────────────────────
458
+
459
+ // SidebarHeader / SidebarFooter:
460
+ // - 固定高度 `var(--chrome-header-height)`(md=48 / lg=56,density-responsive)
461
+ // - 跨元件 chrome header 共享同一個 token,自動跟主內容 page header 對齊
462
+ // - 水平 padding 用 loose token(跟 items 的 px 對齊)
463
+ // - 邊框是結構邊界(分隔 fixed/scroll 區),full-width 不內縮
464
+ //
465
+ // 為什麼 density-responsive:chrome 裡放的 button 綁定 field-height token,
466
+ // 會隨 density 變大。Chrome 如果不跟著放大,lg density 下 padding 會被擠壓。
467
+ const SidebarHeader = React.forwardRef<
468
+ HTMLDivElement,
469
+ React.ComponentProps<"div"> & { withTabs?: boolean; tabsSlot?: React.ReactNode }
470
+ >(({ className, withTabs, tabsSlot, ...props }, ref) => {
471
+ return (
472
+ // 2026-05-21 v9 — restore main behavior(per user「不應該調整原本的樣式,只有說收起來的時候
473
+ // 要調整 header logo 的位置使其與其下 icon 水平置中」):
474
+ // - Expanded mode:ChromeHeader default `px-loose`(16px L+R),avatar 跟 menu icon 對齊
475
+ // - Collapsed mode:`!px-0 !justify-center` 拿掉 padding + 內容置中,WorkspaceBrand text
476
+ // 已 `group-data-[collapsible=icon]:hidden`,collapsed 只剩 avatar 24px,centered in 48px
477
+ // square → avatar.center = 24 = menu icon center.x ✓
478
+ // 完全 match main 行為(v7 leadingRail / v8 -ml-1 surgical 全撤回)。
479
+ <ChromeHeader
480
+ ref={ref}
481
+ withTabs={withTabs}
482
+ tabsSlot={tabsSlot}
483
+ data-sidebar="header"
484
+ className={cn(
485
+ // 2026-05-21 v14 — rail-derived collapsed padding,full SSOT cascade(per user directive
486
+ // 「avatar尺寸、menu item padding、menu icon尺寸任一變動都要正確連動」+ codex Layer B
487
+ // 比稿共識):
488
+ //
489
+ // 公式 derivation(rail-centered geometric identity):
490
+ // avatar.cx = pl + avatar/2 = (sidebar-width-icon - avatar)/2 + avatar/2 = sidebar-width-icon/2
491
+ // menu icon.cx = loose + icon-size/2
492
+ // sidebar-width-icon = 2*loose + icon-size → sidebar-width-icon/2 = loose + icon-size/2 ✓
493
+ //
494
+ // → 任何 avatar / loose / icon-size 改值,公式自動對齊 menu icon 中心(rail 幾何恆等)。
495
+ //
496
+ // SSOT 連動鏈:
497
+ // --layout-space-loose (density token) ─┐
498
+ // --sidebar-menu-icon-size ├→ --sidebar-width-icon (calc cascade)
499
+ // └→ 公式自動跟 ✓
500
+ // --chrome-header-avatar-size (header-canonical.css local token):公式 var 引用,
501
+ // JS 端透過 WorkspaceBrand RowSizeProvider value="md"
502
+ // + AVATAR_SIZE.inline.md spec-coupled 共識 sync。
503
+ //
504
+ // Numerical equivalence to v13 `loose-4` 公式(verified):
505
+ // md density: (48-24)/2 = 12 = loose-4 = 12 ✓ identical
506
+ // lg density: (64-24)/2 = 20 = loose-4 = 20 ✓ identical
507
+ // → v14 upgrade 純 SSOT chain robustness,0 視覺改變。
508
+ "group-data-[collapsible=icon]:!pl-[calc((var(--sidebar-width-icon)-var(--chrome-header-avatar-size))/2)]",
509
+ "group-data-[collapsible=icon]:!pr-0",
510
+ "transition-[padding] duration-200 ease-linear motion-reduce:duration-0",
511
+ className
512
+ )}
513
+ {...props}
514
+ />
515
+ )
516
+ })
517
+ SidebarHeader.displayName = "SidebarHeader"
518
+
519
+ // SidebarFooter — pinned 在 sidebar 底部的 menu group 容器,不是固定高度 chrome slot。
520
+ // 行為跟 SidebarGroup 類似(flex flex-col py-2),但永遠靠底(shrink-0)且有 border-t 分隔。
521
+ // Consumer 放 SidebarMenu + SidebarMenuButton,高度由內容決定(1~N 個 items)。
522
+ // 典型內容:user menu、settings、help、logout 等底部選項群組。
523
+ const SidebarFooter = React.forwardRef<
524
+ HTMLDivElement,
525
+ React.ComponentProps<"div">
526
+ >(({ className, ...props }, ref) => {
527
+ return (
528
+ <div
529
+ ref={ref}
530
+ data-sidebar="footer"
531
+ className={cn(
532
+ "flex shrink-0 flex-col py-2 border-t border-divider",
533
+ className
534
+ )}
535
+ {...props}
536
+ />
537
+ )
538
+ })
539
+ SidebarFooter.displayName = "SidebarFooter"
540
+
541
+ const SidebarSeparator = React.forwardRef<
542
+ React.ElementRef<typeof Separator>,
543
+ React.ComponentProps<typeof Separator>
544
+ >(({ className, ...props }, ref) => {
545
+ // 預設對齊 loose token:分隔兩個 scrollable group 時,跟 item 內容對齊。
546
+ // 分隔 fixed 元件(Header/Footer 與 Content)時,consumer 可傳 className 覆寫成 mx-0。
547
+ return (
548
+ <Separator
549
+ ref={ref}
550
+ data-sidebar="separator"
551
+ className={cn(
552
+ "mx-[var(--layout-space-loose)] w-auto bg-divider",
553
+ className
554
+ )}
555
+ {...props}
556
+ />
557
+ )
558
+ })
559
+ SidebarSeparator.displayName = "SidebarSeparator"
560
+
561
+ const SidebarContent = React.forwardRef<
562
+ HTMLDivElement,
563
+ React.ComponentProps<"div">
564
+ >(({ className, children, ...props }, ref) => {
565
+ return (
566
+ <div
567
+ ref={ref}
568
+ data-sidebar="content"
569
+ // SidebarContent 用 ScrollArea 處理長列表 scroll——跨 OS 一致不吃寬度(macOS
570
+ // overlay vs Windows/Linux always-visible 差異見 scroll-area.tsx 註解)。
571
+ // 呼吸空間和分隔線由 SidebarGroup 自己處理(對齊 MenuGroup 的 py-2 + [&+&]:border-t)。
572
+ // ScrollArea Root 本身 overflow-hidden,icon-collapsed 時不會露出 scroll chrome。
573
+ className={cn(
574
+ "flex min-h-0 flex-1 flex-col",
575
+ className
576
+ )}
577
+ {...props}
578
+ >
579
+ <ScrollArea className="flex-1">
580
+ <div className="flex flex-col">{children}</div>
581
+ </ScrollArea>
582
+ </div>
583
+ )
584
+ })
585
+ SidebarContent.displayName = "SidebarContent"
586
+
587
+ // ── Group ──────────────────────────────────────────────────────────────────
588
+
589
+ // SidebarGroupContext——讓 `SidebarGroupLabel` 和 `SidebarGroupContent` 知道當前
590
+ // 所在的 group 是否 collapsible,以自動切換渲染模式(label 變 trigger,content 變
591
+ // `Collapsible.Content`)。沒有 context 就是舊行為(非 collapsible 的 plain div)。
592
+ type SidebarGroupContextValue = {
593
+ collapsible: boolean
594
+ }
595
+ const SidebarGroupContext = React.createContext<SidebarGroupContextValue | null>(null)
596
+
597
+ function useSidebarGroup() {
598
+ return React.useContext(SidebarGroupContext)
599
+ }
600
+
601
+ /**
602
+ * SidebarGroup
603
+ *
604
+ * 預設是非互動的 plain group(div)。當 `collapsible` = true 時自動切換成 Radix
605
+ * Collapsible:SidebarGroupLabel 變 trigger、SidebarGroupContent 變 Content、
606
+ * 自動渲染 chevron 於 label 尾端、chevron 依 open state 旋轉。
607
+ *
608
+ * ── API 設計決策 ──
609
+ * 為什麼是 group 層級的 prop 而不是 label 層級?因為「group 是否可收合」是結構層
610
+ * 的決定,影響 group 所有子 primitive 的渲染模式(label 變 button、content 變
611
+ * animated container)。把 prop 放在 label 上會讓 content 不知道自己該不該被包,
612
+ * 形成跨元件的 prop drilling。放在 group 上用 context 傳遞是 React 的標準做法。
613
+ */
614
+ const SidebarGroup = React.forwardRef<
615
+ HTMLDivElement,
616
+ React.ComponentProps<"div"> & {
617
+ collapsible?: boolean
618
+ defaultOpen?: boolean
619
+ open?: boolean
620
+ onOpenChange?: (open: boolean) => void
621
+ }
622
+ >(({ className, collapsible = false, defaultOpen = true, open, onOpenChange, children, ...props }, ref) => {
623
+ const contextValue = React.useMemo<SidebarGroupContextValue>(
624
+ () => ({ collapsible }),
625
+ [collapsible]
626
+ )
627
+
628
+ // 非 collapsible 就是 plain div,舊行為完全不變
629
+ const baseClass = cn(
630
+ "relative flex w-full min-w-0 flex-col py-2",
631
+ "[&+&]:before:absolute [&+&]:before:top-0 [&+&]:before:left-[var(--layout-space-loose)] [&+&]:before:right-[var(--layout-space-loose)] [&+&]:before:h-px [&+&]:before:bg-divider [&+&]:before:content-['']",
632
+ className
633
+ )
634
+
635
+ if (!collapsible) {
636
+ return (
637
+ <SidebarGroupContext.Provider value={contextValue}>
638
+ <div ref={ref} data-sidebar="group" className={baseClass} {...props}>
639
+ {children}
640
+ </div>
641
+ </SidebarGroupContext.Provider>
642
+ )
643
+ }
644
+
645
+ // Collapsible 模式:Radix Collapsible.Root 當 group container
646
+ return (
647
+ <SidebarGroupContext.Provider value={contextValue}>
648
+ <CollapsiblePrimitive.Root
649
+ ref={ref}
650
+ data-sidebar="group"
651
+ defaultOpen={defaultOpen}
652
+ open={open}
653
+ onOpenChange={onOpenChange}
654
+ className={baseClass}
655
+ // Collapsible 在 icon 模式下整個隱藏(跟 TreeView 一樣——icon rail 沒空間放展開的 tree)
656
+ // Consumer 若要在 icon 模式顯示整個 group,自行傳 className 覆寫
657
+ {...(props as React.ComponentProps<typeof CollapsiblePrimitive.Root>)}
658
+ >
659
+ {children}
660
+ </CollapsiblePrimitive.Root>
661
+ </SidebarGroupContext.Provider>
662
+ )
663
+ })
664
+ SidebarGroup.displayName = "SidebarGroup"
665
+
666
+ const SidebarGroupContent = React.forwardRef<
667
+ HTMLDivElement,
668
+ React.ComponentProps<"div">
669
+ >(({ className, ...props }, ref) => {
670
+ const group = useSidebarGroup()
671
+ if (group?.collapsible) {
672
+ return (
673
+ <CollapsiblePrimitive.Content
674
+ // Radix 提供 data-state="open|closed",搭配 animate 做展開/收合動畫
675
+ // (若之後需要,可在這個 div 上加 CSS animation)
676
+ data-sidebar="group-content"
677
+ className={cn("w-full overflow-hidden", className)}
678
+ {...(props as React.ComponentProps<typeof CollapsiblePrimitive.Content>)}
679
+ >
680
+ <div ref={ref}>{props.children}</div>
681
+ </CollapsiblePrimitive.Content>
682
+ )
683
+ }
684
+ return (
685
+ <div
686
+ ref={ref}
687
+ data-sidebar="group-content"
688
+ className={cn("w-full", className)}
689
+ {...props}
690
+ />
691
+ )
692
+ })
693
+ SidebarGroupContent.displayName = "SidebarGroupContent"
694
+
695
+ // SidebarGroupLabel — 完全對齊 MenuItem header 的視覺 + TreeView 的 sm/md/lg 尺寸:
696
+ // - 跟 SidebarMenuButton 共用 item-layout 公式(同 py 公式、同 text size、同 icon size)
697
+ // - 唯一差異:font-medium + text-fg-muted + pointer-events-none(header 語意)
698
+ // 這樣 label 和 items 的 row height 在任何 size 下都完全對齊。
699
+ //
700
+ // Icon 模式:整列 display:none(不是 -mt + opacity 0,避免脆弱的 margin 計算)
701
+ const sidebarGroupLabelVariants = cva(
702
+ [
703
+ // items-start 對齊 item-layout 規則(跟 SidebarMenuButton / TreeItem / MenuItem 一致)
704
+ "flex w-full items-start gap-2",
705
+ "px-[var(--layout-space-loose)]",
706
+ "font-medium text-fg-muted",
707
+ "cursor-default select-none pointer-events-none outline-none",
708
+ // icon 模式:display:none 硬隱藏(跟 SidebarMenuButton label span 一致的策略)
709
+ "group-data-[collapsible=icon]:hidden",
710
+ ],
711
+ {
712
+ variants: {
713
+ // 消費 ROW_PADDING_BY_SIZE SSOT(跟 SidebarMenuButton / MenuItem / TreeItem 一致)
714
+ size: ROW_PADDING_BY_SIZE,
715
+ },
716
+ defaultVariants: { size: "md" },
717
+ }
718
+ )
719
+
720
+ const SidebarGroupLabel = React.forwardRef<
721
+ HTMLDivElement,
722
+ React.ComponentProps<"div"> & {
723
+ asChild?: boolean
724
+ /** ARIA label for the expand/collapse chevron (only when group is collapsible). Override for i18n. Default: 「展開或收合」 */
725
+ toggleAriaLabel?: string
726
+ } & VariantProps<typeof sidebarGroupLabelVariants>
727
+ >(({ className, asChild = false, size: sizeProp, children, toggleAriaLabel = '展開或收合' /* i18n-allow: DS default; consumer override via toggleAriaLabel prop */, ...props }, ref) => {
728
+ const { size: contextSize } = useSidebar()
729
+ const size = sizeProp ?? contextSize
730
+ const group = useSidebarGroup()
731
+
732
+ // Collapsible group:label 本身仍是 plain header(保留原本語意),
733
+ // chevron 是 suffix slot 裡的 inline action button,用 Radix Collapsible.Trigger asChild
734
+ // 包住 `ItemInlineActionButton`——視覺規格完全對齊 uiSize.spec.md「Inline Action」,
735
+ // 跟 SidebarMenuButton 的 suffix inline action 同一套 canonical 實作。
736
+ //
737
+ // **為什麼 chevron 是 inline action 而非整個 label 是 trigger**:
738
+ // 1. 同 sidebar 內的 inline action 視覺必須一致(fg-muted → foreground、hover bg、圓角)
739
+ // 2. Linear / Notion / Finder 等世界級 sidebar 都是「label 裝飾、chevron 互動」
740
+ // 3. 整個 label 當 trigger 會把 label 升格為 button,跟非 collapsible group 的 label
741
+ // 語意不一致(一個是 div、一個是 button),無障礙與視覺焦點都會跳動
742
+ if (group?.collapsible) {
743
+ return (
744
+ <div
745
+ ref={ref}
746
+ data-sidebar="group-label"
747
+ role="presentation"
748
+ className={cn(
749
+ sidebarGroupLabelVariants({ size }),
750
+ // Single-line group header:改用 items-center 讓 chevron button(16×16)跟 text 垂直精確置中。
751
+ // cva base 的 `items-start` 是 multi-line safety,單行 header 用不到,明確覆寫成 center。
752
+ "!items-center",
753
+ className
754
+ )}
755
+ {...props}
756
+ >
757
+ <span className="min-w-0 flex-1 truncate">{children}</span>
758
+ {/* Chevron:inline action button,Radix Collapsible.Trigger asChild 包住,
759
+ Radix 會 merge onClick + aria-expanded + data-state 到 button。
760
+ ── Suffix 位置的 chevron 用 accordion 慣例 ──
761
+ base = ChevronDown (`v`),open 時 rotate-180° → `^`。
762
+ 對齊 Radix Accordion / shadcn Collapsible / Material Expansion / Linear 的 section header。
763
+ (Prefix 位置的 chevron 如 TreeView 才用 ChevronRight + rotate-90 的 tree disclosure 慣例) */}
764
+ <CollapsiblePrimitive.Trigger asChild>
765
+ <ItemInlineActionButton
766
+ icon={ChevronDown}
767
+ aria-label={toggleAriaLabel}
768
+ className="pointer-events-auto ml-auto"
769
+ iconClassName="transition-transform duration-150 motion-reduce:duration-0 [[data-state=open]_&]:rotate-180"
770
+ />
771
+ </CollapsiblePrimitive.Trigger>
772
+ </div>
773
+ )
774
+ }
775
+
776
+ // 非 collapsible:plain label(div,pointer-events-none)
777
+ const Comp = asChild ? Slot : "div"
778
+ return (
779
+ <Comp
780
+ ref={ref}
781
+ data-sidebar="group-label"
782
+ role="presentation"
783
+ className={cn(sidebarGroupLabelVariants({ size }), className)}
784
+ {...props}
785
+ >
786
+ {children}
787
+ </Comp>
788
+ )
789
+ })
790
+ SidebarGroupLabel.displayName = "SidebarGroupLabel"
791
+
792
+ const SidebarGroupAction = React.forwardRef<
793
+ HTMLButtonElement,
794
+ React.ComponentProps<"button"> & { asChild?: boolean }
795
+ >(({ className, asChild = false, ...props }, ref) => {
796
+ const Comp = asChild ? Slot : "button"
797
+ return (
798
+ <Comp
799
+ ref={ref}
800
+ data-sidebar="group-action"
801
+ className={cn(
802
+ "absolute right-[var(--layout-space-loose)] top-2 flex aspect-square w-5 items-center justify-center rounded-md p-0 text-fg-muted outline-none ring-ring transition-colors hover:bg-neutral-hover hover:text-foreground focus-visible:ring-2 [&>svg]:size-4 [&>svg]:shrink-0",
803
+ "after:absolute after:-inset-2 after:md:hidden",
804
+ "group-data-[collapsible=icon]:hidden",
805
+ className
806
+ )}
807
+ {...props}
808
+ />
809
+ )
810
+ })
811
+ SidebarGroupAction.displayName = "SidebarGroupAction"
812
+
813
+ // ── Menu ───────────────────────────────────────────────────────────────────
814
+
815
+ // SidebarMenu — items 容器
816
+ //
817
+ // Prefix 對齊由 `SidebarProvider` 全域 auto-detect 處理(school B,Notion/Linear 慣例):
818
+ // 整個 sidebar 子樹同時存在 icon 和 avatar prefix 時,自動套用固定槽,跨 menu 跨 group 對齊。
819
+ // 若要關閉,在 `<SidebarProvider uniformPrefix={false}>` 全域控制。SidebarMenu 沒有
820
+ // per-menu 覆寫——沒有真實 use case,YAGNI。
821
+ const SidebarMenu = React.forwardRef<
822
+ HTMLUListElement,
823
+ React.ComponentProps<"ul">
824
+ >(({ className, ...props }, ref) => (
825
+ <ul
826
+ ref={ref}
827
+ data-sidebar="menu"
828
+ // 無 gap:items 連續緊貼(對齊 DropdownMenu / TreeView 的視覺節奏)
829
+ className={cn("flex w-full min-w-0 flex-col", className)}
830
+ {...props}
831
+ />
832
+ ))
833
+ SidebarMenu.displayName = "SidebarMenu"
834
+
835
+ const SidebarMenuItem = React.forwardRef<
836
+ HTMLLIElement,
837
+ React.ComponentProps<"li">
838
+ >(({ className, ...props }, ref) => (
839
+ <li
840
+ ref={ref}
841
+ data-sidebar="menu-item"
842
+ className={cn("group/menu-item relative", className)}
843
+ {...props}
844
+ />
845
+ ))
846
+ SidebarMenuItem.displayName = "SidebarMenuItem"
847
+
848
+ /**
849
+ * SidebarMenuButton
850
+ *
851
+ * 對齊 MenuItem / TreeItem 的 item-layout,支援 sm/md/lg 三種尺寸:
852
+ * - horizontal: px = var(--layout-space-loose)
853
+ * - vertical: py = calc((--field-height-{size} - 1lh) / 2)
854
+ * - typography: text-body leading-compact(sm/md)/ text-body-lg(lg)+ font-medium
855
+ * - icon: size-4 (sm/md) / size-5 (lg)
856
+ * - hover: bg-neutral-hover
857
+ * - selected: bg-neutral-selected(data-active=true)
858
+ *
859
+ * Icon 模式:button 保持 w-full 填滿 sidebar icon rail,鎖高為 field-height-{size},
860
+ * content 用 justify-center 居中,label span 以 display:none 硬隱藏。
861
+ */
862
+ const sidebarMenuButtonVariants = cva(
863
+ [
864
+ "peer/menu-button group/menu-button",
865
+ // items-start(跟 TreeItem / MenuItem 一致的 item-layout 規則):
866
+ // 多行 label 時 prefix 留在第一行不飄移。
867
+ // 單行 label 時(我們的預設情境,truncate = line-clamp-1),效果跟 items-center 完全相同,
868
+ // 因為 prefix 被包在 `h-[1lh] flex items-center` 容器,強制對齊第一行文字中線。
869
+ "flex w-full items-start gap-2 text-left overflow-hidden",
870
+ "px-[var(--layout-space-loose)]",
871
+ "font-medium text-fg-secondary",
872
+ "cursor-pointer select-none outline-none",
873
+ // 2026-05-21 v4 C* refactor(per codex M31 Layer C 比稿 final architecture):
874
+ // 撤回所有 `group-data-[collapsible=icon]:*` overrides。Sidebar outer overflow-x:hidden
875
+ // 自動 clip menu button 右側 label / badge / action,左側 icon 自然顯示在 sidebar-width-icon
876
+ // range 內。Row geometry 永遠 stable(px-loose + items-start + gap-2 + height auto),
877
+ // 無 discrete property switch → 無 fly / 無 flutter / 無 jitter。
878
+ //
879
+ // Label/badge/action 自然存在 DOM(a11y screen reader 仍讀到),只是 outer clip 視覺消失。
880
+ // Tooltip 仍在 collapsed state 顯示(per L1043-1055 既有 hidden 條件)。
881
+ //
882
+ // 對齊 MUI MiniDrawer + shadcn canonical(width morph + overflow clip + row geometry 不變)。
883
+ "transition-[background-color,color] duration-200 ease-linear motion-reduce:duration-0",
884
+ "hover:bg-neutral-hover hover:text-foreground",
885
+ "focus-visible:bg-neutral-hover focus-visible:text-foreground",
886
+ "disabled:pointer-events-none disabled:opacity-disabled",
887
+ "aria-disabled:pointer-events-none aria-disabled:opacity-disabled",
888
+ "data-[active=true]:bg-neutral-selected data-[active=true]:text-foreground",
889
+ "group-has-[[data-sidebar=menu-action]]/menu-item:pr-8",
890
+ // 2026-05-21 v5 restore label display:none(user 抓「label 沒消失」):
891
+ // C* outer overflow-x:hidden 理論 clip,但 label.x=40 在 sidebar-width-icon=48 內 → 首字
892
+ // 部分可見。display:none 是 instant 切換非 main animation,跟 C* 「不用 display:none 做主
893
+ // 動畫」不衝突(label 不參與 width 動畫,純 final state)。對齊 shadcn canonical。
894
+ "group-data-[collapsible=icon]:[&_[data-sidebar=menu-label]]:hidden",
895
+ // 2026-05-22 Approach 9 — 收合時所有 prefix center 鎖回 rail center
896
+ // (--sidebar-width-icon/2 = GlobalHeader toggle geometry SSOT)。
897
+ // 公式: pl = (rail - prefix-width) / 2,prefix-width 由 SidebarProvider 注入的
898
+ // --item-icon-size / --item-avatar-size CSS var(JS const mirror)提供。
899
+ // 任何 row size(sm/md/lg)× density(md/lg)× prefix type(icon/avatar)6 cell 全對齊。
900
+ "group-data-[collapsible=icon]:has-[[data-prefix-type=icon]]:!pl-[calc((var(--sidebar-width-icon)-var(--item-icon-size))/2)]",
901
+ "group-data-[collapsible=icon]:has-[[data-prefix-type=avatar]]:!pl-[calc((var(--sidebar-width-icon)-var(--item-avatar-size))/2)]",
902
+ ],
903
+ {
904
+ variants: {
905
+ // size:消費 ROW_PADDING_BY_SIZE SSOT(item-anatomy.tsx)+ Sidebar-specific collapsed height
906
+ // 前為 3 cva(menu / sidebar / tree)重複同一 py 公式,drift risk 已知。
907
+ // 改用 shared SSOT 後 formula 改一處全同步。
908
+ size: {
909
+ // 2026-05-21 v4 C*:撤回 `group-data-[collapsible=icon]:!h-[...]` height lock
910
+ // (per codex Layer C — row height 永遠 stable,collapsed 只 outer clip 視覺)
911
+ sm: ROW_PADDING_BY_SIZE.sm,
912
+ md: ROW_PADDING_BY_SIZE.md,
913
+ lg: ROW_PADDING_BY_SIZE.lg,
914
+ },
915
+ variant: {
916
+ /** 預設 — 導覽 item,參與 single-selection */
917
+ default: "",
918
+ /**
919
+ * Meta 命令 row(例:「查看更多」「載入更多」「Show all」「+ 新增專案」)。
920
+ *
921
+ * 語意上**不是導覽目的地**,是 section 底部的命令。規格:
922
+ * - 文字從 `text-fg-secondary` 退到 `text-fg-muted`(視覺重量下沉,hover 才升到 foreground)
923
+ * - **不該參與 single-selection**:不傳 `id`,TS 階段提醒;執行階段傳了也不會 active
924
+ * - `font-medium` 降為 `font-normal`(更輕,signal「這不是 primary 導覽」)
925
+ * - 世界級對照:Linear "Show N more"、Notion "Show N more"、Slack "Show more"、
926
+ * Gmail Labels "More"
927
+ */
928
+ meta: [
929
+ "font-normal text-fg-muted",
930
+ // meta variant 不該有 active 態;即使誤傳 isActive 也不啟動
931
+ "data-[active=true]:bg-transparent data-[active=true]:text-fg-muted",
932
+ ],
933
+ },
934
+ },
935
+ defaultVariants: { size: "md", variant: "default" },
936
+ }
937
+ )
938
+
939
+ // code-quality-allow: long-function — foundational composite main body — 拆 sub-fn 會複雜化 local state / ref / context binding
940
+ const SidebarMenuButton = React.forwardRef<
941
+ HTMLButtonElement,
942
+ Omit<React.ComponentProps<"button">, "id"> & {
943
+ asChild?: boolean
944
+ /**
945
+ * Item 的唯一識別(對齊 `SidebarProvider.activeId`)。**強烈建議傳**——
946
+ * 傳了之後 `isActive` 自動從 context 算、`onClick` 自動 setActiveId,
947
+ * 整個 sidebar 的 single-selection 自動成立,不會寫出啞 item。
948
+ */
949
+ id?: string
950
+ /**
951
+ * 手動覆寫 active 狀態(極少用)。預設從 `SidebarProvider.activeId === id` 自動算。
952
+ * 兩者都傳時以 `isActive` 為準。
953
+ */
954
+ isActive?: boolean
955
+ startIcon?: LucideIcon
956
+ tooltip?: string | React.ComponentProps<typeof TooltipContent>
957
+ /**
958
+ * Suffix slot 的 inline actions(宣告式 API,對齊 uiSize.spec.md「Inline Action」)。
959
+ * Host 自動用 `<ItemInlineAction>` 渲染,consumer 只宣告 intent。
960
+ * Icon 模式下自動隱藏。
961
+ */
962
+ inlineActions?: InlineActionConfig[]
963
+ /**
964
+ * 右側 actions slot(ReactNode)— escape hatch 供 consumer 放自訂元素
965
+ * (如 DropdownMenu trigger / 自訂 popover trigger / 多 tier 動作)。
966
+ *
967
+ * 跟 `inlineActions` 互斥(同時傳 `inlineActionsSlot` 會優先,`inlineActions` 被忽略)。
968
+ * 規則對齊 Input.endSlot canonical:90% case 用 `inlineActions` 宣告式 API,
969
+ * 10% config 表達不出時走 slot。
970
+ *
971
+ * Padding budget:slot mode 預留 1 icon 寬度的 paddingRight(覆寫多 icon 寬度需 consumer 自控 className)。
972
+ * Reveal / collapsed-hide / 絕對定位 chrome 跟 inlineActions 共用,consumer 不需重做。
973
+ */
974
+ inlineActionsSlot?: React.ReactNode
975
+ /**
976
+ * Inline actions 的顯示模式:
977
+ * - `false`(預設):永遠顯示
978
+ * - `"hover"`:row hover 時才淡入(TreeView 模式)
979
+ */
980
+ actionsReveal?: false | "hover"
981
+ } & VariantProps<typeof sidebarMenuButtonVariants>
982
+ >(
983
+ (
984
+ {
985
+ asChild = false,
986
+ id,
987
+ isActive: isActiveProp,
988
+ size: sizeProp,
989
+ variant = "default",
990
+ startIcon: StartIcon,
991
+ tooltip,
992
+ inlineActions,
993
+ inlineActionsSlot,
994
+ actionsReveal = false,
995
+ className,
996
+ children,
997
+ onClick,
998
+ ...props
999
+ },
1000
+ ref
1001
+ ) => {
1002
+ const Comp = asChild ? Slot : "button"
1003
+ // 沒傳 size 就從 SidebarProvider context 繼承(預設 md)
1004
+ const { isMobile, state, size: contextSize, activeId, setActiveId } = useSidebar()
1005
+ const size = sizeProp ?? contextSize
1006
+
1007
+ // Meta variant 永不參與 single-selection,即使誤傳 id / isActive
1008
+ const isMeta = variant === "meta"
1009
+
1010
+ // Active 狀態:明確傳的 isActive 優先;否則 `activeId === id` 自動算
1011
+ // Meta variant 永遠 false
1012
+ const isActive = isMeta ? false : (isActiveProp ?? (id !== undefined && activeId === id))
1013
+
1014
+ // Click handler:id 存在且非 meta 才 setActiveId(single-selection 自動建立),
1015
+ // 同時呼叫 consumer 傳入的 onClick
1016
+ // code-quality-allow: long-function — helper fn 結構緊密,拆 sub-fn 會跨 fn 傳 state 反而複雜
1017
+ const handleClick = React.useCallback(
1018
+ (e: React.MouseEvent<HTMLButtonElement>) => {
1019
+ if (!isMeta && id !== undefined) setActiveId(id)
1020
+ onClick?.(e)
1021
+ },
1022
+ [isMeta, id, setActiveId, onClick]
1023
+ )
1024
+
1025
+ // asChild 時,我們不能額外 wrap span / icon——Slot 要求單一 child。
1026
+ // 所以 asChild 的 consumer 自行放 icon + label(記得 prefix 要包在 h-[1lh] 容器,
1027
+ // label span 要有 data-sidebar="menu-label" attribute 才能參與 icon 模式自動隱藏)。
1028
+ //
1029
+ // Label span 在 icon 模式下透過 cva base 的 `[&_[data-sidebar=menu-label]]:hidden` display:none,
1030
+ // 這樣 flex 只剩 icon 一個 child,justify-center 可以真正置中。
1031
+ // Sidebar 寬度 transition 200ms 是唯一持續動畫,使用者視線跟著寬度走,不會察覺 label 的瞬切。
1032
+ // 用 `<ItemIcon>` / `<ItemLabel>` helper,**不直接寫 ItemPrefix wrap StartIcon**——
1033
+ // ItemIcon 內部會自動加 `data-prefix-type="icon"`,讓 SidebarProvider 的全域
1034
+ // `:has()` prefix-mix 偵測能命中。直接用 ItemPrefix 就會錯過這個 tag,
1035
+ // 全域對齊功能對 SidebarMenuButton 路徑失效(曾經發生過的 bug)。
1036
+ const content = asChild ? (
1037
+ children
1038
+ ) : (
1039
+ <>
1040
+ {StartIcon && <ItemIcon icon={StartIcon} />}
1041
+ <ItemLabel>{children}</ItemLabel>
1042
+ </>
1043
+ )
1044
+
1045
+ const hasSlot = !!inlineActionsSlot
1046
+ const hasActions = hasSlot || (!!inlineActions && inlineActions.length > 0)
1047
+
1048
+ // 計算 suffix 所佔寬度:N×icon + (N-1)×gap-2(8px),再加 gap-2 跟 label 之間的間隔
1049
+ // Slot mode 預設按 1 icon 預留(consumer 寬度自控)
1050
+ const iconSz = ICON_SIZE[size ?? "md"]
1051
+ const n = hasSlot ? 1 : (inlineActions?.length ?? 0)
1052
+ const suffixContentWidth = n > 0 ? n * iconSz + (n - 1) * 8 : 0
1053
+ // Button 的 paddingRight = loose + suffix 寬度 + gap-2
1054
+ // 用 CSS calc 表達 loose token,不硬寫 px(loose 會隨 density 變)
1055
+ const buttonPaddingRight = hasActions
1056
+ ? `calc(var(--layout-space-loose) + ${suffixContentWidth}px + 0.5rem)`
1057
+ : undefined
1058
+
1059
+ const button = (
1060
+ <Comp
1061
+ ref={ref}
1062
+ data-sidebar="menu-button"
1063
+ data-active={isActive}
1064
+ className={cn(sidebarMenuButtonVariants({ size, variant }), className)}
1065
+ style={hasActions ? { paddingRight: buttonPaddingRight } : undefined}
1066
+ onClick={handleClick}
1067
+ {...props}
1068
+ >
1069
+ {content}
1070
+ </Comp>
1071
+ )
1072
+
1073
+ // Suffix inline actions——絕對定位在 menu-item 右邊,
1074
+ // 跟 button 同層(不是 button 的 child,避免巢狀 button)。
1075
+ const suffixNode = hasActions ? (
1076
+ <span
1077
+ data-sidebar="menu-inline-actions"
1078
+ className={cn(
1079
+ "absolute top-1/2 -translate-y-1/2 flex items-center gap-2",
1080
+ "right-[var(--layout-space-loose)]",
1081
+ // Icon 模式隱藏(跟 SidebarMenuBadge / SidebarMenuAction 一致)
1082
+ "group-data-[collapsible=icon]:hidden",
1083
+ // hover-reveal:滑鼠 hover 或鍵盤 focus(但不是 mouse click 的 focus)時顯示。
1084
+ // 用 `:has(:focus-visible)` 而非 `:focus-within`——focus-within 會被
1085
+ // mouse click 觸發,導致 click 之後 actions 永久顯示直到焦點移走;
1086
+ // focus-visible 只在鍵盤 tab 時啟動,mouse click 不會觸發,符合使用者直覺。
1087
+ actionsReveal === "hover" &&
1088
+ "opacity-0 group-hover/menu-item:opacity-100 group-has-[:focus-visible]/menu-item:opacity-100 transition-opacity duration-150"
1089
+ )}
1090
+ >
1091
+ {hasSlot
1092
+ ? inlineActionsSlot
1093
+ : inlineActions!.map((action, i) => (
1094
+ <ItemInlineAction key={action.label + i} action={action} />
1095
+ ))}
1096
+ </span>
1097
+ ) : null
1098
+
1099
+ // code-quality-allow: long-function — helper fn 結構緊密,拆 sub-fn 會跨 fn 傳 state 反而複雜
1100
+ const buttonWithTooltip = tooltip ? (
1101
+ (() => {
1102
+ const tooltipProps =
1103
+ typeof tooltip === "string" ? { children: tooltip } : tooltip
1104
+ return (
1105
+ <Tooltip>
1106
+ <TooltipTrigger asChild>{button}</TooltipTrigger>
1107
+ <TooltipContent
1108
+ side="right"
1109
+ align="center"
1110
+ hidden={state !== "collapsed" || isMobile}
1111
+ {...tooltipProps}
1112
+ />
1113
+ </Tooltip>
1114
+ )
1115
+ })()
1116
+ ) : (
1117
+ button
1118
+ )
1119
+
1120
+ if (!suffixNode) return buttonWithTooltip
1121
+ return (
1122
+ <>
1123
+ {buttonWithTooltip}
1124
+ {suffixNode}
1125
+ </>
1126
+ )
1127
+ }
1128
+ )
1129
+ SidebarMenuButton.displayName = "SidebarMenuButton"
1130
+
1131
+ const SidebarMenuAction = React.forwardRef<
1132
+ HTMLButtonElement,
1133
+ React.ComponentProps<"button"> & {
1134
+ asChild?: boolean
1135
+ showOnHover?: boolean
1136
+ }
1137
+ >(({ className, asChild = false, showOnHover = false, ...props }, ref) => {
1138
+ const Comp = asChild ? Slot : "button"
1139
+
1140
+ return (
1141
+ <Comp
1142
+ ref={ref}
1143
+ data-sidebar="menu-action"
1144
+ className={cn(
1145
+ "absolute right-[var(--layout-space-loose)] top-1/2 -translate-y-1/2 flex aspect-square w-5 items-center justify-center rounded-md p-0 text-fg-muted outline-none ring-ring transition-colors hover:bg-neutral-hover hover:text-foreground focus-visible:ring-2 [&>svg]:size-4 [&>svg]:shrink-0",
1146
+ "after:absolute after:-inset-2 after:md:hidden",
1147
+ "group-data-[collapsible=icon]:hidden",
1148
+ showOnHover &&
1149
+ "group-focus-within/menu-item:opacity-100 group-hover/menu-item:opacity-100 data-[state=open]:opacity-100 peer-data-[active=true]/menu-button:text-foreground md:opacity-0",
1150
+ className
1151
+ )}
1152
+ {...props}
1153
+ />
1154
+ )
1155
+ })
1156
+ SidebarMenuAction.displayName = "SidebarMenuAction"
1157
+
1158
+ // SidebarMenuBadge — 重用專案的 Badge 元件,絕對定位在 menu item 右側
1159
+ // Consumer 傳入 Badge 的所有 props(count / variant / max 等)
1160
+ const SidebarMenuBadge = React.forwardRef<
1161
+ React.ElementRef<typeof Badge>,
1162
+ React.ComponentProps<typeof Badge>
1163
+ >(({ className, ...props }, ref) => (
1164
+ <Badge
1165
+ ref={ref}
1166
+ data-sidebar="menu-badge"
1167
+ className={cn(
1168
+ "pointer-events-none absolute right-[var(--layout-space-loose)] top-1/2 -translate-y-1/2",
1169
+ "group-data-[collapsible=icon]:hidden",
1170
+ className
1171
+ )}
1172
+ {...props}
1173
+ />
1174
+ ))
1175
+ SidebarMenuBadge.displayName = "SidebarMenuBadge"
1176
+
1177
+ // SidebarMenuSkeleton — 對齊 SidebarMenuButton 的 item-layout 公式,支援 sm/md/lg
1178
+ // 沒傳 size 就從 context 繼承,確保 loading 狀態跟實際 item 同高不跳動
1179
+ const sidebarMenuSkeletonVariants = cva(
1180
+ [
1181
+ "flex items-start gap-2",
1182
+ "px-[var(--layout-space-loose)]",
1183
+ ],
1184
+ {
1185
+ variants: {
1186
+ // 消費 ROW_PADDING_BY_SIZE SSOT + skeleton-icon size modifier(Sidebar-specific)
1187
+ // 注:skeleton 無文字,typography class 雖被帶入但無害,保持公式一處同步
1188
+ size: {
1189
+ sm: [ROW_PADDING_BY_SIZE.sm, "[&>[data-sidebar=menu-skeleton-icon]]:size-4"],
1190
+ md: [ROW_PADDING_BY_SIZE.md, "[&>[data-sidebar=menu-skeleton-icon]]:size-4"],
1191
+ lg: [ROW_PADDING_BY_SIZE.lg, "[&>[data-sidebar=menu-skeleton-icon]]:size-5"],
1192
+ },
1193
+ },
1194
+ defaultVariants: { size: "md" },
1195
+ }
1196
+ )
1197
+
1198
+ const SidebarMenuSkeleton = React.forwardRef<
1199
+ HTMLDivElement,
1200
+ React.ComponentProps<"div"> & {
1201
+ showIcon?: boolean
1202
+ } & VariantProps<typeof sidebarMenuSkeletonVariants>
1203
+ >(({ className, showIcon = false, size: sizeProp, ...props }, ref) => {
1204
+ const { size: contextSize } = useSidebar()
1205
+ const size = sizeProp ?? contextSize
1206
+ const width = React.useMemo(() => {
1207
+ return `${Math.floor(Math.random() * 40) + 50}%`
1208
+ }, [])
1209
+
1210
+ return (
1211
+ <div
1212
+ ref={ref}
1213
+ data-sidebar="menu-skeleton"
1214
+ className={cn(sidebarMenuSkeletonVariants({ size }), className)}
1215
+ {...props}
1216
+ >
1217
+ {showIcon && (
1218
+ <Skeleton
1219
+ className="rounded-md shrink-0"
1220
+ data-sidebar="menu-skeleton-icon"
1221
+ />
1222
+ )}
1223
+ <Skeleton
1224
+ className="h-4 max-w-[var(--skeleton-width)] flex-1"
1225
+ data-sidebar="menu-skeleton-text"
1226
+ style={
1227
+ {
1228
+ "--skeleton-width": width,
1229
+ } as React.CSSProperties
1230
+ }
1231
+ />
1232
+ </div>
1233
+ )
1234
+ })
1235
+ SidebarMenuSkeleton.displayName = "SidebarMenuSkeleton"
1236
+
1237
+ // Story auto-compile metadata — Phase 1 mechanical migration(2026-04-24)
1238
+ // Phase 2 fill needed: purpose descriptions + when rationale + world-class refs
1239
+ export const sidebarMeta = {
1240
+ component: 'Sidebar',
1241
+ family: null, // non-family composite / overlay / layout
1242
+ variants: {
1243
+ default: { when: '標準導覽 row,參與 single-selection' },
1244
+ meta: { when: 'Section 底部命令 row(Show more / 新增),不參與 selection' },
1245
+ },
1246
+ sizes: {
1247
+ sm: { px: 28, when: '次導覽 / 設定頁 / 緊湊空間' },
1248
+ md: { px: 32, when: '預設 — 應用程式主導覽' },
1249
+ lg: { px: 36, when: '重要主導覽 / icon-prominent workspace switcher' },
1250
+ },
1251
+ states: ['default', 'hover', 'active', 'focus-visible', 'disabled'],
1252
+ tokens: {
1253
+ bg: ['bg-neutral-hover', 'bg-surface', 'bg-transparent'],
1254
+ fg: ['text-fg-muted', 'text-fg-secondary', 'text-foreground'],
1255
+ ring: ['ring-ring'],
1256
+ },
1257
+ defaultSize: 'md',
1258
+ } as const
1259
+
1260
+ export {
1261
+ Sidebar,
1262
+ SidebarContent,
1263
+ SidebarFooter,
1264
+ SidebarGroup,
1265
+ SidebarGroupAction,
1266
+ SidebarGroupContent,
1267
+ SidebarGroupLabel,
1268
+ SidebarHeader,
1269
+ SidebarInput,
1270
+ SidebarMenu,
1271
+ SidebarMenuAction,
1272
+ SidebarMenuBadge,
1273
+ SidebarMenuButton,
1274
+ SidebarMenuItem,
1275
+ SidebarMenuSkeleton,
1276
+ SidebarProvider,
1277
+ SidebarSeparator,
1278
+ SidebarTrigger,
1279
+ useSidebar,
1280
+ }