@sableui/react 0.6.1 → 0.7.0

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 (633) hide show
  1. package/AGENTS.md +2 -2
  2. package/CHANGELOG.md +285 -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 +200 -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 +33 -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 +611 -33
  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 +25 -6
  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 +19 -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 +210 -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.js +43 -185
  566. package/dist/styles.css +118 -3
  567. package/dist/{theme-provider-kQszHYji.d.ts → theme-provider-S0NVJs2Y.d.ts} +6 -1
  568. package/dist/{chunk-NETUFI34.js → themes/_contract.js} +14 -2
  569. package/dist/themes/atelier/index.js +17 -1
  570. package/dist/themes/contrast.js +288 -0
  571. package/dist/themes/ember/ember.css +6 -6
  572. package/dist/themes/ember/index.js +16 -1
  573. package/dist/themes/index.d.ts +1 -1
  574. package/dist/themes/index.js +3 -8
  575. package/dist/themes/meridian/index.js +16 -1
  576. package/dist/themes/poppy/index.js +17 -1
  577. package/dist/themes/prism/index.js +17 -1
  578. package/dist/themes/prism/prism.css +2 -2
  579. package/dist/themes/registry.js +59 -0
  580. package/dist/themes/sable/index.js +16 -1
  581. package/dist/{chunk-EFLC4NA3.js → themes/theme-provider.js} +38 -54
  582. package/dist/themes/verdant/index.js +17 -1
  583. package/llms/blocks/media-player-block.md +20 -7
  584. package/llms/blocks/product-card-block.md +6 -4
  585. package/llms/blocks/task-list-block.md +1 -1
  586. package/llms/blocks/verify-code-block.md +1 -1
  587. package/llms/components/app-shell.md +5 -2
  588. package/llms/components/avatar.md +25 -3
  589. package/llms/components/button.md +20 -1
  590. package/llms/components/card.md +1 -1
  591. package/llms/components/disclosure-picker.md +168 -0
  592. package/llms/components/font-picker.md +104 -0
  593. package/llms/components/media-image.md +98 -0
  594. package/llms/components/media-tile.md +143 -0
  595. package/llms/components/page-header.md +6 -5
  596. package/llms/components/section-header.md +144 -0
  597. package/llms/components/sheet.md +10 -0
  598. package/llms/components/sidebar.md +4 -3
  599. package/llms/components/slider.md +14 -2
  600. package/llms/components/theme-selector.md +44 -4
  601. package/llms/components/tree.md +3 -3
  602. package/llms/layout/grid.md +24 -3
  603. package/llms/patterns/dashboard-page.md +3 -6
  604. package/llms/patterns/feed-page.md +4 -4
  605. package/llms/patterns/list-detail-page.md +1 -0
  606. package/llms/patterns/settings-page.md +2 -5
  607. package/llms/reference/utilities.md +84 -4
  608. package/llms/tasks/converting-an-app.md +14 -4
  609. package/llms/tasks/reviewing-a-design.md +15 -0
  610. package/llms-full.txt +981 -65
  611. package/llms.txt +27 -10
  612. package/package.json +1 -1
  613. package/registry.json +2324 -171
  614. package/scripts/agent-setup.mjs +90 -0
  615. package/scripts/design-presets.mjs +46 -4
  616. package/scripts/grade.mjs +195 -29
  617. package/scripts/lint-rules.mjs +486 -10
  618. package/scripts/review-evidence.mjs +28 -1
  619. package/skills/sable/SKILL.md +56 -22
  620. package/targets/ios.json +45 -0
  621. package/dist/chunk-3IWZYBGH.js +0 -398
  622. package/dist/chunk-7MG4SHYX.js +0 -15
  623. package/dist/chunk-7QSDYBXO.js +0 -7421
  624. package/dist/chunk-ADLLDWLW.js +0 -15
  625. package/dist/chunk-C4Z3CF54.js +0 -15
  626. package/dist/chunk-EDXLM7YL.js +0 -5078
  627. package/dist/chunk-EHH4MHO6.js +0 -14
  628. package/dist/chunk-K7RUHUGC.js +0 -14
  629. package/dist/chunk-KO3NAEBF.js +0 -15
  630. package/dist/chunk-L4IEIQQA.js +0 -14
  631. package/dist/chunk-QJ6FWTUG.js +0 -40
  632. package/dist/chunk-VVW4UCDI.js +0 -4908
  633. package/dist/chunk-XAZGMY73.js +0 -187
package/bin/sable.mjs CHANGED
@@ -31,16 +31,16 @@
31
31
  * hand-written capability list is a capability list that lies.
32
32
  */
33
33
 
34
- import { execFileSync } from "node:child_process"
34
+ import { spawnSync } from "node:child_process"
35
35
  import { appendFileSync, existsSync, readdirSync, readFileSync, statSync } from "node:fs"
36
36
  import { dirname, join, relative, resolve } from "node:path"
37
37
  import { fileURLToPath } from "node:url"
38
38
  import { skeletonize } from "../scripts/skeleton.mjs"
39
- import { installAgentSkill } from "../scripts/agent-setup.mjs"
39
+ import { claudeStopHook, installAgentSkill, installClaudeStopHook } from "../scripts/agent-setup.mjs"
40
40
 
41
41
  const HERE = dirname(fileURLToPath(import.meta.url))
42
42
  const PKG_ROOT = join(HERE, "..")
43
- const CWD = process.cwd()
43
+ let CWD = process.cwd()
44
44
 
45
45
  /* ── The catalog. ───────────────────────────────────────────────────────────── */
46
46
 
@@ -99,6 +99,7 @@ const COMMANDS = [
99
99
  { name: "--root", value: "dir", describe: "Write the entries in this ancestor instead, e.g. the repository root when Sable belongs to one workspace package. Run setup in that package." },
100
100
  { name: "--update", value: null, describe: "Refresh unchanged managed entries after a package upgrade." },
101
101
  { name: "--dry-run", value: null, describe: "Check and report the destination without writing files." },
102
+ { name: "--hook", value: null, describe: "Also add a Claude Code Stop hook that runs `sable check` to .claude/settings.json (Claude targets only). Without it, setup prints the hook and writes nothing there." },
102
103
  ],
103
104
  returns: ["setup.agent"],
104
105
  },
@@ -111,17 +112,30 @@ const COMMANDS = [
111
112
  },
112
113
  {
113
114
  name: "search",
114
- summary: "One ranked result set across components, patterns, blocks, layout primitives, utilities, tokens, and docs.",
115
+ summary: "One ranked result set across components, providers, patterns, blocks, layout primitives, utilities, tokens, and docs.",
115
116
  arg: { name: "query", required: true },
116
117
  flags: [
117
- { name: "--type", value: "component|pattern|block|layout|utility|token|doc", describe: "Restrict to one kind." },
118
+ { name: "--type", value: "component|provider|pattern|block|layout|utility|token|doc", describe: "Restrict to one kind." },
118
119
  { name: "--limit", value: "n", describe: "Cap results. Default 20." },
119
120
  ],
120
121
  returns: ["search"],
121
122
  },
123
+ {
124
+ name: "check",
125
+ summary: "Run before declaring a build done: doctor, lint and grade in one pass, then the review steps no tool can verify. Exits 1 if any check fails.",
126
+ arg: { name: "path...", required: false },
127
+ flags: [
128
+ { name: "--ignore", value: "glob", repeatable: true, describe: "Skip matching paths in lint and grade. Repeatable." },
129
+ { name: "--review", value: "review.json", describe: "Also require recorded review evidence (as sable grade --review)." },
130
+ { name: "--icons", value: "icons.json", describe: "Icon names the app adds, as for sable grade --icons." },
131
+ { name: "--root", value: "dir", describe: "Check this project directory instead of the current one. Under --stop-hook it is relative to $CLAUDE_PROJECT_DIR." },
132
+ { name: "--stop-hook", value: null, describe: "Agent Stop-hook mode: checks the project at $CLAUDE_PROJECT_DIR (plus --root); on failure prints the summary to stderr and exits 2, so the agent keeps working instead of ending its turn." },
133
+ ],
134
+ returns: ["check"],
135
+ },
122
136
  {
123
137
  name: "component",
124
- summary: "A component's contract: props, subcomponents, a11y, tokens, examples.",
138
+ summary: "A component's contract: props, subcomponents, a11y, tokens, examples. A provider name resolves to its provider entry.",
125
139
  arg: { name: "Name", required: false },
126
140
  flags: [
127
141
  { name: "--list", value: null, describe: "List the catalog instead." },
@@ -166,6 +180,17 @@ const COMMANDS = [
166
180
  ],
167
181
  returns: ["layout.list", "layout.detail", "layout.doctrine"],
168
182
  },
183
+ {
184
+ name: "provider",
185
+ summary: "App-level providers (SableThemeProvider, IconProvider, MotionProvider, ToastProvider, …): where to mount each, props, and examples.",
186
+ arg: { name: "Name", required: false },
187
+ flags: [
188
+ { name: "--list", value: null, describe: "List the providers instead." },
189
+ { name: "--props", value: null, describe: "Props table only." },
190
+ { name: "--examples", value: null, describe: "Examples only, imports included." },
191
+ ],
192
+ returns: ["provider.list", "provider.detail", "provider.props", "provider.examples"],
193
+ },
169
194
  {
170
195
  name: "token",
171
196
  summary: "The token contract, with intent — what each token means and what it is NOT.",
@@ -175,7 +200,7 @@ const COMMANDS = [
175
200
  },
176
201
  {
177
202
  name: "utility",
178
- summary: "The CSS utility layer, with intent — relative surfaces, frames, state variants, scroll policies — and what each replaces.",
203
+ summary: "The CSS utility layer, with intent — relative surfaces, frames, state variants, scroll policies, motion recipes, type tiers — and what each replaces.",
179
204
  arg: { name: "name", required: false },
180
205
  flags: [
181
206
  { name: "--list", value: null, describe: "List the utilities, grouped by family." },
@@ -185,11 +210,11 @@ const COMMANDS = [
185
210
  },
186
211
  {
187
212
  name: "docs",
188
- summary: "The llms/ markdown mirrors: per-component pages, task walkthroughs, token intent.",
213
+ summary: "The package's own guides (AGENTS, README, CONTRIBUTING, DESIGN, CHANGELOG) and the llms/ markdown mirrors: per-component pages, task walkthroughs, token intent.",
189
214
  arg: { name: "slug", required: false },
190
215
  flags: [
191
216
  { name: "--list", value: null, describe: "List every doc slug." },
192
- { name: "--kind", value: "components|layout|patterns|blocks|reference|tasks|taste", describe: "With --list: one kind." },
217
+ { name: "--kind", value: "package|components|layout|patterns|blocks|foundations|reference|tasks|taste", describe: "With --list: one kind." },
193
218
  ],
194
219
  returns: ["docs.list", "docs.detail"],
195
220
  },
@@ -210,7 +235,7 @@ const COMMANDS = [
210
235
  { name: "--strict", value: null, describe: "Exit 1 for contract findings or missing/incomplete/unresolved review evidence." },
211
236
  { name: "--cli-log", value: "file.jsonl", describe: "Include retrieval usage from a SABLE_CLI_LOG file." },
212
237
  { name: "--icons", value: "icons.json", describe: 'Icon names the app adds with composeIconSets, as { "names": [...] }. Default: .sable/icons.json when present.' },
213
- { name: "--ignore", value: "glob", describe: "Skip matching paths in the checks, with the same globs as sable-lint --ignore. Repeatable." },
238
+ { name: "--ignore", value: "glob", repeatable: true, describe: "Skip matching paths in the checks, with the same globs as sable-lint --ignore. Repeatable." },
214
239
  ],
215
240
  returns: ["grade.score", "grade.template"],
216
241
  },
@@ -232,8 +257,12 @@ const COMMANDS = [
232
257
  name: "lint",
233
258
  summary: "Run the design contract against this project (delegates to sable-lint).",
234
259
  arg: { name: "dir...", required: false },
235
- flags: [{ name: "--rules", value: null, describe: "List the rules." }],
236
- returns: ["(passthrough)"],
260
+ flags: [
261
+ { name: "--rules", value: null, describe: "List the rules." },
262
+ { name: "--ignore", value: "glob", repeatable: true, describe: "Skip matching paths. Repeatable." },
263
+ { name: "--since", value: "version", describe: "What the contract added since that version." },
264
+ ],
265
+ returns: ["lint"],
237
266
  },
238
267
  {
239
268
  name: "atlas",
@@ -292,7 +321,10 @@ const ERROR_CODES = [
292
321
  { code: "ERR_UNKNOWN_LAYOUT", when: "No layout primitive by that name." },
293
322
  { code: "ERR_UNKNOWN_TOKEN_GROUP", when: "No token group by that name." },
294
323
  { code: "ERR_UNKNOWN_UTILITY", when: "No utility or variant by that name. `suggestions` carries near matches." },
295
- { code: "ERR_UNKNOWN_DOC", when: "No doc mirror by that slug." },
324
+ { code: "ERR_UNKNOWN_DOC", when: "No package guide or doc mirror by that slug. `suggestions` carries near matches and the package guides." },
325
+ { code: "ERR_UNKNOWN_PROVIDER", when: "No provider by that name. `suggestions` carries near matches." },
326
+ { code: "ERR_LINT_INPUT", when: "sable lint --json: a path to lint does not exist." },
327
+ { code: "ERR_CHECK_FAILED", when: "sable check: doctor, lint, or grade failed. `data.checks` names which." },
296
328
  { code: "ERR_BAD_FLAG", when: "A flag value is outside its allowed set." },
297
329
  { code: "ERR_NO_RESULTS", when: "A search matched nothing." },
298
330
  { code: "ERR_REGISTRY_MISSING", when: "The installed package has no registry.json." },
@@ -312,24 +344,56 @@ const ERROR_CODES = [
312
344
 
313
345
  /* ── Args. ──────────────────────────────────────────────────────────────────── */
314
346
 
347
+ /**
348
+ * Global flags belong to `sable`, not to the command, so every command accepts
349
+ * them. The parser reads the command's own flag table to know which flags are
350
+ * switches: a switch never takes the next word as its value, so
351
+ * `sable component --dense Button` names Button rather than setting
352
+ * `--dense=Button`. A command that delegates to a script gets argv with the
353
+ * flags only `sable` understands (`--dense`, `--detail`) taken out, because the
354
+ * scripts reject flags they do not know — which is how `sable learn --search
355
+ * "…" --dense` used to fail with "Unknown flag: --dense" while `--help` listed
356
+ * `--dense` as global.
357
+ */
315
358
  const argv = process.argv.slice(2)
359
+ const GLOBAL_VALUE_FLAGS = new Set(GLOBAL_FLAGS.filter((f) => f.value).map((f) => f.name.slice(2)))
360
+ const SABLE_ONLY_FLAGS = new Set(["dense", "detail"])
361
+ const command = (() => {
362
+ for (let i = 0; i < argv.length; i++) {
363
+ const a = argv[i]
364
+ if (!a.startsWith("--")) return a
365
+ if (!a.includes("=") && GLOBAL_VALUE_FLAGS.has(a.slice(2))) i++
366
+ }
367
+ return undefined
368
+ })()
369
+ const SPEC = new Map(
370
+ [...GLOBAL_FLAGS, ...(COMMANDS.find((c) => c.name === command)?.flags ?? [])].map((f) => [f.name.slice(2), f]),
371
+ )
316
372
  const flags = new Map()
317
373
  const positional = []
374
+ /** argv for a delegating command: everything after the command word, minus sable-only flags. */
375
+ const forwarded = []
318
376
  for (let i = 0; i < argv.length; i++) {
319
377
  const a = argv[i]
320
378
  if (!a.startsWith("--")) {
321
379
  positional.push(a)
380
+ if (positional.length > 1) forwarded.push(a)
322
381
  continue
323
382
  }
324
383
  const eq = a.indexOf("=")
325
- if (eq !== -1) flags.set(a.slice(2, eq), a.slice(eq + 1))
326
- else if (argv[i + 1] && !argv[i + 1].startsWith("--")) flags.set(a.slice(2), argv[++i])
327
- else flags.set(a.slice(2), true)
384
+ const name = eq === -1 ? a.slice(2) : a.slice(2, eq)
385
+ const spec = SPEC.get(name)
386
+ const takesValue = eq === -1 && argv[i + 1] !== undefined && !argv[i + 1].startsWith("--") && (spec ? spec.value !== null : true)
387
+ const value = eq !== -1 ? a.slice(eq + 1) : takesValue ? argv[++i] : true
388
+ // Repeatable flags (`--ignore`) keep every value; everything else keeps the last.
389
+ if (flags.has(name) && spec?.repeatable) flags.set(name, [flags.get(name)].flat().concat(value))
390
+ else flags.set(name, spec?.repeatable ? [value] : value)
391
+ if (!SABLE_ONLY_FLAGS.has(name)) forwarded.push(...(eq !== -1 || value === true ? [a] : [a, value]))
328
392
  }
329
393
 
330
394
  const JSON_OUT = flags.has("json")
331
395
  const DENSE = flags.has("dense")
332
- const [command, ...rest] = positional
396
+ const rest = positional.slice(1)
333
397
 
334
398
  /* ── Usage log. Opt-in, off unless SABLE_CLI_LOG names a file. ──────────────────
335
399
  *
@@ -438,6 +502,9 @@ const UTILITIES = R.utilities?.items ?? []
438
502
 
439
503
  const importLine = (name) => `import { ${name} } from "@sableui/react"`
440
504
 
505
+ const PROVIDERS = R.providers?.items ?? []
506
+ const findProvider = (name) => PROVIDERS.find((p) => p.name.toLowerCase() === String(name).toLowerCase())
507
+
441
508
  function findComponent(name) {
442
509
  const hit = COMPONENTS.find((c) => c.name.toLowerCase() === name.toLowerCase())
443
510
  if (hit) return hit
@@ -445,7 +512,7 @@ function findComponent(name) {
445
512
  "ERR_UNKNOWN_COMPONENT",
446
513
  `No component named "${name}".`,
447
514
  [
448
- ...nearest(name, COMPONENTS.map((c) => c.name)).map((n) => `Did you mean ${n}?`),
515
+ ...nearest(name, [...COMPONENTS, ...PROVIDERS].map((c) => c.name)).map((n) => `Did you mean ${n}?`),
449
516
  `sable search "${name}" — searches summaries and keywords too`,
450
517
  "sable component --list",
451
518
  ],
@@ -518,6 +585,9 @@ function cmdSearch() {
518
585
  if (!type || type === "component")
519
586
  for (const c of COMPONENTS)
520
587
  pool.push({ kind: "component", name: c.name, summary: c.summary, status: c.status, group: c.group, _s: score({ name: c.name, keywords: c.keywords, text: `${c.summary} ${c.usage?.description ?? ""}` }) })
588
+ if (!type || type === "provider")
589
+ for (const p of PROVIDERS)
590
+ pool.push({ kind: "provider", name: p.name, summary: p.summary, _s: score({ name: p.name, keywords: ["provider", ...(p.companions ?? [])], text: `${p.summary} ${p.placement ?? ""}` }) })
521
591
  if (!type || type === "pattern")
522
592
  for (const p of PATTERNS)
523
593
  pool.push({ kind: "pattern", name: p.name, summary: p.summary, _s: score({ name: p.name, text: `${p.summary} ${p.intent ?? ""}` }) })
@@ -540,7 +610,7 @@ function cmdSearch() {
540
610
  // Ties resolve in the order AGENTS.md tells an agent to resolve a need:
541
611
  // a component first, then a pattern or primitive, then a utility, then a
542
612
  // token. "segmented" should land on ToggleGroup before the well behind it.
543
- const KIND_ORDER = ["component", "pattern", "block", "layout", "utility", "token", "doc"]
613
+ const KIND_ORDER = ["component", "provider", "pattern", "block", "layout", "utility", "token", "doc"]
544
614
  const hits = pool
545
615
  .filter((h) => h._s > 0)
546
616
  .sort((a, b) => b._s - a._s || KIND_ORDER.indexOf(a.kind) - KIND_ORDER.indexOf(b.kind))
@@ -610,6 +680,10 @@ function cmdComponent() {
610
680
  )
611
681
  }
612
682
 
683
+ // A provider is asked for the way a component is. Answer with its entry rather
684
+ // than ERR_UNKNOWN_COMPONENT, which is what `sable component SableThemeProvider`
685
+ // used to say.
686
+ if (!COMPONENTS.some((c) => c.name.toLowerCase() === rest[0].toLowerCase()) && findProvider(rest[0])) return cmdProvider()
613
687
  const c = findComponent(rest[0])
614
688
 
615
689
  if (flags.has("props"))
@@ -707,6 +781,74 @@ function docSlugFor(name) {
707
781
  return entry.path.split("/").pop().replace(/\.tsx$/, "")
708
782
  }
709
783
 
784
+ /* ── provider ───────────────────────────────────────────────────────────────── */
785
+
786
+ /**
787
+ * App-level providers. Two kinds share one shape: app-wide ones documented in
788
+ * src/providers/providers.meta.ts (props, placement, examples), and ones owned
789
+ * by a component (`partOf`), whose full contract stays with that component.
790
+ */
791
+ function cmdProvider() {
792
+ if (flags.has("list") || !rest[0]) {
793
+ const data = PROVIDERS.map((p) => ({
794
+ name: p.name,
795
+ ...(p.partOf ? { partOf: p.partOf } : { required: p.required.startsWith("Required") }),
796
+ summary: p.summary,
797
+ }))
798
+ return ok("provider.list", { count: data.length, about: R.providers?.about, providers: data }, () =>
799
+ data
800
+ .map((p) =>
801
+ DENSE
802
+ ? `${p.name}${p.partOf ? ` partOf:${p.partOf}` : p.required ? " required" : ""}`
803
+ : ` ${p.name.padEnd(24)}${(p.partOf ? `part of ${p.partOf}` : p.required ? "required" : "optional").padEnd(18)}${firstSentence(p.summary)}`,
804
+ )
805
+ .join("\n"),
806
+ )
807
+ }
808
+ const p = findProvider(rest[0])
809
+ if (!p)
810
+ fail("ERR_UNKNOWN_PROVIDER", `No provider named "${rest[0]}".`, [
811
+ ...nearest(rest[0], PROVIDERS.map((x) => x.name)).map((n) => `Did you mean ${n}?`),
812
+ "sable provider --list",
813
+ ])
814
+ const owner = p.partOf ? COMPONENTS.find((c) => c.name === p.partOf) : null
815
+ const props = p.props ?? []
816
+ const contract = owner ? `sable component ${owner.name} --props` : null
817
+ if (flags.has("props"))
818
+ return ok("provider.props", { name: p.name, props, ...(p.signature ? { signature: p.signature } : {}), ...(contract ? { contract } : {}) }, () =>
819
+ p.partOf ? ` ${p.name} ${p.signature}\n\nFull contract: ${contract}` : renderProps(p),
820
+ )
821
+ if (flags.has("examples")) return ok("provider.examples", { name: p.name, examples: p.examples ?? [] }, () => renderExamples(p))
822
+
823
+ const level = detailLevel("compact")
824
+ const data = { name: p.name, kind: "provider", import: p.import, summary: p.summary }
825
+ if (p.partOf) Object.assign(data, { partOf: p.partOf, signature: p.signature, contract })
826
+ else Object.assign(data, { required: p.required, placement: p.placement })
827
+ if (level !== "brief") {
828
+ if (!p.partOf) data.props = props
829
+ data.companions = p.companions ?? []
830
+ data.related = p.related ?? []
831
+ data.examples = p.examples ?? []
832
+ }
833
+ if (level === "full") data.notes = p.notes ?? []
834
+ ok("provider.detail", data, () =>
835
+ [
836
+ `${p.name} [provider${p.partOf ? `, part of ${p.partOf}` : ""}]`,
837
+ p.summary,
838
+ `\n${p.import}`,
839
+ p.partOf ? `\n ${p.name} ${p.signature}\n\nFull contract: ${contract}` : "",
840
+ p.required ? `\n${p.required}` : "",
841
+ p.placement ? `${DENSE ? "" : "Where: "}${p.placement}` : "",
842
+ level === "brief" || p.partOf ? "" : renderProps(p),
843
+ level === "brief" || !(p.companions ?? []).length ? "" : `\nWith: ${p.companions.join(", ")}`,
844
+ level === "brief" ? "" : renderExamples(p),
845
+ level === "full" ? section("notes", (p.notes ?? []).map(bullet)) : "",
846
+ ]
847
+ .filter(Boolean)
848
+ .join("\n"),
849
+ )
850
+ }
851
+
710
852
  /* ── pattern / block ────────────────────────────────────────────────────────── */
711
853
 
712
854
  /**
@@ -895,9 +1037,30 @@ function findUtility(input) {
895
1037
  const name = input.replace(/^\./, "").replace(/:$/, "")
896
1038
  const exact = UTILITIES.find((u) => u.name === name)
897
1039
  if (exact) return exact
898
- return UTILITIES.filter((u) => u.name.endsWith("-*")).find((u) => name.startsWith(u.name.slice(0, -1)))
1040
+ const functional = UTILITIES.filter((u) => u.name.endsWith("-*")).find((u) => name.startsWith(u.name.slice(0, -1)))
1041
+ if (functional) return functional
1042
+ return TYPE_TIERS.find((u) => u.name === name)
899
1043
  }
900
1044
 
1045
+ /**
1046
+ * The paired `text-*` page tiers are `@utility` declarations too, documented in
1047
+ * `tokens.typography` (one home, so the two descriptions cannot disagree). An
1048
+ * agent types the class it wants to write, so `sable utility text-heading-1`
1049
+ * resolves there instead of answering ERR_UNKNOWN_UTILITY.
1050
+ */
1051
+ const TYPE_TIERS = (R.tokens?.typography ?? [])
1052
+ .filter((t) => typeof t === "object" && t.name)
1053
+ .map((t) => ({
1054
+ name: `text-${t.name}`,
1055
+ kind: "utility",
1056
+ family: "typography",
1057
+ intent: t.intent,
1058
+ whenToUse: t.whenToUse,
1059
+ insteadOf: t.insteadOf,
1060
+ example: `<p className="text-${t.name}">…</p>`,
1061
+ documentedIn: "registry.json → tokens.typography",
1062
+ }))
1063
+
901
1064
  function cmdUtility() {
902
1065
  const name = rest[0]
903
1066
  if (flags.has("list") || !name) {
@@ -930,7 +1093,7 @@ function cmdUtility() {
930
1093
  const u = findUtility(name)
931
1094
  if (!u)
932
1095
  fail("ERR_UNKNOWN_UTILITY", `No utility "${name}".`, [
933
- ...nearest(name, UTILITIES.map((x) => x.name)).map((n) => `sable utility ${n}`),
1096
+ ...nearest(name, [...UTILITIES, ...TYPE_TIERS].map((x) => x.name)).map((n) => `sable utility ${n}`),
934
1097
  `sable search "${name}"`,
935
1098
  "sable utility --list",
936
1099
  ])
@@ -952,9 +1115,29 @@ function cmdUtility() {
952
1115
 
953
1116
  /* ── docs ───────────────────────────────────────────────────────────────────── */
954
1117
 
1118
+ /**
1119
+ * The package's own guides, served by name. llms.txt tells an agent to read
1120
+ * AGENTS.md first, and `sable docs AGENTS` answered ERR_UNKNOWN_DOC — the one
1121
+ * document the CLI was pointing at was the one it could not serve. These ship
1122
+ * in the package's `files`, so they are always next to this CLI.
1123
+ */
1124
+ const PACKAGE_DOCS = [
1125
+ { slug: "AGENTS", file: "AGENTS.md", aliases: ["agents.md", "agent"] },
1126
+ { slug: "README", file: "README.md", aliases: ["readme.md", "install"] },
1127
+ { slug: "CONTRIBUTING", file: "CONTRIBUTING.md", aliases: ["contributing.md"] },
1128
+ { slug: "DESIGN", file: "DESIGN.md", aliases: ["design.md"] },
1129
+ { slug: "CHANGELOG", file: "CHANGELOG.md", aliases: ["changelog.md", "changes"] },
1130
+ { slug: "llms", file: "llms.txt", aliases: ["llms.txt"] },
1131
+ ]
1132
+
1133
+ function packageDoc(name) {
1134
+ const q = String(name).toLowerCase()
1135
+ return PACKAGE_DOCS.find((d) => d.slug.toLowerCase() === q || d.aliases.includes(q))
1136
+ }
1137
+
955
1138
  function docSlugs() {
956
- if (!existsSync(LLMS_DIR)) return []
957
- const out = []
1139
+ const out = PACKAGE_DOCS.filter((d) => existsSync(join(PKG_ROOT, d.file))).map((d) => ({ kind: "package", slug: d.slug, path: join(PKG_ROOT, d.file) }))
1140
+ if (!existsSync(LLMS_DIR)) return out
958
1141
  for (const kind of readdirSync(LLMS_DIR)) {
959
1142
  const dir = join(LLMS_DIR, kind)
960
1143
  if (!statSync(dir).isDirectory()) continue
@@ -968,6 +1151,9 @@ function docSlugs() {
968
1151
  function cmdDocs() {
969
1152
  const all = docSlugs()
970
1153
  const kind = flags.get("kind")
1154
+ const kinds = [...new Set(all.map((d) => d.kind))]
1155
+ if (kind !== undefined && !kinds.includes(kind))
1156
+ fail("ERR_BAD_FLAG", `No doc kind "${kind}".`, kinds.map((k) => `sable docs --list --kind ${k}`))
971
1157
  if (flags.has("list") || !rest[0]) {
972
1158
  const list = kind ? all.filter((d) => d.kind === kind) : all
973
1159
  return ok("docs.list", { count: list.length, docs: list.map(({ kind, slug }) => ({ kind, slug })) }, () => {
@@ -980,14 +1166,18 @@ function cmdDocs() {
980
1166
  }
981
1167
 
982
1168
  const want = docSlugFor(rest[0])
1169
+ const pkg = packageDoc(rest[0])
983
1170
  const hit =
1171
+ (pkg && all.find((d) => d.kind === "package" && d.slug === pkg.slug)) ??
984
1172
  all.find((d) => d.slug === rest[0]) ??
985
1173
  all.find((d) => d.slug === want) ??
986
1174
  all.find((d) => d.slug === slugify(rest[0]))
987
1175
  if (!hit)
988
- fail("ERR_UNKNOWN_DOC", `No doc mirror for "${rest[0]}".`, [
989
- ...nearest(want, all.map((d) => d.slug)).map((s) => `Did you mean ${s}?`),
990
- "sable docs --list",
1176
+ fail("ERR_UNKNOWN_DOC", `No package guide or doc mirror for "${rest[0]}".`, [
1177
+ ...nearest(want, all.map((d) => d.slug), 5).map((s) => `Did you mean sable docs ${s}?`),
1178
+ `Package guides: ${all.filter((d) => d.kind === "package").map((d) => d.slug).join(", ")}`,
1179
+ `Task guides: ${all.filter((d) => d.kind === "tasks").map((d) => d.slug).join(", ")}`,
1180
+ "sable docs --list — every slug, by kind",
991
1181
  ])
992
1182
  const content = readFileSync(hit.path, "utf8")
993
1183
  ok("docs.detail", { kind: hit.kind, slug: hit.slug, content }, content)
@@ -1002,7 +1192,7 @@ function cmdDocs() {
1002
1192
  * package's classes, or `data-sable-theme` set with no theme CSS imported, which
1003
1193
  * renders the default theme and looks like the switcher is broken.
1004
1194
  */
1005
- function cmdDoctor() {
1195
+ function doctorReport() {
1006
1196
  const checks = []
1007
1197
  const add = (status, name, detail, fix) => checks.push({ status, name, detail, ...(fix ? { fix } : {}) })
1008
1198
 
@@ -1095,28 +1285,51 @@ function cmdDoctor() {
1095
1285
  )
1096
1286
  }
1097
1287
 
1288
+ /* The icon trap: icons are opt-in, so an app that renders Sable components with
1289
+ * no IconProvider gets every icon — its own and the ones inside Sable's
1290
+ * components — as nothing, with no error. */
1291
+ const usesSable = /from\s+["']@sableui\/react(?:\/(?:patterns|blocks|layout))?["']/.test(srcText)
1292
+ if (usesSable) {
1293
+ if (/<IconProvider\b/.test(srcText)) add("pass", "icons", "<IconProvider> is mounted.")
1294
+ else
1295
+ add(
1296
+ "fail",
1297
+ "icons",
1298
+ "No <IconProvider> — every <Icon> renders nothing.",
1299
+ 'Wrap your app in <IconProvider set={reiconIconSet}> from "@sableui/react/icons/reicon".',
1300
+ )
1301
+ }
1302
+
1098
1303
  if (pkg && !/resolve\s*:\s*\{[^}]*dedupe|dedupe/.test(readConfigs()))
1099
1304
  add("info", "dedupe", "Could not confirm React dedupe in a bundler config.", 'resolve.dedupe: ["react","react-dom"] — a second React is an invalid-hook blank page.')
1100
1305
 
1101
- const failed = checks.filter((c) => c.status === "fail").length
1102
- const data = { version: R.version, checks, failed }
1306
+ return { version: R.version, checks, failed: checks.filter((c) => c.status === "fail").length }
1307
+ }
1308
+
1309
+ function cmdDoctor() {
1310
+ const data = doctorReport()
1311
+ const { checks, failed } = data
1103
1312
  if (JSON_OUT) {
1104
- process.stdout.write(`${JSON.stringify({ type: "doctor", data }, null, 1)}\n`)
1313
+ process.stdout.write(`${JSON.stringify({ type: "doctor", data }, null, DENSE ? 0 : 1)}\n`)
1105
1314
  } else {
1106
- const mark = { pass: "✓", warn: "!", fail: "✗", info: "·" }
1107
- for (const c of checks) {
1108
- process.stdout.write(` ${mark[c.status]} ${c.name.padEnd(14)}${c.detail}\n`)
1109
- if (c.fix) process.stdout.write(` → ${c.fix}\n`)
1110
- }
1315
+ process.stdout.write(doctorLines(checks))
1111
1316
  process.stdout.write(
1112
1317
  failed
1113
1318
  ? `\n[sable doctor] ✗ ${failed} failure(s). Each one renders wrong without erroring.\n`
1114
1319
  : "\n[sable doctor] ✓ wiring holds.\n",
1115
1320
  )
1116
1321
  }
1322
+ logInvocation(failed ? 1 : 0)
1117
1323
  process.exit(failed ? 1 : 0)
1118
1324
  }
1119
1325
 
1326
+ function doctorLines(checks, indent = " ") {
1327
+ const mark = { pass: "✓", warn: "!", fail: "✗", info: "·" }
1328
+ return checks
1329
+ .map((c) => `${indent}${mark[c.status]} ${c.name.padEnd(14)}${c.detail}\n${c.fix ? `${indent} → ${c.fix}\n` : ""}`)
1330
+ .join("")
1331
+ }
1332
+
1120
1333
  const SKIP_DIRS = new Set(["node_modules", ".next", ".nuxt", "dist", "build", "out", "coverage", ".git", ".turbo"])
1121
1334
  function collectSources(match, limit = 4000) {
1122
1335
  const out = []
@@ -1154,26 +1367,201 @@ function readConfigs() {
1154
1367
 
1155
1368
  /* ── lint (passthrough) ─────────────────────────────────────────────────────── */
1156
1369
 
1370
+ /**
1371
+ * Run one of the package's scripts with the argv a delegating command was given,
1372
+ * minus the flags only `sable` understands. With `--json --dense`, a JSON
1373
+ * document on stdout is re-emitted on one line, so `--dense` means the same
1374
+ * thing here as on the commands `sable` answers itself.
1375
+ */
1376
+ function runScript(file, args = forwarded) {
1377
+ const r = spawnSync(process.execPath, [file, ...args], { encoding: "utf8", cwd: CWD, maxBuffer: 256 * 1024 * 1024 })
1378
+ return { code: r.status ?? 1, stdout: r.stdout ?? "", stderr: r.stderr ?? "" }
1379
+ }
1380
+
1381
+ function passThrough({ code, stdout, stderr }) {
1382
+ let out = stdout
1383
+ if (JSON_OUT && DENSE && out.trim()) {
1384
+ try {
1385
+ out = `${JSON.stringify(JSON.parse(out))}\n`
1386
+ } catch {
1387
+ /* Not one JSON document; print it as it came. */
1388
+ }
1389
+ }
1390
+ process.stdout.write(out)
1391
+ process.stderr.write(stderr)
1392
+ logInvocation(code)
1393
+ process.exit(code)
1394
+ }
1395
+
1157
1396
  function cmdLint() {
1158
- try {
1159
- execFileSync(process.execPath, [join(HERE, "sable-lint.mjs"), ...argv.slice(1)], { stdio: "inherit" })
1160
- process.exit(0)
1161
- } catch (e) {
1162
- process.exit(e.status ?? 1)
1397
+ passThrough(runScript(join(HERE, "sable-lint.mjs")))
1398
+ }
1399
+
1400
+ /* ── check ──────────────────────────────────────────────────────────────────── */
1401
+
1402
+ /**
1403
+ * The review steps from the skill's finish checklist that no tool can verify.
1404
+ * They are printed, numbered, after the automated checks pass or fail, because a
1405
+ * green `sable check` is a contract result, not a design approval — and an agent
1406
+ * that reads "✓" as "done" skips exactly these.
1407
+ */
1408
+ const MANUAL_REVIEW = [
1409
+ { step: "Review the rendered design", how: "sable docs reviewing-a-design — capture desktop (≥1024px) and mobile (≤480px), every promised colour mode; judge brief fit, hierarchy, rhythm, imagery." },
1410
+ { step: "Walk the journey", how: "sable docs reviewing-a-journey — operate the primary task and its recovery path end to end; loading, empty, error and success states; keyboard and touch." },
1411
+ { step: "Review the motion", how: "sable docs reviewing-motion — observe playback and interruption for anything that moves." },
1412
+ { step: "Check reduced motion", how: "Turn on prefers-reduced-motion (or MotionProvider \"reduced\"): state still changes, nothing essential depends on animation, nothing keeps moving." },
1413
+ { step: "Write the reuse inventory", how: "List the Sable components, parts, utilities and token groups used, and every app-owned UI or CSS addition with the catalog search that justified it. Start from the app-owned surfaces sable check lists (sable grade --json → inventory): each composes the component named beside it, or the inventory says why the app owns it." },
1414
+ { step: "Record what you learned", how: "sable docs learning-from-builds — keep conditional lessons with evidence (sable learn --record); never record human approval you did not get." },
1415
+ ]
1416
+
1417
+ function cmdCheck() {
1418
+ /* Where to check. A Stop hook's command must carry no shell syntax — Claude
1419
+ * Code runs it under Git Bash, or PowerShell 5.1 where neither `&&` nor
1420
+ * "${VAR}" works — so it cannot `cd` first. Instead the hook runs from
1421
+ * anywhere and `check` finds the project itself: Claude Code's
1422
+ * CLAUDE_PROJECT_DIR, plus `--root` for a workspace package inside it. */
1423
+ const rootFlag = flags.get("root")
1424
+ if (rootFlag === true) fail("ERR_BAD_ARGUMENT", "--root needs a directory.", ["sable check --root apps/web"])
1425
+ const base = flags.has("stop-hook") && process.env.CLAUDE_PROJECT_DIR ? process.env.CLAUDE_PROJECT_DIR : CWD
1426
+ const target = rootFlag ? resolve(base, rootFlag) : base
1427
+ if (!existsSync(target) || !statSync(target).isDirectory()) fail("ERR_BAD_ARGUMENT", `Not a directory: ${target}`, ["sable check --root <project dir>"])
1428
+ if (target !== CWD) {
1429
+ process.chdir(target)
1430
+ CWD = target
1431
+ }
1432
+ const paths = rest.length ? rest : ["src"]
1433
+ const ignore = [flags.get("ignore") ?? []].flat().filter((g) => typeof g === "string").flatMap((g) => ["--ignore", g])
1434
+ const checks = []
1435
+
1436
+ const doctor = doctorReport()
1437
+ const doctorFailures = doctor.checks.filter((c) => c.status === "fail")
1438
+ checks.push({
1439
+ name: "doctor",
1440
+ status: doctorFailures.length ? "fail" : "pass",
1441
+ summary: doctorFailures.length ? `${doctorFailures.length} wiring failure(s): ${doctorFailures.map((c) => c.name).join(", ")}` : "wiring holds",
1442
+ details: doctorFailures.map((c) => `${c.name}: ${c.detail}${c.fix ? ` → ${c.fix}` : ""}`),
1443
+ data: doctor,
1444
+ })
1445
+
1446
+ const parse = (r) => {
1447
+ try {
1448
+ return JSON.parse(r.stdout)
1449
+ } catch {
1450
+ return null
1451
+ }
1452
+ }
1453
+ const firstLine = (r) => (r.stderr || r.stdout).trim().split("\n")[0] ?? "no output"
1454
+
1455
+ const lintRun = runScript(join(HERE, "sable-lint.mjs"), [...paths, ...ignore, "--json"])
1456
+ const lint = parse(lintRun)
1457
+ if (!lint || lint.type === "error") checks.push({ name: "lint", status: "error", summary: lint?.message ?? firstLine(lintRun), details: [] })
1458
+ else {
1459
+ const v = lint.data.violations
1460
+ checks.push({
1461
+ name: "lint",
1462
+ status: v.length ? "fail" : "pass",
1463
+ summary: v.length ? `${v.length} violation(s) in ${lint.data.filesScanned} files` : `${lint.data.filesScanned} files clean`,
1464
+ details: v.map((x) => `${x.ruleId} — ${x.file}:${x.line} → ${x.match}`),
1465
+ data: lint.data,
1466
+ })
1163
1467
  }
1468
+
1469
+ const review = typeof flags.get("review") === "string" ? ["--review", flags.get("review")] : []
1470
+ const icons = typeof flags.get("icons") === "string" ? ["--icons", flags.get("icons")] : []
1471
+ const gradeRun = runScript(join(PKG_ROOT, "scripts", "grade.mjs"), [...paths, ...ignore, ...review, ...icons, "--json"])
1472
+ const grade = parse(gradeRun)
1473
+ if (!grade || grade.type === "error") checks.push({ name: "grade", status: "error", summary: grade?.message ?? firstLine(gradeRun), details: [] })
1474
+ else {
1475
+ // Contract violations are lint's to report; grade adds what lint cannot see.
1476
+ const LABELS = { hallucinatedComponent: "hallucinated components", hallucinatedProp: "hallucinated props", radixIdiom: "Radix idioms", hardcodedControl: "hardcoded control heights", offContractIcon: "off-contract icons" }
1477
+ const found = Object.entries(LABELS).filter(([k]) => grade.counts[k])
1478
+ const reviewMissing = review.length > 0 && grade.review?.status !== "review-recorded"
1479
+ const details = found.flatMap(([k]) => grade.findings[k].map((f) => `${LABELS[k]} — ${f.file}:${f.line} → ${f.name ?? f.prop ?? f.what ?? f.match}${f.component ? ` on ${f.component}` : ""}${f.from ? ` from ${f.from}${f.reason ? ` (${f.reason})` : ""}` : ""}`))
1480
+ if (reviewMissing) details.push(`review: ${grade.review?.status ?? "missing"}${grade.review?.problems?.length ? ` — ${grade.review.problems.join("; ")}` : ""}`)
1481
+ checks.push({
1482
+ name: "grade",
1483
+ status: found.length || reviewMissing ? "fail" : "pass",
1484
+ summary: [
1485
+ `score ${grade.score}`,
1486
+ found.length ? found.map(([k]) => `${grade.counts[k]} ${LABELS[k]}`).join(", ") : "no hallucinated components or props, Radix idioms, hardcoded heights or off-contract icons",
1487
+ reviewMissing ? `review ${grade.review?.status ?? "missing"}` : "",
1488
+ ]
1489
+ .filter(Boolean)
1490
+ .join("; "),
1491
+ details,
1492
+ data: { score: grade.score, contract: grade.contract, counts: grade.counts, review: grade.review, findings: grade.findings },
1493
+ })
1494
+ }
1495
+ /* App-owned surfaces, from grade. Never a failure: the input to the reuse
1496
+ * inventory (finish step 6), which otherwise gets written from memory. */
1497
+ const inventory = grade && grade.type !== "error" ? (grade.inventory ?? null) : null
1498
+
1499
+ const failed = checks.filter((c) => c.status !== "pass").map((c) => c.name)
1500
+ const data = { paths, passed: !failed.length, failed, checks, inventory, manualReview: MANUAL_REVIEW }
1501
+ const MARK = { pass: "✓", fail: "✗", error: "✗" }
1502
+ const MAX_DETAILS = 12
1503
+ const summaryText = () =>
1504
+ [
1505
+ `sable check — ${paths.join(" ")}`,
1506
+ ...checks.flatMap((c) => [
1507
+ ` ${MARK[c.status]} ${c.name.padEnd(7)}${c.summary}`,
1508
+ ...c.details.slice(0, MAX_DETAILS).map((d) => ` ${d}`),
1509
+ ...(c.details.length > MAX_DETAILS ? [` … ${c.details.length - MAX_DETAILS} more: sable ${c.name}${c.name === "doctor" ? "" : ` ${paths.join(" ")}`}`] : []),
1510
+ ]),
1511
+ "",
1512
+ failed.length ? `[sable check] ✗ ${failed.join(", ")} failed. Fix these and run sable check again.` : "[sable check] ✓ doctor, lint and grade pass.",
1513
+ ].join("\n")
1514
+ const inventoryText = () => {
1515
+ const list = inventory?.surfaces ?? []
1516
+ if (!list.length) return ""
1517
+ return [
1518
+ "",
1519
+ `App-owned surfaces for the reuse inventory (review, not failures) — ${list.length}:`,
1520
+ ...list.slice(0, MAX_DETAILS).map((s) => ` ${s.file}:${s.line} <${s.tag}> ${s.kind} (${s.classes}) → ${s.consider}`),
1521
+ ...(list.length > MAX_DETAILS ? [` … ${list.length - MAX_DETAILS} more: sable grade ${paths.join(" ")} --json → inventory.surfaces`] : []),
1522
+ "",
1523
+ ].join("\n")
1524
+ }
1525
+ const manualText = () =>
1526
+ [
1527
+ "",
1528
+ "Automated checks cannot judge the design. Before calling it done:",
1529
+ ...MANUAL_REVIEW.map((m, i) => ` ${i + 1}. ${m.step}: ${m.how}`),
1530
+ ].join("\n")
1531
+
1532
+ /* Stop-hook mode (Claude Code): exit 2 blocks the agent from ending its turn
1533
+ * and feeds stderr back to it. A Stop hook that always blocks can loop, so
1534
+ * when the agent is already continuing because of a stop hook
1535
+ * (`stop_hook_active`), report and let it stop rather than block again. */
1536
+ if (flags.has("stop-hook")) {
1537
+ let input = {}
1538
+ try {
1539
+ input = JSON.parse(readFileSync(0, "utf8") || "{}")
1540
+ } catch {
1541
+ /* No or malformed hook input: treat it as a first stop. */
1542
+ }
1543
+ logInvocation(failed.length ? 2 : 0)
1544
+ if (!failed.length) process.exit(0)
1545
+ if (input.stop_hook_active) {
1546
+ process.stdout.write(`${summaryText()}\n(sable check still fails; not blocking a second time.)\n`)
1547
+ process.exit(0)
1548
+ }
1549
+ process.stderr.write(`${summaryText()}\n${manualText()}\n`)
1550
+ process.exit(2)
1551
+ }
1552
+
1553
+ if (JSON_OUT) process.stdout.write(`${JSON.stringify({ type: "check", data }, null, DENSE ? 0 : 1)}\n`)
1554
+ else process.stdout.write(`${summaryText()}\n${inventoryText()}${manualText()}\n`)
1555
+ logInvocation(failed.length ? 1 : 0, failed.length ? "ERR_CHECK_FAILED" : undefined)
1556
+ process.exit(failed.length ? 1 : 0)
1164
1557
  }
1165
1558
 
1166
1559
  /* ── Dispatch. ──────────────────────────────────────────────────────────────── */
1167
1560
 
1168
1561
  function cmdGrade() {
1169
- let output
1170
- let code = 0
1171
- try {
1172
- output = execFileSync(process.execPath, [join(PKG_ROOT, "scripts/grade.mjs"), ...argv.slice(1)], { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] })
1173
- } catch (error) {
1174
- output = `${error.stdout ?? ""}${error.stderr ?? ""}`
1175
- code = error.status ?? 1
1176
- }
1562
+ const run = runScript(join(PKG_ROOT, "scripts/grade.mjs"))
1563
+ let output = run.code === 0 ? run.stdout : `${run.stdout}${run.stderr}`
1564
+ const code = run.code
1177
1565
  if (JSON_OUT) {
1178
1566
  try {
1179
1567
  const data = JSON.parse(output)
@@ -1190,13 +1578,35 @@ function cmdLearn() {
1190
1578
  }
1191
1579
 
1192
1580
  function cmdDesignTool(script) {
1581
+ passThrough(runScript(join(PKG_ROOT, "scripts", script)))
1582
+ }
1583
+
1584
+ function cmdSetup() {
1585
+ if (!flags.has("agent") || rest.length) fail("ERR_BAD_ARGUMENT", "Use sable setup --agent from your project root.", ["sable setup --agent --dry-run"])
1586
+ const root = flags.get("root")
1587
+ if (root === true) fail("ERR_BAD_ARGUMENT", "--root needs a directory.", ["pnpm --filter <package> exec sable setup --agent --root ../.."])
1588
+ const target = flags.get("target") ?? "all"
1589
+ const forClaude = target === "all" || target === "claude"
1590
+ if (flags.has("hook") && !forClaude)
1591
+ fail("ERR_BAD_ARGUMENT", "--hook installs a Claude Code Stop hook; use it with --target claude or all.", ["sable setup --agent --target claude --hook"])
1193
1592
  try {
1194
- execFileSync(process.execPath, [join(PKG_ROOT, "scripts", script), ...argv.slice(1)], { stdio: "inherit" })
1195
- logInvocation(0)
1196
- process.exit(0)
1593
+ const options = { root: root ? resolve(CWD, root) : CWD, project: CWD, packageRoot: PKG_ROOT, target, update: flags.has("update"), dryRun: flags.has("dry-run") }
1594
+ // Preflight the settings file too, so a refusal there writes nothing anywhere.
1595
+ if (flags.has("hook")) installClaudeStopHook({ ...options, dryRun: true })
1596
+ const data = installAgentSkill(options)
1597
+ /* The Stop hook changes how the user's agent behaves, so setup never adds it
1598
+ * on its own: it prints the exact entry, and writes it only behind --hook —
1599
+ * the same rule setup keeps for local edits. */
1600
+ if (forClaude) data.stopHook = flags.has("hook") ? installClaudeStopHook(options) : claudeStopHook(options)
1601
+ const hook = data.stopHook
1602
+ const hookText = !hook
1603
+ ? ""
1604
+ : hook.installed
1605
+ ? `\n${data.dryRun ? "Would add" : hook.alreadyPresent ? "Already present:" : "Added"} a Claude Code Stop hook to ${hook.settingsFile}: Claude cannot end a task while \`sable check\` fails.`
1606
+ : `\nOptional: block Claude Code from ending a task while \`sable check\` fails. Add this to ${hook.settingsFile} (merge into an existing "hooks" key), or rerun with --hook to add it:\n\n${JSON.stringify(hook.settings, null, 2)}\n\nThe hook runs: ${hook.command}`
1607
+ ok("setup.agent", data, `${data.dryRun ? "Would install" : "Installed"} Sable skill entries at ${data.destinations.map((d) => relative(CWD, join(data.root, d)).replaceAll("\\", "/")).join(", ")}.\nNext: ${data.next}${hookText}`)
1197
1608
  } catch (error) {
1198
- logInvocation(error.status ?? 1)
1199
- process.exit(error.status ?? 1)
1609
+ fail("ERR_AGENT_SETUP", error.message, ["sable setup --agent --dry-run"])
1200
1610
  }
1201
1611
  }
1202
1612
 
@@ -1217,15 +1627,9 @@ const HANDLERS = {
1217
1627
  ok("target", target, `iOS ${target.platformVersion}${target.status ? ` (${target.status})` : ""}: ${target.designPolicy.principle}\n${target.statusNote ? `${target.statusNote}\n` : ""}${target.designPolicy.rule}\n${(target.designPolicy.layout ?? []).map((l) => `Layout: ${l}\n`).join("")}` + Object.values(target.components).map(c => `${c.source}: ${c.strategy} → ${c.strategy === "contextual" ? "choose by role" : c.components.join(" + ") || "redesign for phone"}`).join("\n"))
1218
1628
  },
1219
1629
  preset: () => cmdDesignTool("design-presets.mjs"),
1220
- setup: () => {
1221
- if (!flags.has("agent") || rest.length) fail("ERR_BAD_ARGUMENT", "Use sable setup --agent from your project root.", ["sable setup --agent --dry-run"])
1222
- const root = flags.get("root")
1223
- if (root === true) fail("ERR_BAD_ARGUMENT", "--root needs a directory.", ["pnpm --filter <package> exec sable setup --agent --root ../.."])
1224
- try {
1225
- const data = installAgentSkill({ root: root ? resolve(CWD, root) : CWD, project: CWD, packageRoot: PKG_ROOT, target: flags.get("target") ?? "all", update: flags.has("update"), dryRun: flags.has("dry-run") })
1226
- ok("setup.agent", data, `${data.dryRun ? "Would install" : "Installed"} Sable skill entries at ${data.destinations.map((d) => relative(CWD, join(data.root, d)).replaceAll("\\", "/")).join(", ")}.\nNext: ${data.next}`)
1227
- } catch (error) { fail("ERR_AGENT_SETUP", error.message, ["sable setup --agent --dry-run"]) }
1228
- },
1630
+ setup: cmdSetup,
1631
+ check: cmdCheck,
1632
+ provider: cmdProvider,
1229
1633
  manifest: cmdManifest,
1230
1634
  search: cmdSearch,
1231
1635
  component: cmdComponent,
@@ -1261,6 +1665,10 @@ if (!command || flags.has("help") || command === "help") {
1261
1665
  cmdManifest()
1262
1666
  }
1263
1667
 
1668
+ // Global flags are validated once, here, so a bad value is an error on every
1669
+ // command — not only on the ones that happen to read it.
1670
+ if (flags.has("detail")) detailLevel()
1671
+
1264
1672
  const handler = HANDLERS[command]
1265
1673
  if (!handler)
1266
1674
  fail("ERR_UNKNOWN_COMMAND", `No command "${command}".`, [