@sableui/react 0.6.2 → 0.7.1

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 (642) hide show
  1. package/AGENTS.md +2 -2
  2. package/CHANGELOG.md +315 -0
  3. package/CONTRIBUTING.md +16 -10
  4. package/DESIGN.md +3 -3
  5. package/README.md +3 -3
  6. package/bin/sable-lint.mjs +18 -0
  7. package/bin/sable.mjs +470 -62
  8. package/dist/blocks/api-keys-block.js +44 -0
  9. package/dist/blocks/api-keys-block.meta.js +21 -0
  10. package/dist/blocks/chat-thread-block.js +17 -0
  11. package/dist/blocks/chat-thread-block.meta.js +20 -0
  12. package/dist/blocks/command-palette-block.js +24 -0
  13. package/dist/blocks/command-palette-block.meta.js +21 -0
  14. package/dist/blocks/cta-band-block.js +27 -0
  15. package/dist/blocks/cta-band-block.meta.js +20 -0
  16. package/dist/blocks/danger-zone-block.js +39 -0
  17. package/dist/blocks/danger-zone-block.meta.js +21 -0
  18. package/dist/blocks/device-comparison-block.js +69 -0
  19. package/dist/blocks/device-comparison-block.meta.js +21 -0
  20. package/dist/blocks/event-card-block.js +40 -0
  21. package/dist/blocks/event-card-block.meta.js +21 -0
  22. package/dist/blocks/faq-block.js +38 -0
  23. package/dist/blocks/faq-block.meta.js +20 -0
  24. package/dist/blocks/feature-grid-block.js +38 -0
  25. package/dist/blocks/feature-grid-block.meta.js +21 -0
  26. package/dist/blocks/feedback-form-block.js +30 -0
  27. package/dist/blocks/feedback-form-block.meta.js +21 -0
  28. package/dist/blocks/goal-progress-block.js +24 -0
  29. package/dist/blocks/goal-progress-block.meta.js +21 -0
  30. package/dist/blocks/hero-block.js +33 -0
  31. package/dist/blocks/hero-block.meta.js +21 -0
  32. package/dist/blocks/index.js +162 -1964
  33. package/dist/blocks/invite-team-block.js +36 -0
  34. package/dist/blocks/invite-team-block.meta.js +20 -0
  35. package/dist/blocks/invoice-history-block.js +41 -0
  36. package/dist/blocks/invoice-history-block.meta.js +21 -0
  37. package/dist/blocks/kpi-sparkline-block.js +29 -0
  38. package/dist/blocks/kpi-sparkline-block.meta.js +20 -0
  39. package/dist/blocks/media-player-block.js +68 -0
  40. package/dist/blocks/media-player-block.meta.js +21 -0
  41. package/dist/blocks/newsletter-block.js +35 -0
  42. package/dist/blocks/newsletter-block.meta.js +21 -0
  43. package/dist/blocks/not-found-block.js +27 -0
  44. package/dist/blocks/not-found-block.meta.js +20 -0
  45. package/dist/blocks/notification-list-block.js +32 -0
  46. package/dist/blocks/notification-list-block.meta.js +21 -0
  47. package/dist/blocks/notification-settings-block.js +25 -0
  48. package/dist/blocks/notification-settings-block.meta.js +20 -0
  49. package/dist/blocks/order-tracking-block.js +26 -0
  50. package/dist/blocks/order-tracking-block.meta.js +20 -0
  51. package/dist/blocks/pricing-card-block.js +46 -0
  52. package/dist/blocks/pricing-card-block.meta.js +21 -0
  53. package/dist/blocks/pricing-table-block.js +88 -0
  54. package/dist/blocks/pricing-table-block.meta.js +20 -0
  55. package/dist/blocks/product-card-block.js +26 -0
  56. package/dist/blocks/product-card-block.meta.js +21 -0
  57. package/dist/blocks/profile-card-block.js +58 -0
  58. package/dist/blocks/profile-card-block.meta.js +21 -0
  59. package/dist/blocks/profile-form-block.js +70 -0
  60. package/dist/blocks/profile-form-block.meta.js +21 -0
  61. package/dist/blocks/revenue-breakdown-block.js +38 -0
  62. package/dist/blocks/revenue-breakdown-block.meta.js +21 -0
  63. package/dist/blocks/sign-in-block.js +38 -0
  64. package/dist/blocks/sign-in-block.meta.js +21 -0
  65. package/dist/blocks/sign-up-block.js +48 -0
  66. package/dist/blocks/sign-up-block.meta.js +21 -0
  67. package/dist/blocks/site-footer-block.js +54 -0
  68. package/dist/blocks/site-footer-block.meta.js +20 -0
  69. package/dist/blocks/stat-row-block.js +28 -0
  70. package/dist/blocks/stat-row-block.meta.js +21 -0
  71. package/dist/blocks/steps-block.js +37 -0
  72. package/dist/blocks/steps-block.meta.js +20 -0
  73. package/dist/blocks/task-list-block.js +52 -0
  74. package/dist/blocks/task-list-block.meta.js +21 -0
  75. package/dist/blocks/team-members-block.js +49 -0
  76. package/dist/blocks/team-members-block.meta.js +21 -0
  77. package/dist/blocks/testimonial-block.js +49 -0
  78. package/dist/blocks/testimonial-block.meta.js +20 -0
  79. package/dist/blocks/traffic-sources-block.js +39 -0
  80. package/dist/blocks/traffic-sources-block.meta.js +21 -0
  81. package/dist/blocks/traffic-trend-block.js +61 -0
  82. package/dist/blocks/traffic-trend-block.meta.js +21 -0
  83. package/dist/blocks/upload-list-block.js +39 -0
  84. package/dist/blocks/upload-list-block.meta.js +21 -0
  85. package/dist/blocks/verify-code-block.js +28 -0
  86. package/dist/blocks/verify-code-block.meta.js +21 -0
  87. package/dist/blocks/wallet-block.js +48 -0
  88. package/dist/blocks/wallet-block.meta.js +21 -0
  89. package/dist/components/accordion/accordion.js +63 -0
  90. package/dist/components/accordion/accordion.meta.js +155 -0
  91. package/dist/components/accordion/index.js +15 -0
  92. package/dist/components/app-header/app-header.js +30 -0
  93. package/dist/components/app-header/app-header.meta.js +88 -0
  94. package/dist/components/app-header/index.js +7 -0
  95. package/dist/components/app-shell/app-shell-dock.js +37 -0
  96. package/dist/components/app-shell/app-shell.js +67 -0
  97. package/dist/components/app-shell/app-shell.meta.js +176 -0
  98. package/dist/components/app-shell/index.js +7 -0
  99. package/dist/components/area-chart/area-chart.js +10 -0
  100. package/dist/components/area-chart/area-chart.meta.js +218 -0
  101. package/dist/components/area-chart/index.js +7 -0
  102. package/dist/components/attachment/attachment.js +176 -0
  103. package/dist/components/attachment/attachment.meta.js +179 -0
  104. package/dist/components/attachment/attachment.variants.js +47 -0
  105. package/dist/components/attachment/index.js +7 -0
  106. package/dist/components/avatar/avatar.js +192 -0
  107. package/dist/components/avatar/avatar.meta.js +233 -0
  108. package/dist/components/avatar/avatar.variants.js +44 -0
  109. package/dist/components/avatar/index.js +22 -0
  110. package/dist/components/badge/badge.js +26 -0
  111. package/dist/components/badge/badge.meta.js +171 -0
  112. package/dist/components/badge/badge.variants.js +103 -0
  113. package/dist/components/badge/index.js +11 -0
  114. package/dist/components/banner/banner.js +76 -0
  115. package/dist/components/banner/banner.meta.js +172 -0
  116. package/dist/components/banner/banner.variants.js +44 -0
  117. package/dist/components/banner/index.js +9 -0
  118. package/dist/components/bar-chart/bar-chart.js +10 -0
  119. package/dist/components/bar-chart/bar-chart.meta.js +245 -0
  120. package/dist/components/bar-chart/index.js +7 -0
  121. package/dist/components/bottom-nav/bottom-nav.js +63 -0
  122. package/dist/components/bottom-nav/bottom-nav.meta.js +95 -0
  123. package/dist/components/bottom-nav/index.js +11 -0
  124. package/dist/components/breadcrumbs/breadcrumbs.js +90 -0
  125. package/dist/components/breadcrumbs/breadcrumbs.meta.js +111 -0
  126. package/dist/components/breadcrumbs/index.js +17 -0
  127. package/dist/components/browser/browser.js +39 -0
  128. package/dist/components/browser/browser.meta.js +80 -0
  129. package/dist/components/browser/index.js +7 -0
  130. package/dist/components/bubble/bubble.js +110 -0
  131. package/dist/components/bubble/bubble.meta.js +158 -0
  132. package/dist/components/bubble/bubble.variants.js +90 -0
  133. package/dist/components/bubble/index.js +13 -0
  134. package/dist/components/button/button.js +87 -0
  135. package/dist/components/button/button.meta.js +302 -0
  136. package/dist/components/button/button.variants.js +137 -0
  137. package/dist/components/button/index.js +9 -0
  138. package/dist/components/calendar/calendar.js +89 -0
  139. package/dist/components/calendar/calendar.meta.js +136 -0
  140. package/dist/components/calendar/index.js +7 -0
  141. package/dist/components/card/card.js +203 -0
  142. package/dist/components/card/card.meta.js +212 -0
  143. package/dist/components/card/index.js +23 -0
  144. package/dist/components/carousel/carousel.js +428 -0
  145. package/dist/components/carousel/carousel.meta.js +100 -0
  146. package/dist/components/carousel/index.js +7 -0
  147. package/dist/components/chat-composer/chat-composer.js +174 -0
  148. package/dist/components/chat-composer/chat-composer.meta.js +241 -0
  149. package/dist/components/chat-composer/chat-composer.variants.js +17 -0
  150. package/dist/components/chat-composer/index.js +7 -0
  151. package/dist/components/checkbox/checkbox-mark.js +28 -0
  152. package/dist/components/checkbox/checkbox.js +30 -0
  153. package/dist/components/checkbox/checkbox.meta.js +127 -0
  154. package/dist/components/checkbox/index.js +7 -0
  155. package/dist/components/checkbox-card/checkbox-card.js +94 -0
  156. package/dist/components/checkbox-card/checkbox-card.meta.js +154 -0
  157. package/dist/components/checkbox-card/checkbox-card.variants.js +43 -0
  158. package/dist/components/checkbox-card/index.js +11 -0
  159. package/dist/components/code/code.js +19 -0
  160. package/dist/components/code/code.meta.js +71 -0
  161. package/dist/components/code/index.js +7 -0
  162. package/dist/components/code-block/code-block.js +84 -0
  163. package/dist/components/code-block/code-block.meta.js +103 -0
  164. package/dist/components/code-block/index.js +8 -0
  165. package/dist/components/collapsible/collapsible.js +38 -0
  166. package/dist/components/collapsible/collapsible.meta.js +117 -0
  167. package/dist/components/collapsible/index.js +15 -0
  168. package/dist/components/color-field/color-field.js +293 -0
  169. package/dist/components/color-field/color-field.meta.js +142 -0
  170. package/dist/components/color-field/color.js +69 -0
  171. package/dist/components/color-field/index.js +15 -0
  172. package/dist/components/combobox/combobox.js +486 -0
  173. package/dist/components/combobox/combobox.meta.js +217 -0
  174. package/dist/components/combobox/index.js +37 -0
  175. package/dist/components/command-menu/command-menu-trigger.js +122 -0
  176. package/dist/components/command-menu/command-menu.js +433 -0
  177. package/dist/components/command-menu/command-menu.meta.js +279 -0
  178. package/dist/components/command-menu/index.js +21 -0
  179. package/dist/components/context-card/context-card.js +62 -0
  180. package/dist/components/context-card/context-card.meta.js +106 -0
  181. package/dist/components/context-card/index.js +17 -0
  182. package/dist/components/context-menu/context-menu.js +92 -0
  183. package/dist/components/context-menu/context-menu.meta.js +138 -0
  184. package/dist/components/context-menu/index.js +23 -0
  185. package/dist/components/data-table/data-table.js +142 -0
  186. package/dist/components/data-table/data-table.meta.js +146 -0
  187. package/dist/components/data-table/index.js +9 -0
  188. package/dist/components/date-picker/date-picker.js +196 -0
  189. package/dist/components/date-picker/date-picker.meta.js +164 -0
  190. package/dist/components/date-picker/index.js +7 -0
  191. package/dist/components/date-tile/date-tile.js +29 -0
  192. package/dist/components/date-tile/date-tile.meta.js +72 -0
  193. package/dist/components/date-tile/index.js +7 -0
  194. package/dist/components/destructive-action-modal/destructive-action-modal.js +130 -0
  195. package/dist/components/destructive-action-modal/destructive-action-modal.meta.js +136 -0
  196. package/dist/components/destructive-action-modal/index.js +9 -0
  197. package/dist/components/dialog/dialog.js +207 -0
  198. package/dist/components/dialog/dialog.meta.js +239 -0
  199. package/dist/components/dialog/index.js +45 -0
  200. package/dist/components/disclosure-picker/disclosure-picker.js +417 -0
  201. package/dist/components/disclosure-picker/disclosure-picker.meta.js +213 -0
  202. package/dist/components/disclosure-picker/index.js +21 -0
  203. package/dist/components/empty-state/empty-state.js +52 -0
  204. package/dist/components/empty-state/empty-state.meta.js +158 -0
  205. package/dist/components/empty-state/index.js +7 -0
  206. package/dist/components/entity/entity.js +70 -0
  207. package/dist/components/entity/entity.meta.js +108 -0
  208. package/dist/components/entity/entity.variants.js +16 -0
  209. package/dist/components/entity/index.js +7 -0
  210. package/dist/components/feedback/feedback.js +93 -0
  211. package/dist/components/feedback/feedback.meta.js +109 -0
  212. package/dist/components/feedback/index.js +7 -0
  213. package/dist/components/fieldset/fieldset.js +80 -0
  214. package/dist/components/fieldset/fieldset.meta.js +160 -0
  215. package/dist/components/fieldset/index.js +17 -0
  216. package/dist/components/file-dropzone/file-dropzone.js +193 -0
  217. package/dist/components/file-dropzone/file-dropzone.meta.js +210 -0
  218. package/dist/components/file-dropzone/file-dropzone.variants.js +26 -0
  219. package/dist/components/file-dropzone/index.js +13 -0
  220. package/dist/components/font-picker/font-picker.js +77 -0
  221. package/dist/components/font-picker/font-picker.meta.js +119 -0
  222. package/dist/components/font-picker/index.js +7 -0
  223. package/dist/components/form/form.js +145 -0
  224. package/dist/components/form/form.meta.js +142 -0
  225. package/dist/components/form/index.js +11 -0
  226. package/dist/components/gliding-highlight/gliding-highlight.js +50 -0
  227. package/dist/components/gliding-highlight/gliding-highlight.meta.js +135 -0
  228. package/dist/components/gliding-highlight/index.js +9 -0
  229. package/dist/components/gradient-art/gradient-art.js +168 -0
  230. package/dist/components/gradient-art/gradient-art.meta.js +81 -0
  231. package/dist/components/gradient-art/index.js +7 -0
  232. package/dist/components/gradient-art/renderer.js +336 -0
  233. package/dist/components/inline-status/index.js +7 -0
  234. package/dist/components/inline-status/inline-status.js +33 -0
  235. package/dist/components/inline-status/inline-status.meta.js +83 -0
  236. package/dist/components/inline-status/inline-status.variants.js +18 -0
  237. package/dist/components/input/index.js +7 -0
  238. package/dist/components/input/input.js +33 -0
  239. package/dist/components/input/input.meta.js +155 -0
  240. package/dist/components/kbd/index.js +8 -0
  241. package/dist/components/kbd/kbd.js +54 -0
  242. package/dist/components/kbd/kbd.meta.js +95 -0
  243. package/dist/components/label/index.js +7 -0
  244. package/dist/components/label/label.js +51 -0
  245. package/dist/components/label/label.meta.js +98 -0
  246. package/dist/components/line-chart/index.js +7 -0
  247. package/dist/components/line-chart/line-chart.js +10 -0
  248. package/dist/components/line-chart/line-chart.meta.js +222 -0
  249. package/dist/components/marker/index.js +7 -0
  250. package/dist/components/marker/marker.js +39 -0
  251. package/dist/components/marker/marker.meta.js +132 -0
  252. package/dist/components/marker/marker.variants.js +25 -0
  253. package/dist/components/marquee/index.js +7 -0
  254. package/dist/components/marquee/marquee.js +146 -0
  255. package/dist/components/marquee/marquee.meta.js +106 -0
  256. package/dist/components/media-image/index.js +7 -0
  257. package/dist/components/media-image/media-image.js +56 -0
  258. package/dist/components/media-image/media-image.meta.js +92 -0
  259. package/dist/components/media-tile/index.js +7 -0
  260. package/dist/components/media-tile/media-tile.js +135 -0
  261. package/dist/components/media-tile/media-tile.meta.js +133 -0
  262. package/dist/components/media-tile/media-tile.variants.js +48 -0
  263. package/dist/components/menu/index.js +33 -0
  264. package/dist/components/menu/menu.js +168 -0
  265. package/dist/components/menu/menu.meta.js +176 -0
  266. package/dist/components/message/index.js +7 -0
  267. package/dist/components/message/message.js +83 -0
  268. package/dist/components/message/message.meta.js +150 -0
  269. package/dist/components/message/message.variants.js +41 -0
  270. package/dist/components/message-scroller/index.js +9 -0
  271. package/dist/components/message-scroller/message-scroller.js +300 -0
  272. package/dist/components/message-scroller/message-scroller.meta.js +172 -0
  273. package/dist/components/meter/index.js +20 -0
  274. package/dist/components/meter/meter.js +112 -0
  275. package/dist/components/meter/meter.meta.js +227 -0
  276. package/dist/components/meter/meter.variants.js +61 -0
  277. package/dist/components/middle-truncate/index.js +7 -0
  278. package/dist/components/middle-truncate/middle-truncate.js +28 -0
  279. package/dist/components/middle-truncate/middle-truncate.meta.js +77 -0
  280. package/dist/components/note/index.js +9 -0
  281. package/dist/components/note/note.js +73 -0
  282. package/dist/components/note/note.meta.js +112 -0
  283. package/dist/components/note/note.variants.js +22 -0
  284. package/dist/components/notification-badge/index.js +15 -0
  285. package/dist/components/notification-badge/notification-badge.js +48 -0
  286. package/dist/components/notification-badge/notification-badge.meta.js +107 -0
  287. package/dist/components/notification-badge/notification-badge.variants.js +57 -0
  288. package/dist/components/number-field/index.js +7 -0
  289. package/dist/components/number-field/number-field.js +70 -0
  290. package/dist/components/number-field/number-field.meta.js +109 -0
  291. package/dist/components/otp-field/index.js +7 -0
  292. package/dist/components/otp-field/otp-field.js +70 -0
  293. package/dist/components/otp-field/otp-field.meta.js +197 -0
  294. package/dist/components/overflow-menu/index.js +7 -0
  295. package/dist/components/overflow-menu/overflow-menu.js +26 -0
  296. package/dist/components/overflow-menu/overflow-menu.meta.js +100 -0
  297. package/dist/components/page-header/index.js +7 -0
  298. package/dist/components/page-header/page-header.js +34 -0
  299. package/dist/components/page-header/page-header.meta.js +104 -0
  300. package/dist/components/page-loader/index.js +7 -0
  301. package/dist/components/page-loader/page-loader.js +37 -0
  302. package/dist/components/page-loader/page-loader.meta.js +85 -0
  303. package/dist/components/pagination/index.js +7 -0
  304. package/dist/components/pagination/pagination.js +105 -0
  305. package/dist/components/pagination/pagination.meta.js +141 -0
  306. package/dist/components/panel-group/index.js +10 -0
  307. package/dist/components/panel-group/panel-group.js +168 -0
  308. package/dist/components/panel-group/panel-group.meta.js +153 -0
  309. package/dist/components/panel-section/index.js +9 -0
  310. package/dist/components/panel-section/panel-section.js +115 -0
  311. package/dist/components/panel-section/panel-section.meta.js +156 -0
  312. package/dist/components/phone/index.js +7 -0
  313. package/dist/components/phone/phone.js +50 -0
  314. package/dist/{chunk-KFZ43RKU.js → components/phone/phone.meta.js} +3 -50
  315. package/dist/{chunk-NEQNXI2G.js → components/phone/status-icons.js} +4 -4
  316. package/dist/components/pie-chart/index.js +7 -0
  317. package/dist/components/pie-chart/pie-chart.js +191 -0
  318. package/dist/components/pie-chart/pie-chart.meta.js +210 -0
  319. package/dist/components/pill/index.js +9 -0
  320. package/dist/components/pill/pill.js +24 -0
  321. package/dist/components/pill/pill.meta.js +108 -0
  322. package/dist/components/pill/pill.variants.js +65 -0
  323. package/dist/components/popover/index.js +23 -0
  324. package/dist/components/popover/popover.js +100 -0
  325. package/dist/components/popover/popover.meta.js +136 -0
  326. package/dist/components/progress/index.js +19 -0
  327. package/dist/components/progress/progress.js +70 -0
  328. package/dist/components/progress/progress.meta.js +171 -0
  329. package/dist/components/progress/progress.variants.js +17 -0
  330. package/dist/components/radar-chart/index.js +7 -0
  331. package/dist/components/radar-chart/radar-chart.js +161 -0
  332. package/dist/components/radar-chart/radar-chart.meta.js +204 -0
  333. package/dist/components/radial-chart/index.js +7 -0
  334. package/dist/components/radial-chart/radial-chart.js +175 -0
  335. package/dist/components/radial-chart/radial-chart.meta.js +216 -0
  336. package/dist/components/radio/index.js +8 -0
  337. package/dist/components/radio/radio.js +39 -0
  338. package/dist/components/radio/radio.meta.js +104 -0
  339. package/dist/components/radio-card/index.js +7 -0
  340. package/dist/components/radio-card/radio-card.js +81 -0
  341. package/dist/components/radio-card/radio-card.meta.js +144 -0
  342. package/dist/components/radio-card/radio-card.variants.js +43 -0
  343. package/dist/components/relative-time/index.js +7 -0
  344. package/dist/components/relative-time/relative-time.js +47 -0
  345. package/dist/components/relative-time/relative-time.meta.js +77 -0
  346. package/dist/components/scroller/index.js +7 -0
  347. package/dist/components/scroller/scroller.js +111 -0
  348. package/dist/components/scroller/scroller.meta.js +118 -0
  349. package/dist/components/scrub-field/index.js +7 -0
  350. package/dist/components/scrub-field/scrub-field.js +124 -0
  351. package/dist/components/scrub-field/scrub-field.meta.js +139 -0
  352. package/dist/components/search-input/index.js +7 -0
  353. package/dist/components/search-input/search-input.js +91 -0
  354. package/dist/components/search-input/search-input.meta.js +129 -0
  355. package/dist/components/section-header/index.js +7 -0
  356. package/dist/components/section-header/section-header.js +39 -0
  357. package/dist/components/section-header/section-header.meta.js +174 -0
  358. package/dist/components/segmented-bar/index.js +14 -0
  359. package/dist/components/segmented-bar/segmented-bar.js +26 -0
  360. package/dist/components/segmented-bar/segmented-bar.meta.js +108 -0
  361. package/dist/components/segmented-bar/segmented-bar.variants.js +39 -0
  362. package/dist/components/select/index.js +25 -0
  363. package/dist/components/select/select.js +191 -0
  364. package/dist/components/select/select.meta.js +189 -0
  365. package/dist/components/separator/index.js +7 -0
  366. package/dist/components/separator/separator.js +42 -0
  367. package/dist/components/separator/separator.meta.js +84 -0
  368. package/dist/components/settings-menu/index.js +11 -0
  369. package/dist/components/settings-menu/settings-menu.js +101 -0
  370. package/dist/components/settings-menu/settings-menu.meta.js +177 -0
  371. package/dist/components/settings-menu/settings-menu.variants.js +15 -0
  372. package/dist/components/sheet/index.js +29 -0
  373. package/dist/components/sheet/sheet.js +121 -0
  374. package/dist/components/sheet/sheet.meta.js +167 -0
  375. package/dist/components/sheet/sheet.variants.js +21 -0
  376. package/dist/components/show-more/index.js +7 -0
  377. package/dist/components/show-more/show-more.js +87 -0
  378. package/dist/components/show-more/show-more.meta.js +79 -0
  379. package/dist/components/side-nav/index.js +13 -0
  380. package/dist/components/side-nav/side-nav.js +75 -0
  381. package/dist/components/side-nav/side-nav.meta.js +139 -0
  382. package/dist/components/side-nav/side-nav.variants.js +34 -0
  383. package/dist/components/sidebar/index.js +21 -0
  384. package/dist/components/sidebar/sidebar.js +196 -0
  385. package/dist/components/sidebar/sidebar.meta.js +118 -0
  386. package/dist/components/skeleton/index.js +7 -0
  387. package/dist/components/skeleton/skeleton.js +16 -0
  388. package/dist/components/skeleton/skeleton.meta.js +90 -0
  389. package/dist/components/slider/index.js +21 -0
  390. package/dist/components/slider/slider.js +115 -0
  391. package/dist/components/slider/slider.meta.js +214 -0
  392. package/dist/components/sparkline/index.js +7 -0
  393. package/dist/components/sparkline/sparkline.js +84 -0
  394. package/dist/components/sparkline/sparkline.meta.js +147 -0
  395. package/dist/components/spinner/index.js +7 -0
  396. package/dist/components/spinner/spinner.js +59 -0
  397. package/dist/components/spinner/spinner.meta.js +118 -0
  398. package/dist/components/status-bar/index.js +9 -0
  399. package/dist/components/status-bar/status-bar.js +40 -0
  400. package/dist/components/status-bar/status-bar.meta.js +117 -0
  401. package/dist/components/status-dot/index.js +9 -0
  402. package/dist/components/status-dot/status-dot.js +40 -0
  403. package/dist/components/status-dot/status-dot.meta.js +113 -0
  404. package/dist/components/status-dot/status-dot.variants.js +28 -0
  405. package/dist/components/stepper/index.js +17 -0
  406. package/dist/components/stepper/stepper.js +161 -0
  407. package/dist/components/stepper/stepper.meta.js +190 -0
  408. package/dist/components/stepper/stepper.variants.js +72 -0
  409. package/dist/components/streaming-text/index.js +7 -0
  410. package/dist/components/streaming-text/streaming-text.js +55 -0
  411. package/dist/components/streaming-text/streaming-text.meta.js +147 -0
  412. package/dist/components/switch/index.js +7 -0
  413. package/dist/components/switch/switch.js +26 -0
  414. package/dist/components/switch/switch.meta.js +133 -0
  415. package/dist/components/table/index.js +23 -0
  416. package/dist/components/table/table.js +117 -0
  417. package/dist/components/table/table.meta.js +143 -0
  418. package/dist/components/tabs/index.js +17 -0
  419. package/dist/components/tabs/tabs.js +58 -0
  420. package/dist/components/tabs/tabs.meta.js +120 -0
  421. package/dist/components/textarea/index.js +7 -0
  422. package/dist/components/textarea/textarea.js +38 -0
  423. package/dist/components/textarea/textarea.meta.js +117 -0
  424. package/dist/components/theme-selector/index.js +11 -0
  425. package/dist/components/theme-selector/theme-selector.js +145 -0
  426. package/dist/components/theme-selector/theme-selector.meta.js +181 -0
  427. package/dist/components/theme-switcher/index.js +7 -0
  428. package/dist/components/theme-switcher/theme-switcher.js +106 -0
  429. package/dist/components/theme-switcher/theme-switcher.meta.js +99 -0
  430. package/dist/components/thinking-orb/index.js +9 -0
  431. package/dist/components/thinking-orb/thinking-orb.js +126 -0
  432. package/dist/components/thinking-orb/thinking-orb.meta.js +142 -0
  433. package/dist/components/thinking-orb/thinking-orb.render.js +252 -0
  434. package/dist/components/toast/index.js +29 -0
  435. package/dist/components/toast/toast.js +254 -0
  436. package/dist/components/toast/toast.meta.js +261 -0
  437. package/dist/components/toggle/index.js +13 -0
  438. package/dist/components/toggle/toggle.js +51 -0
  439. package/dist/components/toggle/toggle.meta.js +129 -0
  440. package/dist/components/toolbar/index.js +27 -0
  441. package/dist/components/toolbar/toolbar.js +80 -0
  442. package/dist/components/toolbar/toolbar.meta.js +272 -0
  443. package/dist/components/toolbar/toolbar.variants.js +16 -0
  444. package/dist/components/tooltip/index.js +21 -0
  445. package/dist/components/tooltip/tooltip.js +74 -0
  446. package/dist/components/tooltip/tooltip.meta.js +156 -0
  447. package/dist/components/tree/index.js +12 -0
  448. package/dist/components/tree/tree.js +364 -0
  449. package/dist/components/tree/tree.meta.js +334 -0
  450. package/dist/components/video/index.js +7 -0
  451. package/dist/components/video/video.js +59 -0
  452. package/dist/components/video/video.meta.js +81 -0
  453. package/dist/components/workspace/index.js +9 -0
  454. package/dist/components/workspace/workspace.js +79 -0
  455. package/dist/components/workspace/workspace.meta.js +183 -0
  456. package/dist/foundations/accessibility.js +107 -0
  457. package/dist/foundations/index.js +25 -792
  458. package/dist/foundations/motion.js +135 -0
  459. package/dist/foundations/scrolling.js +62 -0
  460. package/dist/foundations/shape.js +113 -0
  461. package/dist/foundations/spacing.js +121 -0
  462. package/dist/foundations/surfaces.js +107 -0
  463. package/dist/foundations/types.js +0 -0
  464. package/dist/foundations/typography.js +152 -0
  465. package/dist/icon/Icon.js +30 -0
  466. package/dist/icon/IconProvider.js +27 -0
  467. package/dist/icon/compose.js +13 -0
  468. package/dist/icon/index.d.ts +13 -5
  469. package/dist/icon/index.js +13 -2
  470. package/dist/icon/registry.js +0 -0
  471. package/dist/icon-sets/brands.generated.js +123 -0
  472. package/dist/icon-sets/brands.js +10 -123
  473. package/dist/icon-sets/reicon.d.ts +3 -1
  474. package/dist/icon-sets/reicon.js +209 -1
  475. package/dist/index.d.ts +618 -34
  476. package/dist/index.js +211 -11351
  477. package/dist/ios/chrome.js +36 -0
  478. package/dist/ios/index.js +19 -315
  479. package/dist/ios/liquid-glass.js +103 -0
  480. package/dist/ios/native-app.js +46 -0
  481. package/dist/ios/navigation.js +63 -0
  482. package/dist/ios/phone-frame.js +64 -0
  483. package/dist/ios/preview-theme.js +83 -0
  484. package/dist/ios/preview.js +10 -137
  485. package/dist/ios/sheet.js +85 -0
  486. package/dist/ios/theme.js +19 -0
  487. package/dist/layout/AspectBox.js +27 -0
  488. package/dist/layout/Bleed.js +15 -0
  489. package/dist/layout/Center.js +21 -0
  490. package/dist/layout/Columns.js +70 -0
  491. package/dist/layout/Grid.js +50 -0
  492. package/dist/layout/GridOverlay.js +41 -0
  493. package/dist/layout/Page.js +11 -0
  494. package/dist/layout/Region.js +39 -0
  495. package/dist/layout/Row.js +45 -0
  496. package/dist/layout/SafeArea.js +26 -0
  497. package/dist/layout/Spacer.js +9 -0
  498. package/dist/layout/SplitPane.js +55 -0
  499. package/dist/layout/Stack.js +42 -0
  500. package/dist/layout/_responsive.js +16 -0
  501. package/dist/layout/_safeArea.js +18 -0
  502. package/dist/layout/index.d.ts +30 -8
  503. package/dist/layout/index.js +14 -2
  504. package/dist/lib/anchored-scroll.js +7 -0
  505. package/dist/lib/bleed.js +7 -0
  506. package/dist/lib/chart/cartesian.js +365 -0
  507. package/dist/lib/chart/chart-frame.js +163 -0
  508. package/dist/lib/chart/chart-legend.js +13 -0
  509. package/dist/lib/chart/chart-tooltip.js +38 -0
  510. package/dist/lib/chart/format.js +24 -0
  511. package/dist/lib/chart/geometry.js +114 -0
  512. package/dist/lib/chart/motion.js +34 -0
  513. package/dist/lib/chart/scale.js +72 -0
  514. package/dist/lib/chart/types.js +45 -0
  515. package/dist/lib/chart/use-arrival.js +41 -0
  516. package/dist/{chunk-SRQWQUTW.js → lib/cn.js} +5 -5
  517. package/dist/lib/component-guidance.js +215 -0
  518. package/dist/lib/density.js +54 -0
  519. package/dist/lib/field-shell.js +11 -0
  520. package/dist/lib/glyphs.js +9 -0
  521. package/dist/lib/heading-row.js +36 -0
  522. package/dist/lib/highlight-glide.js +84 -0
  523. package/dist/lib/hit-target.js +5 -0
  524. package/dist/lib/meta.js +0 -0
  525. package/dist/lib/portal-theme-scope.js +74 -0
  526. package/dist/{chunk-FKZXAFIY.js → lib/presentation.js} +8 -3
  527. package/dist/lib/refs.js +14 -0
  528. package/dist/lib/scrub-gesture.js +50 -0
  529. package/dist/lib/segmented.js +13 -0
  530. package/dist/lib/selection-card.js +41 -0
  531. package/dist/lib/status-tone.js +13 -0
  532. package/dist/lib/taxonomy.js +214 -0
  533. package/dist/lib/trim-label.js +20 -0
  534. package/dist/lib/use-copy.js +27 -0
  535. package/dist/lib/use-edge-fade.js +32 -0
  536. package/dist/lib/use-isomorphic-layout-effect.js +8 -0
  537. package/dist/lib/use-resize-observer.js +18 -0
  538. package/dist/lib/use-scroll-activity.js +52 -0
  539. package/dist/lib/use-wheel-to-horizontal.js +65 -0
  540. package/dist/motion/MotionProvider.js +75 -0
  541. package/dist/motion/Presence.js +112 -0
  542. package/dist/motion/Reveal.js +42 -0
  543. package/dist/motion/TextReveal.js +61 -0
  544. package/dist/motion/index.js +24 -0
  545. package/dist/motion/useInView.js +36 -0
  546. package/dist/motion/useReducedMotion.js +25 -0
  547. package/dist/motion/viewTransition.js +47 -0
  548. package/dist/patterns/auth-page.js +90 -0
  549. package/dist/patterns/auth-page.meta.js +27 -0
  550. package/dist/patterns/brand-landing-page.js +341 -0
  551. package/dist/patterns/brand-landing-page.meta.js +28 -0
  552. package/dist/patterns/chat-panel.js +185 -0
  553. package/dist/patterns/chat-panel.meta.js +31 -0
  554. package/dist/patterns/dashboard-page.js +332 -0
  555. package/dist/patterns/dashboard-page.meta.js +29 -0
  556. package/dist/patterns/feed-page.js +205 -0
  557. package/dist/patterns/feed-page.meta.js +25 -0
  558. package/dist/patterns/index.js +34 -2140
  559. package/dist/patterns/landing-page.js +356 -0
  560. package/dist/patterns/landing-page.meta.js +26 -0
  561. package/dist/patterns/list-detail-page.js +373 -0
  562. package/dist/patterns/list-detail-page.meta.js +24 -0
  563. package/dist/patterns/settings-page.js +268 -0
  564. package/dist/patterns/settings-page.meta.js +27 -0
  565. package/dist/presets/index.d.ts +7 -1
  566. package/dist/presets/index.js +53 -185
  567. package/dist/styles.css +118 -3
  568. package/dist/{theme-provider-kQszHYji.d.ts → theme-provider-S0NVJs2Y.d.ts} +6 -1
  569. package/dist/{chunk-NETUFI34.js → themes/_contract.js} +14 -2
  570. package/dist/themes/atelier/index.js +17 -1
  571. package/dist/themes/contrast.js +288 -0
  572. package/dist/themes/ember/ember.css +6 -6
  573. package/dist/themes/ember/index.js +16 -1
  574. package/dist/themes/index.d.ts +1 -1
  575. package/dist/themes/index.js +3 -8
  576. package/dist/themes/meridian/index.js +16 -1
  577. package/dist/themes/poppy/index.js +17 -1
  578. package/dist/themes/prism/index.js +17 -1
  579. package/dist/themes/prism/prism.css +2 -2
  580. package/dist/themes/registry.js +59 -0
  581. package/dist/themes/sable/index.js +16 -1
  582. package/dist/{chunk-EFLC4NA3.js → themes/theme-provider.js} +38 -54
  583. package/dist/themes/verdant/index.js +17 -1
  584. package/llms/blocks/media-player-block.md +20 -7
  585. package/llms/blocks/product-card-block.md +6 -4
  586. package/llms/blocks/task-list-block.md +1 -1
  587. package/llms/blocks/traffic-sources-block.md +1 -1
  588. package/llms/blocks/traffic-trend-block.md +5 -3
  589. package/llms/blocks/verify-code-block.md +1 -1
  590. package/llms/components/app-shell.md +5 -2
  591. package/llms/components/avatar.md +25 -3
  592. package/llms/components/button.md +20 -1
  593. package/llms/components/card.md +1 -1
  594. package/llms/components/disclosure-picker.md +168 -0
  595. package/llms/components/font-picker.md +104 -0
  596. package/llms/components/media-image.md +98 -0
  597. package/llms/components/media-tile.md +143 -0
  598. package/llms/components/page-header.md +6 -5
  599. package/llms/components/pie-chart.md +2 -2
  600. package/llms/components/radial-chart.md +1 -1
  601. package/llms/components/section-header.md +144 -0
  602. package/llms/components/sheet.md +10 -0
  603. package/llms/components/sidebar.md +4 -3
  604. package/llms/components/slider.md +14 -2
  605. package/llms/components/theme-selector.md +44 -4
  606. package/llms/components/tree.md +3 -3
  607. package/llms/layout/grid.md +24 -3
  608. package/llms/layout/region.md +16 -3
  609. package/llms/patterns/dashboard-page.md +3 -6
  610. package/llms/patterns/feed-page.md +4 -4
  611. package/llms/patterns/list-detail-page.md +1 -0
  612. package/llms/patterns/settings-page.md +2 -5
  613. package/llms/reference/utilities.md +84 -4
  614. package/llms/tasks/building-a-page.md +34 -3
  615. package/llms/tasks/converting-an-app.md +14 -4
  616. package/llms/tasks/creating-a-preset.md +4 -1
  617. package/llms/tasks/reviewing-a-design.md +19 -0
  618. package/llms/tasks/using-presets.md +19 -0
  619. package/llms-full.txt +1066 -78
  620. package/llms.txt +26 -9
  621. package/package.json +1 -1
  622. package/registry.json +2342 -164
  623. package/scripts/agent-setup.mjs +90 -0
  624. package/scripts/design-presets.mjs +59 -6
  625. package/scripts/grade.mjs +195 -29
  626. package/scripts/lint-rules.mjs +486 -10
  627. package/scripts/review-evidence.mjs +28 -1
  628. package/skills/sable/SKILL.md +67 -24
  629. package/targets/ios.json +45 -0
  630. package/dist/chunk-3IWZYBGH.js +0 -398
  631. package/dist/chunk-7MG4SHYX.js +0 -15
  632. package/dist/chunk-7QSDYBXO.js +0 -7421
  633. package/dist/chunk-ADLLDWLW.js +0 -15
  634. package/dist/chunk-C4Z3CF54.js +0 -15
  635. package/dist/chunk-EDXLM7YL.js +0 -5078
  636. package/dist/chunk-EHH4MHO6.js +0 -14
  637. package/dist/chunk-K7RUHUGC.js +0 -14
  638. package/dist/chunk-KO3NAEBF.js +0 -15
  639. package/dist/chunk-L4IEIQQA.js +0 -14
  640. package/dist/chunk-QJ6FWTUG.js +0 -40
  641. package/dist/chunk-VVW4UCDI.js +0 -4908
  642. package/dist/chunk-XAZGMY73.js +0 -187
@@ -0,0 +1,168 @@
1
+ # DisclosurePicker
2
+
3
+ **Status:** stable
4
+
5
+ A property row that names the current value and opens its list in place, beneath the row, instead of in a popup — for a tool panel or settings column where the choice is made while looking at what it changes. Built on Collapsible; the list is a listbox — arrow keys move, Enter or a click picks, applies and closes — and an optional search field filters long lists.
6
+
7
+ **Keywords:** disclosure picker, inline picker, in-place select, expanding list, property row, font picker, preset picker, inspector, picker row
8
+
9
+ ## Usage
10
+
11
+ Use DisclosurePicker for one-of-many choices in a docked panel or inspector where a popup would cover the preview the choice is about: the row shows the current value (in its own face, with a tile, as the content warrants), and the list opens beneath it, pushing the panel down. Use Select for a form field or anywhere a popup is fine; use Combobox when people type to find an option among hundreds; use RadioGroup or RadioCard when every option should stay visible. Nothing applies while arrowing through the list — only Enter, Space or a click picks — so it suits choices with a cost (a confirm, a font load, a reset); where each option should apply as focus lands on it, that is a radio group (ThemeSelector, RadioGroup).
12
+
13
+ ### Best practices
14
+
15
+ - Do: Enable search (`search` on DisclosurePickerPanel) only for long lists — roughly a dozen options or more, i.e. once the list scrolls. A short list (a handful of presets) reads faster with no field to skip past; the panel then has no search row and no gap.
16
+ - Do: Show the value as the thing it is: a font row renders the family in its own face, a theme row leads with a ThemePreview tile in `media`. The row is a preview of the choice, not a label for it.
17
+ - Do: Use `size="xs"` in a property grid (a compact Density / PanelSection), where it sits on the inset panel-field fill beside ScrubField and an xs SelectTrigger. Keep `md` in a settings column or a simple panel.
18
+ - Do: When several rows share a panel, control `open` from the parent so only one list is open at a time — two open lists push each other and the preview out of view.
19
+ - Do: Use it for a choice with a cost — one that opens a confirm, loads something, or resets other settings. Arrow keys only move focus; onValueChange fires once, on Enter, Space or a click, and only when the pick differs from `value`.
20
+ - Do: Give options a leading visual with `media` (a ThemePreview tile, a swatch) instead of nesting another component's list: ThemeSelector's radio list applies each theme as the arrow keys land on it, which defeats commit-on-Enter. A list the picker cannot express (a Tree) can still close the row with `useDisclosurePicker().close()`.
21
+ - Do: Render DisclosurePickerEmpty in place of the list when a search matches nothing, not inside it — a listbox holds only options.
22
+ - Don't: Use it in a page-register form. A form field that pushes the form down on open reads as a layout jump; that is Select's job.
23
+ - Don't: Rebuild the row from a Collapsible with hand-copied classes. The disclosure, the well, the hover step, the xs field shell, focus return and scroll-into-view all live here; two hand copies of this row in one panel is how it came to exist.
24
+ - Don't: Turn search on for a short list just because the other rows have it. A search field over six options is a control with nothing to do.
25
+
26
+ ### Anatomy
27
+
28
+ | Part | Required | Description |
29
+ | --- | --- | --- |
30
+ | DisclosurePicker | yes | Root. Owns open state, size, focus return and scroll-into-view. |
31
+ | DisclosurePickerTrigger | yes | The row: optional `label` caption, optional `media`, the value, and a chevron that flips when open. |
32
+ | DisclosurePickerPanel | yes | What opens beneath the row: the opt-in search field, then the list. |
33
+ | DisclosurePickerList | no | A listbox of options that scrolls past 16rem. Arrow keys move; Enter, Space or a click picks. |
34
+ | DisclosurePickerOption | no | One option row, with optional leading media and trailing note. Chosen, it lifts out of the well; focused, it takes the ring. |
35
+ | DisclosurePickerEmpty | no | The no-match line, rendered in place of the list when a search filters everything out. |
36
+
37
+ ## Subcomponents
38
+
39
+ | Name | Type | Description |
40
+ | --- | --- | --- |
41
+ | DisclosurePickerTrigger | { label?: ReactNode; media?: ReactNode; children?: ReactNode } + Base UI Collapsible.Trigger props | The row. `label` is a muted leading caption on a fixed column so stacked rows align; `media` a leading visual (a ThemePreview tile); children the value, truncated on one line. ArrowDown opens it, and steps into the list once open. |
42
+ | DisclosurePickerPanel | { search?: { value: string; onValueChange(value: string): void; placeholder?: string; label?: string } } + Collapsible.Panel props | Opens beneath the row. `search` opts in to a SearchInput above the list (off by default); it is controlled because the consumer filters, and it is cleared when the row closes. Escape clears a query first, then closes; ArrowDown in the field steps into the list. |
43
+ | DisclosurePickerList | { value: string; onValueChange(value: string): void; aria-label?: string } + div props | A listbox. ArrowUp/ArrowDown, Home/End and type-ahead move focus without applying anything (ArrowUp from the first option returns to the search field); Enter, Space or a click picks the focused option — onValueChange fires once, only when it differs from `value` — and closes the row. Escape closes without a pick. |
44
+ | DisclosurePickerOption | { value: string; children?: ReactNode; media?: ReactNode; note?: ReactNode; disabled?: boolean; textValue?: string } | One option row. `media` is a leading visual (a ThemePreview tile); `note` a muted trailing annotation — options without one show a check when chosen. `disabled` skips it for the keyboard and refuses the pick; `textValue` is what type-ahead matches when the name is not plain text. |
45
+ | DisclosurePickerEmpty | { children?: ReactNode } | The no-match line, rendered in place of the list. |
46
+ | useDisclosurePicker | () => { open: boolean; close(): void; size: "md" \| "xs" } | For a list the picker does not own (a Tree) that should close the row when something is picked. |
47
+
48
+ ## Props
49
+
50
+ | Name | Type | Default | Description |
51
+ | --- | --- | --- | --- |
52
+ | open | boolean | — | Controlled open state. Pair with onOpenChange. |
53
+ | defaultOpen | boolean | false | Uncontrolled initial open state. |
54
+ | onOpenChange | (open: boolean) => void | — | Called with the next open state — from the trigger, a pick, or Escape. |
55
+ | size | "md" \| "xs" | "md" | md is a --control-md row (40px comfortable) in a settings column, its own well when closed. xs is the property-grid rung: a --control-xs row on the inset panel-field fill with ui-tier type. Height, padding, radius, type and surface move together. |
56
+ | className | string | — | Merged onto the root (layout only — the look comes from size). |
57
+
58
+ ## Tokens used
59
+
60
+ `foreground`, `muted-foreground`, `popover`, `hairline`, `panel-field`, `panel-field-hover`, `ring`, `accent`
61
+
62
+ ## Motion
63
+
64
+ - **arrival** · root · _no motion_ — A property row is present from first paint; animating it in would stage chrome the user is already scanning.
65
+ - **response** · DisclosurePickerTrigger · `motion-interactive` — The row reads as a value, not a button; the hover step is what says it opens before it is clicked.
66
+ - **transition** · DisclosurePickerPanel · `motion-disclosure` — The list pushes the panel down; growing its height ties the movement below to this row rather than to a layout jump.
67
+ - **activity** · root · _no motion_ — Nothing is indeterminate: the options are resident. A list that loads should show its own loader inside the panel.
68
+
69
+ ## Accessibility
70
+
71
+ - The trigger is a real <button>; Base UI wires aria-expanded and aria-controls to the panel. Its accessible name is its text (caption + value) unless an aria-label is given.
72
+ - The list is role="listbox" with role="option" rows; aria-selected marks the committed value, not the focused row, so a screen reader hears both where focus is and what is chosen. Selection does not follow focus: ArrowUp/ArrowDown, Home/End and type-ahead move focus (roving tabindex — one tab stop, so Tab leaves the list in one step) and apply nothing; Enter, Space or a click picks, applies and closes. Disabled options carry aria-disabled and are skipped.
73
+ - Why a listbox and not a radio group: a radio group's checked state follows focus, so every arrow press would be a pick — wrong for a choice that opens a confirm, loads a font or resets settings. Base UI ships no standalone listbox (Select is popup-only, Combobox's inline list is focus-in-the-input, Composite is internal), so the keyboard model lives here.
74
+ - Opening from the keyboard (Enter, Space, ArrowDown) moves focus into the panel: the search field when there is one, otherwise the chosen option (or the first). A pointer open focuses the search field on a fine pointer only, so touch does not raise the on-screen keyboard.
75
+ - Escape closes the row without changing the value (clearing a query first) and returns focus to the trigger; a pick does the same. Focus is not pulled back when it has already moved on, e.g. to a confirm dialog the pick opened.
76
+ - The panel animates with motion-disclosure, which is static under prefers-reduced-motion or MotionProvider off; the scroll that keeps the row in view is instant, never smooth.
77
+
78
+ ## Related
79
+
80
+ - Components: Collapsible, Select, Combobox, SearchInput, RadioGroup, ThemeSelector, FontPicker, PropertyRow
81
+
82
+ ## Examples
83
+
84
+ ### A short list — no search
85
+
86
+ ```tsx
87
+ import { DisclosurePicker, DisclosurePickerList, DisclosurePickerOption, DisclosurePickerPanel, DisclosurePickerTrigger } from "@sableui/react"
88
+
89
+ const [preset, setPreset] = useState("comfortable")
90
+ const [open, setOpen] = useState(false)
91
+ <DisclosurePicker open={open} onOpenChange={setOpen} className="w-72">
92
+ <DisclosurePickerTrigger label="Density">{preset === "compact" ? "Compact" : "Comfortable"}</DisclosurePickerTrigger>
93
+ <DisclosurePickerPanel>
94
+ <DisclosurePickerList aria-label="Density" value={preset} onValueChange={setPreset}>
95
+ <DisclosurePickerOption value="comfortable">Comfortable</DisclosurePickerOption>
96
+ <DisclosurePickerOption value="compact">Compact</DisclosurePickerOption>
97
+ </DisclosurePickerList>
98
+ </DisclosurePickerPanel>
99
+ </DisclosurePicker>
100
+ ```
101
+
102
+ ### A long list — search opted in
103
+
104
+ ```tsx
105
+ import { DisclosurePicker, DisclosurePickerEmpty, DisclosurePickerList, DisclosurePickerOption, DisclosurePickerPanel, DisclosurePickerTrigger } from "@sableui/react"
106
+
107
+ const COUNTRIES = ["Argentina", "Australia", "Austria", "Belgium", "Brazil", "Canada", "Chile", "Denmark", "Finland", "France", "Germany", "Ireland", "Italy", "Japan", "Mexico", "Norway", "Portugal", "Spain", "Sweden"]
108
+ const [country, setCountry] = useState("Canada")
109
+ const [query, setQuery] = useState("")
110
+ const shown = COUNTRIES.filter((name) => name.toLowerCase().includes(query.trim().toLowerCase()))
111
+ <DisclosurePicker className="w-72">
112
+ <DisclosurePickerTrigger label="Country">{country}</DisclosurePickerTrigger>
113
+ <DisclosurePickerPanel search={{ value: query, onValueChange: setQuery, placeholder: `Search ${COUNTRIES.length} countries` }}>
114
+ {shown.length > 0 ? (
115
+ <DisclosurePickerList aria-label="Country" value={country} onValueChange={setCountry}>
116
+ {shown.map((name) => (
117
+ <DisclosurePickerOption key={name} value={name}>{name}</DisclosurePickerOption>
118
+ ))}
119
+ </DisclosurePickerList>
120
+ ) : (
121
+ <DisclosurePickerEmpty>No countries match “{query}”.</DisclosurePickerEmpty>
122
+ )}
123
+ </DisclosurePickerPanel>
124
+ </DisclosurePicker>
125
+ ```
126
+
127
+ ### A theme picker — media tiles, commit on Enter
128
+
129
+ ```tsx
130
+ import { DisclosurePicker, DisclosurePickerList, DisclosurePickerOption, DisclosurePickerPanel, DisclosurePickerTrigger, ThemePreview, useSableTheme } from "@sableui/react"
131
+
132
+ const { themeId, setThemeId, themes, mode } = useSableTheme()
133
+ const current = themes.find((theme) => theme.id === themeId)
134
+ <DisclosurePicker>
135
+ <DisclosurePickerTrigger aria-label={`Theme: ${current?.label}`} media={current ? <ThemePreview theme={current} mode={mode} size="sm" /> : null}>
136
+ {current?.label}
137
+ </DisclosurePickerTrigger>
138
+ <DisclosurePickerPanel>
139
+ <DisclosurePickerList aria-label="Theme" value={themeId} onValueChange={setThemeId}>
140
+ {themes.map((theme) => (
141
+ <DisclosurePickerOption key={theme.id} value={theme.id} media={<ThemePreview theme={theme} mode={mode} size="sm" />}>
142
+ {theme.label}
143
+ </DisclosurePickerOption>
144
+ ))}
145
+ </DisclosurePickerList>
146
+ </DisclosurePickerPanel>
147
+ </DisclosurePicker>
148
+ ```
149
+
150
+ ### Property-grid rung
151
+
152
+ ```tsx
153
+ import { Density, DisclosurePicker, DisclosurePickerList, DisclosurePickerOption, DisclosurePickerPanel, DisclosurePickerTrigger } from "@sableui/react"
154
+
155
+ const [blend, setBlend] = useState("normal")
156
+ <Density value="compact" className="w-64">
157
+ <DisclosurePicker size="xs">
158
+ <DisclosurePickerTrigger label="Blend">{blend}</DisclosurePickerTrigger>
159
+ <DisclosurePickerPanel>
160
+ <DisclosurePickerList aria-label="Blend mode" value={blend} onValueChange={setBlend}>
161
+ <DisclosurePickerOption value="normal">normal</DisclosurePickerOption>
162
+ <DisclosurePickerOption value="multiply">multiply</DisclosurePickerOption>
163
+ <DisclosurePickerOption value="screen">screen</DisclosurePickerOption>
164
+ </DisclosurePickerList>
165
+ </DisclosurePickerPanel>
166
+ </DisclosurePicker>
167
+ </Density>
168
+ ```
@@ -0,0 +1,104 @@
1
+ # FontPicker
2
+
3
+ **Status:** stable
4
+
5
+ A DisclosurePicker for a typeface: the row names the chosen family in its own face and opens in place into a searchable list of families, each drawn in itself. The app supplies the families and loads the faces; Sable bundles no catalog.
6
+
7
+ **Keywords:** font picker, typeface, font family, font selector, typography, google fonts, inspector, customizer
8
+
9
+ ## Usage
10
+
11
+ Use FontPicker wherever someone chooses a typeface for a role (body, headings, code) in a panel that shows the result: a theme editor, a document inspector, a brand customizer. It is a DisclosurePicker, so the list opens beneath the row rather than over the preview; arrow keys walk the families (each drawn in its own face) without applying or loading anything, and Enter, Space or a click picks one, loads it and closes the row.
12
+
13
+ ### Best practices
14
+
15
+ - Do: Pass `loadFont` and load faces lazily: `"preview"` fires for every family when the list opens (load just the glyphs of its name — Google Fonts' `text=` parameter makes a 20-font list cost a few KB), `"use"` for the chosen one (load its real weights). Make it idempotent and define it once, outside render.
16
+ - Do: Put non-font choices — "Preset (Figtree)", "Same as body" — in `options`, with a short `valueLabel` for the row. They list first and render in the app's face, so they never read as a family.
17
+ - Do: Give each row a role `label` ("Body", "Headings", "Mono") and stack them; the caption column aligns. Control `open` from the parent so only one list is open at a time.
18
+ - Don't: Leave search on for a short curated list — under a dozen families, pass `searchable={false}`. Search is on by default because font lists are usually long.
19
+ - Don't: Use a Select of family names for this. A popup covers the text it changes, and a family name set in the app's face says nothing about the face.
20
+
21
+ ## Props
22
+
23
+ | Name | Type | Default | Description |
24
+ | --- | --- | --- | --- |
25
+ | value | string | — | The chosen family, or the value of one of `options`. |
26
+ | onValueChange | (value: string) => void | — | Called once a family (or option) is picked — Enter, Space or a click — and only when it differs from `value`; the row then closes. Arrow keys and type-ahead only move focus; Escape closes without a pick. |
27
+ | fonts | readonly { family: string; note?: string; stack?: string }[] | — | The families to offer, in order. `note` is a muted annotation (searched too); `stack` the CSS font-family to draw it in (default `"<family>"`). |
28
+ | options | readonly { value: string; label: string; valueLabel?: string }[] | [] | Non-font choices listed above the fonts, in the app's face. `valueLabel` is what the row shows while it is chosen. |
29
+ | label | ReactNode | — | The row's leading caption — the role the font plays. |
30
+ | listLabel | string | "Font" | Accessible name of the list. |
31
+ | searchable | boolean | true | A search field above the list, filtering by family and note. Pass false for a short curated list (under a dozen). |
32
+ | searchLabel | string | "Search fonts" | Accessible name of the search field. |
33
+ | loadFont | (font, purpose: "preview" \| "use") => void | — | Loads a face on demand: "preview" for every family when the list opens, "use" for the chosen one once it is picked (never while the list is only being walked). Receives your own font objects. Idempotent. |
34
+ | open | boolean | — | Controlled open state, as DisclosurePicker. Control it to keep one of several rows open at a time. |
35
+ | defaultOpen | boolean | false | Uncontrolled initial open state. |
36
+ | onOpenChange | (open: boolean) => void | — | Called with the next open state. |
37
+ | size | "md" \| "xs" | "md" | md for a settings column; xs for the property-grid rung (see DisclosurePicker). |
38
+
39
+ ## Tokens used
40
+
41
+ `foreground`, `muted-foreground`, `popover`, `hairline`, `panel-field`, `ring`
42
+
43
+ ## Motion
44
+
45
+ - **arrival** · root · _no motion_ — A property row is present from first paint.
46
+ - **response** · Trigger · `motion-interactive` — Inherited from DisclosurePicker: the hover step says the value row opens.
47
+ - **transition** · Panel · `motion-disclosure` — Inherited from DisclosurePicker: the list grows in place, tying the panel's movement to this row.
48
+ - **activity** · root · _no motion_ — Faces load in the background and swap in when ready; the names are legible in the fallback meanwhile, so there is nothing to wait on.
49
+
50
+ ## Accessibility
51
+
52
+ - Inherits DisclosurePicker: a button trigger with aria-expanded/controls whose name is the caption plus the family; a listbox named by `listLabel`, aria-selected on the chosen family.
53
+ - Keyboard open focuses the search field; ArrowDown steps from it into the list (ArrowUp from the first option steps back); arrows, Home/End and type-ahead move without applying; Enter or Space picks and closes; Escape clears the query, then closes without a pick and returns focus to the row.
54
+ - Each family is set in its own face, but its name is still plain text, so a screen reader reads the name, not the glyphs.
55
+
56
+ ## Related
57
+
58
+ - Components: DisclosurePicker, ThemeSelector, Select, Combobox
59
+
60
+ ## Examples
61
+
62
+ ### Body font with lazy loading
63
+
64
+ ```tsx
65
+ import { FontPicker } from "@sableui/react"
66
+
67
+ const FONTS = [
68
+ { family: "Inter", note: "Neutral, dense UI" },
69
+ { family: "Figtree", note: "Friendly geometric" },
70
+ { family: "Fraunces", note: "Soft, wonky serif", stack: '"Fraunces", serif' },
71
+ ]
72
+ // Your loader; Sable never fetches fonts itself.
73
+ const loadFont = (font: { family: string }, purpose: "preview" | "use") => {
74
+ const id = `font-${purpose}-${font.family}`
75
+ if (document.getElementById(id)) return
76
+ const link = Object.assign(document.createElement("link"), { id, rel: "stylesheet" })
77
+ const family = font.family.replace(/ /g, "+")
78
+ link.href = purpose === "preview"
79
+ ? `https://fonts.googleapis.com/css2?family=${family}&text=${encodeURIComponent(font.family)}`
80
+ : `https://fonts.googleapis.com/css2?family=${family}:wght@400..700&display=swap`
81
+ document.head.appendChild(link)
82
+ }
83
+ const [family, setFamily] = useState("Inter")
84
+ <FontPicker label="Body" listLabel="Body font" value={family} onValueChange={setFamily} fonts={FONTS} loadFont={loadFont} searchable={false} />
85
+ ```
86
+
87
+ ### With a preset option, one row of several
88
+
89
+ ```tsx
90
+ import { FontPicker } from "@sableui/react"
91
+
92
+ const [open, setOpen] = useState<string | null>(null)
93
+ const [body, setBody] = useState("theme")
94
+ <FontPicker
95
+ label="Body"
96
+ listLabel="Body font"
97
+ value={body}
98
+ onValueChange={setBody}
99
+ options={[{ value: "theme", label: "Preset (Figtree)", valueLabel: "Figtree" }]}
100
+ fonts={[{ family: "Inter" }, { family: "Figtree" }, { family: "Geist" }]}
101
+ open={open === "body"}
102
+ onOpenChange={(next) => setOpen(next ? "body" : null)}
103
+ />
104
+ ```
@@ -0,0 +1,98 @@
1
+ # MediaImage
2
+
3
+ **Status:** beta
4
+
5
+ An image that fills its box and falls back to GradientArt seeded by the item when it has no source or the source fails — Avatar's image-load tracking, for covers, thumbnails and product shots instead of faces.
6
+
7
+ **Keywords:** image, img, image fallback, broken image, cover art, album art, thumbnail, placeholder image, onError, missing image
8
+
9
+ ## Usage
10
+
11
+ Use MediaImage wherever content imagery may be missing or broken: a cover in a grid, a thumbnail in a row, a product shot. It fills the box it is given — an AspectBox for a cover, a sized and rounded box for a thumbnail — shows a quiet muted surface while the image loads, fades the image in once decoded, and paints GradientArt seeded by the item when there is no image to show, so a grid of missing covers still reads as distinct things. For a person use Avatar; for a whole tile with title and actions use MediaTile, which composes this.
12
+
13
+ ### Best practices
14
+
15
+ - Do: Give the box the shape: `<AspectBox ratio={1} className="rounded-md"><MediaImage … /></AspectBox>`, or `size-14 rounded-md` on MediaImage itself for a thumbnail. It clips to whatever radius the box has.
16
+ - Do: Pass `seed` (the title or an id) when `alt` is empty, so the fallback art is the item's own and stays the same in every list it appears in.
17
+ - Do: Leave `alt` empty when a visible title sits next to the image — a tile, a row — and describe it when the image stands alone. A set `alt` also names the fallback, so the description survives a broken URL.
18
+ - Don't: Do not hand-write `<img onError={() => setBroken(true)}>`. An image that fails before hydration never fires the handler on a prerendered page; MediaImage preloads off the DOM and catches it.
19
+ - Don't: Do not use it for profile pictures. A person is a circle with initials — Avatar, whose `variant="art"` is the same fallback with the person's initials over it.
20
+
21
+ ## Props
22
+
23
+ | Name | Type | Default | Description |
24
+ | --- | --- | --- | --- |
25
+ | src | string | — | Image URL. Omitted, empty or failing to load ⇒ the fallback shows. |
26
+ | alt | string | "" | Alt text. Empty is decorative — right beside a visible title. When set and no image shows, it names the fallback instead. |
27
+ | seed | string \| number | — | Seeds the fallback art. Defaults to `alt`; pass the title or an id when `alt` is empty. |
28
+ | fallback | ReactNode | — | Replaces the GradientArt fallback — a glyph or monogram. Fills the frame under the same clipping. |
29
+ | fit | "cover" \| "contain" | "cover" | `cover` crops to fill (artwork, photos); `contain` letterboxes on the muted surface (a logo, a product that must not be cropped). |
30
+ | srcSet | string | — | Responsive sources, passed to the `<img>`. |
31
+ | sizes | string | — | The `sizes` attribute for `srcSet`. |
32
+ | loading | "lazy" \| "eager" | "lazy" | Native lazy loading. Use `eager` for the first row of a grid above the fold. |
33
+ | className | string | — | Sizing and rounding when MediaImage is the box itself (`size-14 rounded-md`). Inside an AspectBox it already fills. |
34
+
35
+ ## Variants
36
+
37
+ - **fit**: cover, contain
38
+
39
+ ## Tokens used
40
+
41
+ `muted`, `art-1`, `art-2`, `art-3`, `art-4`, `art-5`, `art-6`, `art-ground`
42
+
43
+ ## Motion
44
+
45
+ - **arrival** · image · `motion-fade` — The image fades in over the muted surface once it has decoded, so a slow cover settles in instead of popping row by row as the grid loads.
46
+ - **response** · root · _no motion_ — Imagery, not a control. The tile or row around it answers the pointer.
47
+ - **transition** · root · _no motion_ — Its only state change is loading → loaded or failed, which the arrival covers.
48
+ - **activity** · root · _no motion_ — A loading image rests on a static muted surface; a shimmer per cover across a long grid would be noise.
49
+
50
+ ## Accessibility
51
+
52
+ - `alt` defaults to empty (decorative), because the usual neighbour is a visible title that already names the item; describe the image when it stands alone.
53
+ - When no image is on screen and `alt` is set, the frame takes `role="img"` with that label, so a broken URL does not erase the description. The fallback art itself is always aria-hidden.
54
+ - The `<img>` mounts only once it has loaded, so assistive technology never meets a broken-image icon.
55
+
56
+ ## Related
57
+
58
+ - Components: MediaTile, Avatar, GradientArt, AspectBox
59
+
60
+ ## Examples
61
+
62
+ ### Cover with fallback
63
+
64
+ ```tsx
65
+ import { AspectBox, MediaImage } from "@sableui/react"
66
+
67
+ <AspectBox ratio={1} className="rounded-md">
68
+ <MediaImage src={album.cover} seed={album.title} />
69
+ </AspectBox>
70
+ ```
71
+
72
+ ### No image yet
73
+
74
+ ```tsx
75
+ import { AspectBox, MediaImage } from "@sableui/react"
76
+
77
+ <AspectBox ratio={1} className="rounded-md">
78
+ <MediaImage seed="Low tide, high noon" />
79
+ </AspectBox>
80
+ ```
81
+
82
+ ### Row thumbnail
83
+
84
+ ```tsx
85
+ import { MediaImage } from "@sableui/react"
86
+
87
+ <MediaImage src={track.art} seed={track.title} className="size-12 shrink-0 rounded-sm" />
88
+ ```
89
+
90
+ ### Logo, never cropped
91
+
92
+ ```tsx
93
+ import { AspectBox, MediaImage } from "@sableui/react"
94
+
95
+ <AspectBox ratio={16 / 9} className="rounded-lg">
96
+ <MediaImage src={brand.logo} alt={brand.name} fit="contain" />
97
+ </AspectBox>
98
+ ```
@@ -0,0 +1,143 @@
1
+ # MediaTile
2
+
3
+ **Status:** beta
4
+
5
+ A cover-first tile — square, 16:9 or poster artwork with an optional play-style action over it, then a title and a subtitle that can each be links — that is one link as a whole without nesting its other controls inside one.
6
+
7
+ **Keywords:** album, album art, cover, tile, media card, video thumbnail, podcast, playlist, gallery, product tile, poster, grid item, play button, music
8
+
9
+ ## Usage
10
+
11
+ The item in a media grid or shelf: an album, a playlist, a video, a podcast, a film, a photo, a product. The cover leads and carries the weight; the title and subtitle sit underneath without a card around them. The whole tile follows the title's link, the subtitle can link somewhere else (the artist, the channel), and an action — usually play — floats over the cover's corner, revealed on hover and focus and always shown on touch. A missing or broken cover becomes GradientArt seeded by the title, so a grid of items without artwork still reads as distinct things.
12
+
13
+ ### Best practices
14
+
15
+ - Do: Lay tiles out with `Grid` (`<Grid minItemWidth="10rem">`) or a horizontal `Scroller` for a shelf. The tile fills its cell; the grid decides the width.
16
+ - Do: Pass `render={<Link to="…" />}` (or `href`) for where the tile goes, and `subtitleRender` / `subtitleHref` for a second destination. Each stays a separate control; never wrap the tile in your own link.
17
+ - Do: Name the action for its item — `aria-label="Play Low tide, high noon"`, not "Play". A grid of twenty identical "Play" buttons is unusable from a screen reader's list of controls.
18
+ - Do: Use `titleAs="h3"` when the tiles sit under a section heading, so screen-reader users can move tile to tile by heading.
19
+ - Don't: Do not build this from `Card render={<Link />}`. A card that is a link cannot hold a second link or a button — interactive content cannot nest — which is exactly what a media tile needs.
20
+ - Don't: Do not put more than one action on the cover. A second control over the art is a toolbar on a thumbnail; move secondary actions (add to queue, share) into an OverflowMenu in the action slot or onto the item's page.
21
+ - Don't: Do not use it for a framed commerce card with a price and a buy button — that is ProductCardBlock — or for a person, which is an Avatar.
22
+
23
+ ### Anatomy
24
+
25
+ | Part | Required | Description |
26
+ | --- | --- | --- |
27
+ | Cover | yes | An AspectBox holding MediaImage: the image, or GradientArt seeded by the title. Drawn first, read last. |
28
+ | Title | yes | The item's name. When the tile links, this is the link, stretched over the whole tile. |
29
+ | Subtitle | no | The second line. Optionally its own link, raised above the stretched one. |
30
+ | Action | no | One control over the cover's bottom-end corner — usually play. Revealed on hover and focus; always visible on touch. |
31
+ | Badge | no | A static label over the cover's top-start corner — a duration, "New". Click-through. |
32
+
33
+ ## Props
34
+
35
+ | Name | Type | Default | Description |
36
+ | --- | --- | --- | --- |
37
+ | title | ReactNode | — | The item's name. Seeds the fallback art when it is a string. |
38
+ | subtitle | ReactNode | — | The second line — an artist, a channel, a year. |
39
+ | src | string | — | Cover image URL. Without one, or when it fails, the cover is GradientArt seeded by the item. |
40
+ | alt | string | "" | Cover alt text. Empty by default: the title below already names the item. |
41
+ | seed | string \| number | — | Seeds the fallback art. Defaults to `title` when it is a string. |
42
+ | aspect | "square" \| "video" \| "poster" \| number | "square" | The cover's shape: square (albums, podcasts), video (16:9), poster (2:3 — films, books), or any width / height ratio. |
43
+ | size | "sm" \| "md" \| "lg" | "md" | Moves the type tier, the cover-to-text gap, the corner and the overlay inset together. |
44
+ | href | string | — | Where the tile goes. The title becomes the link, stretched over the whole tile. |
45
+ | render | useRender.RenderProp | — | The title link's element, for a router: `render={<Link to="/albums/1" />}`. Takes precedence over `href`. |
46
+ | subtitleHref | string | — | Makes the subtitle its own link — a separate tab stop above the tile's. |
47
+ | subtitleRender | useRender.RenderProp | — | The subtitle link's element, for a router. |
48
+ | action | ReactNode | — | One control over the cover's bottom-end corner, usually a play Button. Hidden until hover or focus on hover-capable devices; always shown on touch. |
49
+ | badge | ReactNode | — | A static label on the cover's top-start corner — `<Badge appearance="overlay">`. |
50
+ | titleAs | "h2" \| "h3" \| "h4" \| "h5" \| "p" | "p" | The title's element. A heading one level below the section's lets screen-reader users move tile to tile. |
51
+ | loading | "lazy" \| "eager" | "lazy" | Native image loading. `eager` for the first row above the fold. |
52
+ | ...div props | Omit<ComponentPropsWithRef<"div">, "title"> | — | Spread onto the root. |
53
+
54
+ ## Variants
55
+
56
+ - **size**: sm, md, lg
57
+ - **aspect**: square, video, poster
58
+
59
+ ## Tokens used
60
+
61
+ `foreground`, `muted-foreground`, `muted`, `overlay`, `ring`
62
+
63
+ ## Motion
64
+
65
+ - **arrival** · Cover · `motion-fade` — The cover image fades in over a quiet surface once it decodes, so a grid loading on a slow connection settles tile by tile instead of popping.
66
+ - **response** · Cover · `motion-interactive` — Hovering the tile deepens the cover with a light scrim and lifts the action in (`reveal-on-hover`), so it is clear the whole tile is one target and where its action is.
67
+ - **transition** · root · _no motion_ — A tile has no states of its own to move between; the action carries any (play / pause).
68
+ - **activity** · root · _no motion_ — Nothing runs inside a tile; a now-playing indicator belongs to the player, not the grid.
69
+
70
+ ## Accessibility
71
+
72
+ - No nested interactive content: the title is the tile's link, and its `stretched-link` overlay covers the tile, while the subtitle link and the action sit above it as separate controls.
73
+ - Tab order is title → subtitle → action. The text comes first in the DOM and the cover is drawn above it with CSS `order`, so focus follows how a reader names the item.
74
+ - Focusing the title link rings the whole tile, rounded like the cover, because the whole tile is what the link is; the subtitle link and the action ring themselves.
75
+ - The action is hidden with opacity only, and only where the device can hover; it stays in the tab order and the accessibility tree, appears when anything in the tile has focus, and is always visible on touch.
76
+ - The cover is decorative by default (`alt=""`), since the title names the item; the fallback art is aria-hidden.
77
+ - Truncated titles keep their full text in the link's accessible name and in a `title` tooltip.
78
+
79
+ ## Related
80
+
81
+ - Components: MediaImage, GradientArt, Grid, AspectBox, Card, Scroller
82
+
83
+ ## Examples
84
+
85
+ ### Album grid
86
+
87
+ ```tsx
88
+ import { Button, Grid, Icon, MediaTile } from "@sableui/react"
89
+ // <Link> = your router's link (react-router, next/link, @tanstack/router, …)
90
+
91
+ <Grid minItemWidth="10rem" gap={3}>
92
+ {albums.map((album) => (
93
+ <MediaTile
94
+ key={album.id}
95
+ title={album.title}
96
+ subtitle={album.artist}
97
+ src={album.cover}
98
+ render={<Link to={`/albums/${album.id}`} />}
99
+ subtitleRender={<Link to={`/artists/${album.artistId}`} />}
100
+ action={
101
+ <Button size="icon" aria-label={`Play ${album.title}`} onClick={() => play(album)}>
102
+ <Icon name="play" variant="solid" />
103
+ </Button>
104
+ }
105
+ />
106
+ ))}
107
+ </Grid>
108
+ ```
109
+
110
+ ### Video tile with a duration
111
+
112
+ ```tsx
113
+ import { Badge, MediaTile } from "@sableui/react"
114
+
115
+ <MediaTile
116
+ aspect="video"
117
+ title="Building a synth from scratch"
118
+ subtitle="Undertow Sessions · 2 days ago"
119
+ src={video.thumbnail}
120
+ href={`/watch/${video.id}`}
121
+ badge={<Badge appearance="overlay">12:04</Badge>}
122
+ />
123
+ ```
124
+
125
+ ### Gallery, no links
126
+
127
+ ```tsx
128
+ import { Grid, MediaTile } from "@sableui/react"
129
+
130
+ <Grid minItemWidth="8rem" gap={2}>
131
+ {photos.map((photo) => (
132
+ <MediaTile key={photo.id} size="sm" title={photo.caption} src={photo.url} alt={photo.description} />
133
+ ))}
134
+ </Grid>
135
+ ```
136
+
137
+ ### Poster shelf
138
+
139
+ ```tsx
140
+ import { MediaTile } from "@sableui/react"
141
+
142
+ <MediaTile aspect="poster" size="lg" titleAs="h3" title="The Long Dark" subtitle="2026 · Drama" href="/films/the-long-dark" />
143
+ ```
@@ -15,6 +15,7 @@ Use PageHeader to establish a page's title, supporting description, and primary
15
15
  - Do: Use one PageHeader per major page or route.
16
16
  - Do: Keep the title literal and put explanation in the lede or nearby content.
17
17
  - Don't: Do not use PageHeader inside repeated cards, dialogs, or compact panels.
18
+ - Don't: Do not use a second PageHeader for a section of the page. It is the route's one h1; each section below it takes a SectionHeader (an h2 with the same title row, description and actions).
18
19
  - Don't: Do not put it outside the Region that holds the page content. It then sits at a different measure from what it introduces, and the page reads as two documents.
19
20
 
20
21
  ## Props
@@ -23,8 +24,8 @@ Use PageHeader to establish a page's title, supporting description, and primary
23
24
  | --- | --- | --- | --- |
24
25
  | title | ReactNode | — | The page title (renders an <h1>). |
25
26
  | description | ReactNode | — | Supporting copy beneath the title. |
26
- | back | ReactNode | — | Leading element before the title — typically a back control on drill-in routes. Keep it a ghost icon Button (size="icon", variant="ghost", aria-label="Back") so it stays quieter than the title. |
27
- | actions | ReactNode | — | Trailing actions aligned to the title row's end. They drop under the title when the row runs out of room, and the title wraps rather than truncating. |
27
+ | back | ReactNode | — | Leading element before the title — typically a back control on drill-in routes. Keep it a ghost icon Button (size="icon", variant="ghost", aria-label="Back") so it stays quieter than the title. A ghost back control bleeds by its inset at the start, so its glyph lines up with the content edge below (Button bleed="none" opts out). |
28
+ | actions | ReactNode | — | Trailing actions aligned to the title row's end. They drop under the title when the row runs out of room, and the title wraps rather than truncating. A ghost or link Button at either end of the group bleeds by its own padding, so its label meets the content edge (the end on the title row, the start once wrapped); filled buttons keep their edge on the line. Direct children only. |
28
29
  | titleAs | "h1" \| "h2" | "h1" | The title's heading element. Keep h1 — the route's one h1. Use h2 only when something above already owns the h1: a shell that renders the route title, or a page showing a PageHeader as an example. |
29
30
 
30
31
  ## Tokens used
@@ -38,7 +39,7 @@ Use PageHeader to establish a page's title, supporting description, and primary
38
39
 
39
40
  ## Related
40
41
 
41
- - Components: AppHeader, Breadcrumbs, Button
42
+ - Components: AppHeader, SectionHeader, Breadcrumbs, Button
42
43
 
43
44
  ## Examples
44
45
 
@@ -69,8 +70,8 @@ import { Button, PageHeader, Region } from "@sableui/react"
69
70
 
70
71
  // ✗ Outside the Region. The header then sits at a different measure from the
71
72
  // content it introduces, and the page reads as two documents.
72
- // ✗ Two on one page. A second one is a section heading — an h2 in a
73
- // smaller step (text-heading-3 or below), not another PageHeader.
73
+ // ✗ Two on one page. A second one is a section heading: a SectionHeader
74
+ // (an h2 on text-heading-3, same title row and actions), not another PageHeader.
74
75
  // ✗ Global actions in it (account, search). Those persist across routes and
75
76
  // belong in AppHeader.
76
77
  ```
@@ -28,7 +28,7 @@ Use PieChart for a share of one total across two to six categories — traffic s
28
28
  | categories | Record<string, { label?: string; tone?: ChartTone; color?: string; icon?: string }> | — | Labels, tones and icons per slice, keyed by the slice's name. Unlisted slices take the tones in order. |
29
29
  | className | string | "h-64" | Sizes the chart. Set a height. |
30
30
  | donut | boolean \| number | false | Cut out the middle: `true` for a ring at 60% of the radius, or a ratio (0–0.9). |
31
- | centerLabel | { value: string; label?: string } | — | A value and caption in the hole of a donut. |
31
+ | centerLabel | { value: string \| number; label?: string } | — | A value and caption in the hole of a donut. The value holds the centre and the caption hangs below it. A number is formatted for you: in full up to 9,999, compact from 10,000 (10.2K, 102.1K, 1.2M); pass a string for units. |
32
32
  | separator | boolean | true | A hairline in the surface colour between slices. |
33
33
  | labels | "none" \| "outside" \| "inside" | "none" | Label each slice outside the pie or inside it. |
34
34
  | labelContent | "value" \| "percent" \| "name" | "value" | What a slice label says. |
@@ -82,7 +82,7 @@ const sources = [
82
82
  index="source"
83
83
  valueKey="visitors"
84
84
  donut
85
- centerLabel={{ value: "752", label: "Visitors" }}
85
+ centerLabel={{ value: 752, label: "Visitors" }}
86
86
  />
87
87
  ```
88
88
 
@@ -32,7 +32,7 @@ Use RadialChart for progress toward goals (several rings), a single score or usa
32
32
  | stacked | boolean | false | Split ONE ring between the rows instead of a ring each. |
33
33
  | track | boolean | true | Draw each ring's full track behind it. |
34
34
  | labels | boolean | false | Name each ring at its start. |
35
- | centerLabel | { value: string; label?: string } | — | A value and caption in the middle. |
35
+ | centerLabel | { value: string \| number; label?: string } | — | A value and caption in the middle. On a full ring the value holds the centre and the caption hangs below it. A number is formatted for you: in full up to 9,999, compact from 10,000 (10.2K); pass a string for units ("7.2 GB", "72%"). |
36
36
  | legend | boolean | true unless rings are labelled | Show the legend. |
37
37
  | tooltip | boolean \| { indicator?: "dot" \| "line" \| "dashed" \| "none"; hideLabel?: boolean; labelFormatter?: (label, datum) => ReactNode; showTotal?: boolean } | true | Show a tooltip on hover and keyboard focus, or configure it: the row marker, hiding the heading, rewriting it, adding a Total row. |
38
38
  | valueFormatter | (value: number) => string | — | Format values everywhere (axis, labels, tooltip, table) — units like "$12,480". Default: compact on the axis, full elsewhere. |