@ui-sdk/web 0.0.0-stage → 0.1.0-beta.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 (467) hide show
  1. package/LICENSE +202 -0
  2. package/README.md +10 -2
  3. package/browser/web/accordion.js +1 -0
  4. package/browser/web/alert-dialog.js +1 -0
  5. package/browser/web/autocomplete.js +1 -0
  6. package/browser/web/avatar.js +1 -0
  7. package/browser/web/button.js +1 -0
  8. package/browser/web/calendar.js +1 -0
  9. package/browser/web/carousel.js +1 -0
  10. package/browser/web/checkbox.js +1 -0
  11. package/browser/web/chunks/0eRGibL6.js +1 -0
  12. package/browser/web/chunks/2bIEUO6_.js +1 -0
  13. package/browser/web/chunks/5A9w2ULz.js +1 -0
  14. package/browser/web/chunks/5RSyydjV.js +1 -0
  15. package/browser/web/chunks/9pWTy2wl.js +1 -0
  16. package/browser/web/chunks/B0f2j6zz.js +1 -0
  17. package/browser/web/chunks/B1rux6br.js +1 -0
  18. package/browser/web/chunks/B3-APaNf.js +1 -0
  19. package/browser/web/chunks/B7EgSgjQ.js +1 -0
  20. package/browser/web/chunks/B8QcOzJU.js +1 -0
  21. package/browser/web/chunks/BFueEkwm.js +3 -0
  22. package/browser/web/chunks/BJj3oS29.js +1 -0
  23. package/browser/web/chunks/BNSvTCpr.js +1 -0
  24. package/browser/web/chunks/BPD0kAZB.js +1 -0
  25. package/browser/web/chunks/BQeduZqT.js +1 -0
  26. package/browser/web/chunks/B_CVFyiz.js +1 -0
  27. package/browser/web/chunks/B_ns1YHO.js +1 -0
  28. package/browser/web/chunks/BcwkVTsR.js +1 -0
  29. package/browser/web/chunks/BeaQ6TYM.js +1 -0
  30. package/browser/web/chunks/BgBB013u.js +1 -0
  31. package/browser/web/chunks/BgsKhHDm.js +1 -0
  32. package/browser/web/chunks/BjMcbuhi.js +9 -0
  33. package/browser/web/chunks/Bjiy_F2N.js +1 -0
  34. package/browser/web/chunks/BqScCLwv.js +1 -0
  35. package/browser/web/chunks/BqX3Utqx.js +1 -0
  36. package/browser/web/chunks/BtVkQf3z.js +1 -0
  37. package/browser/web/chunks/By30h-7B.js +1 -0
  38. package/browser/web/chunks/C1zqYpy2.js +1 -0
  39. package/browser/web/chunks/C436juFf.js +1 -0
  40. package/browser/web/chunks/C4_bULs1.js +1 -0
  41. package/browser/web/chunks/C4omqJEz.js +1 -0
  42. package/browser/web/chunks/C7tt0T3n.js +1 -0
  43. package/browser/web/chunks/C8w7s5dg.js +1 -0
  44. package/browser/web/chunks/CElY4erO.js +1 -0
  45. package/browser/web/chunks/CFODo-m2.js +1 -0
  46. package/browser/web/chunks/CL4wjam9.js +1 -0
  47. package/browser/web/chunks/CMg-8UVf.js +1 -0
  48. package/browser/web/chunks/COWKfakj.js +1 -0
  49. package/browser/web/chunks/CQBU0qZ4.js +1 -0
  50. package/browser/web/chunks/CV6uABgc.js +1 -0
  51. package/browser/web/chunks/C_SWFpWQ.js +1 -0
  52. package/browser/web/chunks/C_Uanbrl.js +1 -0
  53. package/browser/web/chunks/Ce_CSanC.js +1 -0
  54. package/browser/web/chunks/Cg-ZmRhS.js +1 -0
  55. package/browser/web/chunks/Cgf2wPI_.js +1 -0
  56. package/browser/web/chunks/Cuep_3Vf.js +1 -0
  57. package/browser/web/chunks/D02PaHn6.js +1 -0
  58. package/browser/web/chunks/D5LiaI5R.js +1 -0
  59. package/browser/web/chunks/D9UgVYvF.js +1 -0
  60. package/browser/web/chunks/DC08tnkE.js +2 -0
  61. package/browser/web/chunks/DG8YGSvu.js +1 -0
  62. package/browser/web/chunks/DIQd0FYD.js +1 -0
  63. package/browser/web/chunks/DMLqacE8.js +1 -0
  64. package/browser/web/chunks/DSUI3yI-.js +1 -0
  65. package/browser/web/chunks/DT3Hb76X.js +1 -0
  66. package/browser/web/chunks/DY75g3-W.js +1 -0
  67. package/browser/web/chunks/DZpNQKZ9.js +1 -0
  68. package/browser/web/chunks/Db-W1wrC.js +1 -0
  69. package/browser/web/chunks/Dc1xK3Ii.js +1 -0
  70. package/browser/web/chunks/DdF6awFs.js +1 -0
  71. package/browser/web/chunks/DdaCLml8.js +1 -0
  72. package/browser/web/chunks/Dh8MLjQO.js +1 -0
  73. package/browser/web/chunks/DiuslJRy.js +1 -0
  74. package/browser/web/chunks/DjRcw3Y0.js +1 -0
  75. package/browser/web/chunks/Djdv-16J.js +1 -0
  76. package/browser/web/chunks/DnFXe_lF.js +1 -0
  77. package/browser/web/chunks/DneGv1oC.js +1 -0
  78. package/browser/web/chunks/DsVxP-p-.js +1 -0
  79. package/browser/web/chunks/DwDilCo2.js +1 -0
  80. package/browser/web/chunks/DwV48AXv.js +1 -0
  81. package/browser/web/chunks/F37aF4-L.js +1 -0
  82. package/browser/web/chunks/LyB6XIbN.js +1 -0
  83. package/browser/web/chunks/NKnbYGjC.js +1 -0
  84. package/browser/web/chunks/Vk-rVi16.js +1 -0
  85. package/browser/web/chunks/XsM0J1Ex.js +1 -0
  86. package/browser/web/chunks/asSN9iEp.js +1 -0
  87. package/browser/web/chunks/c-AjHfdF.js +1 -0
  88. package/browser/web/chunks/dR-12TPY.js +1 -0
  89. package/browser/web/chunks/hR4xM0EC.js +1 -0
  90. package/browser/web/chunks/hqOaBxAg.js +1 -0
  91. package/browser/web/chunks/kMKqqeG4.js +1 -0
  92. package/browser/web/chunks/kXboBcfi.js +1 -0
  93. package/browser/web/chunks/l4aQIg22.js +1 -0
  94. package/browser/web/chunks/tJC0JSU4.js +1 -0
  95. package/browser/web/chunks/wHdS_QYM.js +1 -0
  96. package/browser/web/chunks/yv8dTvNI.js +1 -0
  97. package/browser/web/chunks/yw9M3nfi.js +1 -0
  98. package/browser/web/collapsible.js +1 -0
  99. package/browser/web/color-picker.js +1 -0
  100. package/browser/web/combobox.js +2 -0
  101. package/browser/web/command.js +1 -0
  102. package/browser/web/core.js +5 -0
  103. package/browser/web/date-input.js +1 -0
  104. package/browser/web/date-picker.js +1 -0
  105. package/browser/web/dialog.js +1 -0
  106. package/browser/web/drawer.js +1 -0
  107. package/browser/web/field.js +1 -0
  108. package/browser/web/file-input.js +1 -0
  109. package/browser/web/hover-card.js +1 -0
  110. package/browser/web/image-cropper.js +1 -0
  111. package/browser/web/input.js +1 -0
  112. package/browser/web/label.js +1 -0
  113. package/browser/web/listbox.js +1 -0
  114. package/browser/web/menu.js +1 -0
  115. package/browser/web/menubar.js +1 -0
  116. package/browser/web/meter.js +1 -0
  117. package/browser/web/navigation-menu.js +1 -0
  118. package/browser/web/number-input.js +1 -0
  119. package/browser/web/otp-input.js +2 -0
  120. package/browser/web/overlays.js +1 -0
  121. package/browser/web/pagination.js +1 -0
  122. package/browser/web/password-input.js +1 -0
  123. package/browser/web/popover.js +1 -0
  124. package/browser/web/preferences.js +1 -0
  125. package/browser/web/progress.js +1 -0
  126. package/browser/web/radio.js +1 -0
  127. package/browser/web/rating.js +1 -0
  128. package/browser/web/resizable.js +1 -0
  129. package/browser/web/scroll-area.js +1 -0
  130. package/browser/web/segmented-control.js +1 -0
  131. package/browser/web/select.js +1 -0
  132. package/browser/web/separator.js +1 -0
  133. package/browser/web/slider.js +1 -0
  134. package/browser/web/switch.js +1 -0
  135. package/browser/web/tabs.js +1 -0
  136. package/browser/web/textarea.js +1 -0
  137. package/browser/web/time-input.js +1 -0
  138. package/browser/web/timeline.js +1 -0
  139. package/browser/web/toast.js +1 -0
  140. package/browser/web/toggle.js +1 -0
  141. package/browser/web/token-input.js +8 -0
  142. package/browser/web/toolbar.js +1 -0
  143. package/browser/web/tooltip.js +1 -0
  144. package/browser/web/tree.js +1 -0
  145. package/browser/web/trigger-menu.js +4 -0
  146. package/browser/web/waterfall.js +1 -0
  147. package/browser/web.css +3378 -0
  148. package/browser/web.js +3 -0
  149. package/custom-elements.json +32315 -0
  150. package/dist/accordion.d.ts +2 -0
  151. package/dist/accordion.js +57 -0
  152. package/dist/action.d.ts +16 -0
  153. package/dist/action.js +39 -0
  154. package/dist/agent.d.ts +2 -0
  155. package/dist/agent.js +7 -0
  156. package/dist/autocomplete.d.ts +2 -0
  157. package/dist/autocomplete.js +120 -0
  158. package/dist/autoload.d.ts +18 -0
  159. package/dist/autoload.js +63 -0
  160. package/dist/avatar.d.ts +2 -0
  161. package/dist/avatar.js +27 -0
  162. package/dist/browser-core.d.ts +2 -0
  163. package/dist/browser-core.js +8 -0
  164. package/dist/button.d.ts +2 -0
  165. package/dist/button.js +35 -0
  166. package/dist/calendar.d.ts +23 -0
  167. package/dist/calendar.js +429 -0
  168. package/dist/carousel.d.ts +2 -0
  169. package/dist/carousel.js +54 -0
  170. package/dist/checkbox.d.ts +4 -0
  171. package/dist/checkbox.js +75 -0
  172. package/dist/collapsible.d.ts +2 -0
  173. package/dist/collapsible.js +28 -0
  174. package/dist/color-picker.d.ts +2 -0
  175. package/dist/color-picker.js +64 -0
  176. package/dist/combobox.d.ts +2 -0
  177. package/dist/combobox.js +334 -0
  178. package/dist/command.d.ts +2 -0
  179. package/dist/command.js +122 -0
  180. package/dist/date-input.d.ts +3 -0
  181. package/dist/date-input.js +87 -0
  182. package/dist/date-picker.d.ts +2 -0
  183. package/dist/date-picker.js +174 -0
  184. package/dist/default-text.d.ts +3 -0
  185. package/dist/default-text.js +24 -0
  186. package/dist/define-element.d.ts +404 -0
  187. package/dist/define-element.js +1489 -0
  188. package/dist/dialog.d.ts +3 -0
  189. package/dist/dialog.js +94 -0
  190. package/dist/drawer.d.ts +2 -0
  191. package/dist/drawer.js +96 -0
  192. package/dist/entries/accordion.d.ts +3 -0
  193. package/dist/entries/accordion.js +7 -0
  194. package/dist/entries/alert-dialog.d.ts +3 -0
  195. package/dist/entries/alert-dialog.js +13 -0
  196. package/dist/entries/autocomplete.d.ts +3 -0
  197. package/dist/entries/autocomplete.js +7 -0
  198. package/dist/entries/avatar.d.ts +2 -0
  199. package/dist/entries/avatar.js +3 -0
  200. package/dist/entries/button.d.ts +2 -0
  201. package/dist/entries/button.js +3 -0
  202. package/dist/entries/calendar.d.ts +3 -0
  203. package/dist/entries/calendar.js +7 -0
  204. package/dist/entries/carousel.d.ts +3 -0
  205. package/dist/entries/carousel.js +7 -0
  206. package/dist/entries/checkbox.d.ts +3 -0
  207. package/dist/entries/checkbox.js +15 -0
  208. package/dist/entries/collapsible.d.ts +3 -0
  209. package/dist/entries/collapsible.js +7 -0
  210. package/dist/entries/color-picker.d.ts +3 -0
  211. package/dist/entries/color-picker.js +7 -0
  212. package/dist/entries/combobox.d.ts +3 -0
  213. package/dist/entries/combobox.js +7 -0
  214. package/dist/entries/command.d.ts +3 -0
  215. package/dist/entries/command.js +7 -0
  216. package/dist/entries/date-input.d.ts +3 -0
  217. package/dist/entries/date-input.js +7 -0
  218. package/dist/entries/date-picker.d.ts +3 -0
  219. package/dist/entries/date-picker.js +7 -0
  220. package/dist/entries/dialog.d.ts +3 -0
  221. package/dist/entries/dialog.js +7 -0
  222. package/dist/entries/drawer.d.ts +3 -0
  223. package/dist/entries/drawer.js +8 -0
  224. package/dist/entries/field.d.ts +2 -0
  225. package/dist/entries/field.js +3 -0
  226. package/dist/entries/file-input.d.ts +3 -0
  227. package/dist/entries/file-input.js +13 -0
  228. package/dist/entries/hover-card.d.ts +3 -0
  229. package/dist/entries/hover-card.js +7 -0
  230. package/dist/entries/image-cropper.d.ts +3 -0
  231. package/dist/entries/image-cropper.js +7 -0
  232. package/dist/entries/input.d.ts +3 -0
  233. package/dist/entries/input.js +8 -0
  234. package/dist/entries/label.d.ts +2 -0
  235. package/dist/entries/label.js +3 -0
  236. package/dist/entries/listbox.d.ts +3 -0
  237. package/dist/entries/listbox.js +7 -0
  238. package/dist/entries/menu.d.ts +3 -0
  239. package/dist/entries/menu.js +7 -0
  240. package/dist/entries/menubar.d.ts +3 -0
  241. package/dist/entries/menubar.js +7 -0
  242. package/dist/entries/meter.d.ts +2 -0
  243. package/dist/entries/meter.js +3 -0
  244. package/dist/entries/navigation-menu.d.ts +3 -0
  245. package/dist/entries/navigation-menu.js +7 -0
  246. package/dist/entries/number-input.d.ts +3 -0
  247. package/dist/entries/number-input.js +7 -0
  248. package/dist/entries/otp-input.d.ts +3 -0
  249. package/dist/entries/otp-input.js +7 -0
  250. package/dist/entries/overlays.d.ts +2 -0
  251. package/dist/entries/overlays.js +3 -0
  252. package/dist/entries/pagination.d.ts +3 -0
  253. package/dist/entries/pagination.js +11 -0
  254. package/dist/entries/password-input.d.ts +3 -0
  255. package/dist/entries/password-input.js +7 -0
  256. package/dist/entries/popover.d.ts +3 -0
  257. package/dist/entries/popover.js +7 -0
  258. package/dist/entries/preferences.d.ts +3 -0
  259. package/dist/entries/preferences.js +13 -0
  260. package/dist/entries/progress.d.ts +2 -0
  261. package/dist/entries/progress.js +3 -0
  262. package/dist/entries/radio.d.ts +3 -0
  263. package/dist/entries/radio.js +7 -0
  264. package/dist/entries/rating.d.ts +3 -0
  265. package/dist/entries/rating.js +8 -0
  266. package/dist/entries/resizable.d.ts +3 -0
  267. package/dist/entries/resizable.js +11 -0
  268. package/dist/entries/scroll-area.d.ts +3 -0
  269. package/dist/entries/scroll-area.js +7 -0
  270. package/dist/entries/segmented-control.d.ts +3 -0
  271. package/dist/entries/segmented-control.js +7 -0
  272. package/dist/entries/select.d.ts +3 -0
  273. package/dist/entries/select.js +7 -0
  274. package/dist/entries/separator.d.ts +2 -0
  275. package/dist/entries/separator.js +3 -0
  276. package/dist/entries/slider.d.ts +3 -0
  277. package/dist/entries/slider.js +7 -0
  278. package/dist/entries/switch.d.ts +3 -0
  279. package/dist/entries/switch.js +7 -0
  280. package/dist/entries/tabs.d.ts +3 -0
  281. package/dist/entries/tabs.js +7 -0
  282. package/dist/entries/textarea.d.ts +3 -0
  283. package/dist/entries/textarea.js +7 -0
  284. package/dist/entries/time-input.d.ts +3 -0
  285. package/dist/entries/time-input.js +7 -0
  286. package/dist/entries/timeline.d.ts +3 -0
  287. package/dist/entries/timeline.js +7 -0
  288. package/dist/entries/toast.d.ts +2 -0
  289. package/dist/entries/toast.js +3 -0
  290. package/dist/entries/toggle.d.ts +3 -0
  291. package/dist/entries/toggle.js +12 -0
  292. package/dist/entries/token-input.d.ts +3 -0
  293. package/dist/entries/token-input.js +17 -0
  294. package/dist/entries/toolbar.d.ts +3 -0
  295. package/dist/entries/toolbar.js +7 -0
  296. package/dist/entries/tooltip.d.ts +3 -0
  297. package/dist/entries/tooltip.js +7 -0
  298. package/dist/entries/tree.d.ts +3 -0
  299. package/dist/entries/tree.js +17 -0
  300. package/dist/entries/trigger-menu.d.ts +3 -0
  301. package/dist/entries/trigger-menu.js +11 -0
  302. package/dist/entries/waterfall.d.ts +3 -0
  303. package/dist/entries/waterfall.js +11 -0
  304. package/dist/events.d.ts +1658 -0
  305. package/dist/events.js +0 -0
  306. package/dist/field.d.ts +2 -0
  307. package/dist/field.js +101 -0
  308. package/dist/file-input.d.ts +2 -0
  309. package/dist/file-input.js +140 -0
  310. package/dist/forward-attributes.d.ts +2 -0
  311. package/dist/forward-attributes.js +22 -0
  312. package/dist/history.d.ts +2 -0
  313. package/dist/history.js +11 -0
  314. package/dist/hover-card.d.ts +2 -0
  315. package/dist/hover-card.js +31 -0
  316. package/dist/image-cropper.d.ts +2 -0
  317. package/dist/image-cropper.js +66 -0
  318. package/dist/index.d.ts +66 -0
  319. package/dist/index.js +269 -0
  320. package/dist/input.d.ts +3 -0
  321. package/dist/input.js +69 -0
  322. package/dist/label.d.ts +2 -0
  323. package/dist/label.js +11 -0
  324. package/dist/list.d.ts +40 -0
  325. package/dist/list.js +192 -0
  326. package/dist/listbox.d.ts +2 -0
  327. package/dist/listbox.js +84 -0
  328. package/dist/menu.d.ts +4 -0
  329. package/dist/menu.js +295 -0
  330. package/dist/menubar.d.ts +4 -0
  331. package/dist/menubar.js +30 -0
  332. package/dist/meter.d.ts +2 -0
  333. package/dist/meter.js +22 -0
  334. package/dist/navigation-menu.d.ts +2 -0
  335. package/dist/navigation-menu.js +114 -0
  336. package/dist/number-input.d.ts +2 -0
  337. package/dist/number-input.js +73 -0
  338. package/dist/observe.d.ts +2 -0
  339. package/dist/observe.js +7 -0
  340. package/dist/otp-input.d.ts +2 -0
  341. package/dist/otp-input.js +72 -0
  342. package/dist/overlays.d.ts +13 -0
  343. package/dist/overlays.js +156 -0
  344. package/dist/pagination.d.ts +2 -0
  345. package/dist/pagination.js +127 -0
  346. package/dist/password-input.d.ts +2 -0
  347. package/dist/password-input.js +57 -0
  348. package/dist/popover.d.ts +2 -0
  349. package/dist/popover.js +64 -0
  350. package/dist/popup.d.ts +40 -0
  351. package/dist/popup.js +112 -0
  352. package/dist/preferences.d.ts +2 -0
  353. package/dist/preferences.js +56 -0
  354. package/dist/progress.d.ts +2 -0
  355. package/dist/progress.js +22 -0
  356. package/dist/radio.d.ts +2 -0
  357. package/dist/radio.js +33 -0
  358. package/dist/rating.d.ts +2 -0
  359. package/dist/rating.js +64 -0
  360. package/dist/resizable.d.ts +2 -0
  361. package/dist/resizable.js +42 -0
  362. package/dist/scroll-area.d.ts +2 -0
  363. package/dist/scroll-area.js +45 -0
  364. package/dist/segmented-control.d.ts +2 -0
  365. package/dist/segmented-control.js +28 -0
  366. package/dist/select.d.ts +2 -0
  367. package/dist/select.js +239 -0
  368. package/dist/separator.d.ts +2 -0
  369. package/dist/separator.js +11 -0
  370. package/dist/slider.d.ts +2 -0
  371. package/dist/slider.js +77 -0
  372. package/dist/tabs.d.ts +2 -0
  373. package/dist/tabs.js +37 -0
  374. package/dist/timeline.d.ts +2 -0
  375. package/dist/timeline.js +27 -0
  376. package/dist/toast.d.ts +20 -0
  377. package/dist/toast.js +177 -0
  378. package/dist/toggle.d.ts +3 -0
  379. package/dist/toggle.js +64 -0
  380. package/dist/token-input.d.ts +2 -0
  381. package/dist/token-input.js +103 -0
  382. package/dist/toolbar.d.ts +2 -0
  383. package/dist/toolbar.js +36 -0
  384. package/dist/tooltip.d.ts +2 -0
  385. package/dist/tooltip.js +35 -0
  386. package/dist/tree.d.ts +2 -0
  387. package/dist/tree.js +147 -0
  388. package/dist/trigger-menu.d.ts +2 -0
  389. package/dist/trigger-menu.js +133 -0
  390. package/dist/waterfall.d.ts +2 -0
  391. package/dist/waterfall.js +140 -0
  392. package/docs/README.md +89 -0
  393. package/docs/agents.md +1523 -0
  394. package/docs/components/accordion.md +1401 -0
  395. package/docs/components/alert-dialog.md +788 -0
  396. package/docs/components/autocomplete.md +1692 -0
  397. package/docs/components/avatar.md +261 -0
  398. package/docs/components/button.md +441 -0
  399. package/docs/components/calendar.md +1536 -0
  400. package/docs/components/carousel.md +1055 -0
  401. package/docs/components/checkbox.md +1045 -0
  402. package/docs/components/collapsible.md +535 -0
  403. package/docs/components/color-picker.md +651 -0
  404. package/docs/components/combobox.md +3274 -0
  405. package/docs/components/command.md +1431 -0
  406. package/docs/components/date-input.md +563 -0
  407. package/docs/components/date-picker.md +1136 -0
  408. package/docs/components/dialog.md +1360 -0
  409. package/docs/components/drawer.md +1201 -0
  410. package/docs/components/field.md +519 -0
  411. package/docs/components/file-input.md +617 -0
  412. package/docs/components/hover-card.md +486 -0
  413. package/docs/components/image-cropper.md +489 -0
  414. package/docs/components/input.md +327 -0
  415. package/docs/components/label.md +208 -0
  416. package/docs/components/listbox.md +1015 -0
  417. package/docs/components/menu.md +1212 -0
  418. package/docs/components/menubar.md +460 -0
  419. package/docs/components/meter.md +298 -0
  420. package/docs/components/navigation-menu.md +1573 -0
  421. package/docs/components/number-input.md +1087 -0
  422. package/docs/components/otp-input.md +558 -0
  423. package/docs/components/pagination.md +639 -0
  424. package/docs/components/password-input.md +649 -0
  425. package/docs/components/popover.md +1433 -0
  426. package/docs/components/preferences.md +243 -0
  427. package/docs/components/progress.md +406 -0
  428. package/docs/components/radio.md +855 -0
  429. package/docs/components/rating.md +431 -0
  430. package/docs/components/resizable.md +866 -0
  431. package/docs/components/scroll-area.md +898 -0
  432. package/docs/components/segmented-control.md +421 -0
  433. package/docs/components/select.md +1957 -0
  434. package/docs/components/separator.md +238 -0
  435. package/docs/components/slider.md +1018 -0
  436. package/docs/components/switch.md +583 -0
  437. package/docs/components/tabs.md +948 -0
  438. package/docs/components/textarea.md +331 -0
  439. package/docs/components/time-input.md +235 -0
  440. package/docs/components/timeline.md +622 -0
  441. package/docs/components/toast.md +1223 -0
  442. package/docs/components/toggle.md +522 -0
  443. package/docs/components/token-input.md +854 -0
  444. package/docs/components/toolbar.md +815 -0
  445. package/docs/components/tooltip.md +860 -0
  446. package/docs/components/tree.md +1243 -0
  447. package/docs/components/trigger-menu.md +1110 -0
  448. package/docs/components/waterfall.md +709 -0
  449. package/docs/controllers.md +66 -0
  450. package/docs/core.md +8027 -0
  451. package/docs/custom-parts.md +91 -0
  452. package/docs/first-component.md +88 -0
  453. package/docs/forms.md +125 -0
  454. package/docs/installation.md +141 -0
  455. package/docs/introduction.md +32 -0
  456. package/docs/lists-and-menus.md +166 -0
  457. package/docs/llms.txt +1566 -0
  458. package/docs/parts-and-composition.md +57 -0
  459. package/docs/popups.md +92 -0
  460. package/docs/rendering.md +71 -0
  461. package/docs/server-rendering.md +118 -0
  462. package/docs/state-and-events.md +134 -0
  463. package/docs/styling-and-animation.md +104 -0
  464. package/docs/utilities/async-options.md +332 -0
  465. package/docs/utilities/shortcuts.md +184 -0
  466. package/html-custom-data.json +14792 -0
  467. package/package.json +302 -3
package/docs/llms.txt ADDED
@@ -0,0 +1,1566 @@
1
+ # ui-sdk for Web Components
2
+
3
+ > @ui-sdk/web renders ui-sdk's framework-free TypeScript core in Web Components. How an interface behaves lives in the core; @ui-sdk/web turns each primitive into parts. No styles, no dependencies, no providers, no portals.
4
+
5
+ This file is the context an agent needs to build with @ui-sdk/web: how Web Components renders a primitive and the conventions every part shares, then every component with a link to its page. Read the pages of the components you use. It's generated from the docs, for this release.
6
+
7
+ - [Core](./core.md): build a component, compose behaviors, and explore the foundations, from defining a primitive to working with state, the browser and text.
8
+ - [Agents](./agents.md): letting an agent operate a page, and WebMCP.
9
+
10
+ ## Introduction
11
+
12
+ These docs cover ui-sdk in Web Components. Using another framework? See React, Vue, Svelte or Solid. To build your own components from behaviors, see [Core](./core.md).
13
+
14
+ `@ui-sdk/web` gives you components such as Select, Dialog, and Tabs as custom elements, built on ui-sdk's behaviors. They handle interactions, focus, and ARIA attributes. You write the HTML and styles.
15
+
16
+ Import the components you need to register their tags, add them to your page, and style them with your own CSS.
17
+
18
+ ### Quick Start
19
+
20
+ Copy this prompt into your agent:
21
+
22
+ ```text prompt
23
+ Set up ui-sdk in this Web Components project. Each component registers its tags when you import its entry, like @ui-sdk/web/dialog.
24
+
25
+ 1. Install @ui-sdk/web and @ui-sdk/core with the project's package manager.
26
+ 2. Add a note to AGENTS.md, creating it if needed: before using ui-sdk, follow the ui-sdk skill if it's installed, and read node_modules/@ui-sdk/web/docs/llms.txt. To build a new component, also search node_modules/@ui-sdk/web/docs/core.md by heading, and read rendering.md next to it.
27
+ 3. If you support skills, run: npx skills add vercel-labs/ui --skill ui-sdk
28
+ ```
29
+
30
+ It installs ui-sdk and points your `AGENTS.md` at the docs that ship with the packages.
31
+
32
+ ### Set It Up Yourself
33
+
34
+ - [Installation](./installation.md): Install the packages and register their custom elements.
35
+ - [First Component](./first-component.md): Add and style your first component.
36
+
37
+ ### Explore the Components
38
+
39
+ The [Using Components guides](./parts-and-composition.md) cover patterns the components share, such as composing parts, handling state, and styling.
40
+
41
+ ## Installation
42
+
43
+ Use these components in an app that supports custom elements. The examples use a bundler, such as Vite, to resolve package imports.
44
+
45
+ ### Install the Packages
46
+
47
+ ```bash
48
+ npm install @ui-sdk/web @ui-sdk/core
49
+ ```
50
+
51
+ `@ui-sdk/web` provides the custom elements. `@ui-sdk/core` provides their behavior and is a peer dependency. Install both so the components and any helpers you use share one copy of the core.
52
+
53
+ ### Register a Component
54
+
55
+ Import each component from its own entry to register its tags:
56
+
57
+ ```ts
58
+ import "@ui-sdk/web/button"
59
+ import "@ui-sdk/web/toggle"
60
+ ```
61
+
62
+ A dialog has separate tags for its trigger, content, and title. One import registers them all:
63
+
64
+ ```ts
65
+ import "@ui-sdk/web/dialog"
66
+ ```
67
+
68
+ Load these imports from your app's browser entry. The root entry, `@ui-sdk/web`, exports helpers and types but registers no elements. Each [component page](./README.md) shows the tags you can use.
69
+
70
+ ### Without a Build Step
71
+
72
+ A plain HTML page can load the components from ui-sdk.dev, with nothing to install. Add one script, and each component loads the first time its tags appear on the page:
73
+
74
+ ```html
75
+ <script type="module" src="https://ui-sdk.dev/web@0.js"></script>
76
+
77
+ <ui-toggle><button>Bold</button></ui-toggle>
78
+ ```
79
+
80
+ `@0` follows the newest 0.x release. To keep a page on one release, use its exact version, like `web@0.4.1.js`. For a quick test, `web.js` with no version follows the newest release of all.
81
+
82
+ To have an agent build the page, ask it to read web.md first. It's these docs in one file, written for a page that loads `web.js` and `web.css`.
83
+
84
+ Each component also has its own file, named like its import, if you'd rather list them:
85
+
86
+ ```html
87
+ <script type="module" src="https://ui-sdk.dev/web@0/toggle.js"></script>
88
+ ```
89
+
90
+ To host the files yourself, copy the `browser` folder from the `@ui-sdk/web` package into your site. It holds `web.js` and the `web` folder it loads from, so keep the two side by side. Browsers only run module scripts from a server, so open the page through one, not from a file on disk.
91
+
92
+ #### Build Your Own
93
+
94
+ `core.js` has every behavior from `@ui-sdk/core`, along with `definePrimitive` and `createParts`. Import it to build a component we don't ship, on the same page as ours. This one is a small grid of cells you can move through with the arrow keys and select with Shift:
95
+
96
+ ```html
97
+ <script type="module">
98
+ import {
99
+ createCellRange,
100
+ createGridFocus,
101
+ createParts,
102
+ definePrimitive,
103
+ } from "https://ui-sdk.dev/web@0/core.js"
104
+
105
+ const sheet = definePrimitive({
106
+ name: "sheet",
107
+ options: { rows: "number", cols: "number" },
108
+ state: () => ({ active: null, range: null }),
109
+ api: (context) => {
110
+ const rows = () => context.options().rows
111
+ const cols = () => context.options().cols
112
+ const grid = createGridFocus(context, { key: "active", rows, cols })
113
+ return {
114
+ grid,
115
+ range: createCellRange(context, { key: "range", grid, rows, cols }),
116
+ }
117
+ },
118
+ slots: (slot) => ({
119
+ root: slot({ attrs: () => ({ role: "grid" }) }),
120
+ row: slot({ attrs: () => ({ role: "row" }) }),
121
+ cell: slot({
122
+ props: { row: "number", col: "number" },
123
+ attrs: (state, { props, api }) => ({
124
+ role: "gridcell",
125
+ tabindex: api.grid.tabIndex(state, props),
126
+ ...api.range.cellAttrs(state, props),
127
+ }),
128
+ on: {
129
+ keydown: (event, { api }) => api.range.keydown(event),
130
+ pointerdown: (event, { api, props }) =>
131
+ api.range.pointerdown(event, props),
132
+ },
133
+ bind: (el, { api, props }) => api.range.bind(el, props),
134
+ }),
135
+ }),
136
+ })
137
+
138
+ createParts(sheet, {
139
+ root: "my-sheet",
140
+ row: "my-sheet-row",
141
+ cell: "my-sheet-cell",
142
+ })
143
+ </script>
144
+
145
+ <my-sheet rows="2" cols="2" aria-label="Numbers">
146
+ <my-sheet-row>
147
+ <my-sheet-cell row="0" col="0">4</my-sheet-cell>
148
+ <my-sheet-cell row="0" col="1">8</my-sheet-cell>
149
+ </my-sheet-row>
150
+ <my-sheet-row>
151
+ <my-sheet-cell row="1" col="0">15</my-sheet-cell>
152
+ <my-sheet-cell row="1" col="1">16</my-sheet-cell>
153
+ </my-sheet-row>
154
+ </my-sheet>
155
+ ```
156
+
157
+ Selected cells get `aria-selected` and `data-selected` to style. [Compose Behaviors](./core.md#compose-behaviors) walks through building a primitive, and [Rendering Primitives](./rendering.md) covers `createParts`. `core.js` shares its files with the components, so a page that uses both loads that code once.
158
+
159
+ ### Add Your Styles
160
+
161
+ Components use light DOM and are unstyled, so you style them with your app's CSS or with Tailwind CSS. [Styling and Animation](./styling-and-animation.md) covers both.
162
+
163
+ You can render your first component without setting up a provider, a portal root, or a UI SDK stylesheet. CSS Modules need a bundler that imports a class-name map.
164
+
165
+ For right-to-left layouts, set `dir="rtl"` on the page or a container. Use logical CSS properties, such as `padding-inline-start`, in your styles. [Server Rendering](./server-rendering.md) covers markup before the elements upgrade.
166
+
167
+ ### Editor Support
168
+
169
+ The package includes `custom-elements.json`, which describes its tags, attributes, properties, and events. Tools that support custom element manifests can read it.
170
+
171
+ For HTML completion in VS Code, add this to your workspace settings:
172
+
173
+ ```json title=".vscode/settings.json"
174
+ { "html.customData": ["./node_modules/@ui-sdk/web/html-custom-data.json"] }
175
+ ```
176
+
177
+ Component imports also register element types with TypeScript. For example, importing `@ui-sdk/web/toggle` gives `document.querySelector("ui-toggle")` its element type.
178
+
179
+ Continue with [First Component](./first-component.md) to add a working toggle to your page.
180
+
181
+ ## First Component
182
+
183
+ Let's add a Bookmark toggle. Press it once to turn it on, then again to turn it off.
184
+
185
+ If you haven't installed the packages yet, start with [Installation](./installation.md).
186
+
187
+ This example uses a bundler that resolves package imports and compiles TypeScript, such as Vite. Keep `index.html` and `bookmark-button.ts` in the same folder.
188
+
189
+ ### Register the Component
190
+
191
+ Create `bookmark-button.ts` and import the toggle entry:
192
+
193
+ ```ts title="bookmark-button.ts"
194
+ import "@ui-sdk/web/toggle"
195
+ ```
196
+
197
+ Importing the toggle entry registers `<ui-toggle>`. The custom element manages the wrapped button's pressed state and sets `aria-pressed` for screen readers. The native button handles Enter and Space.
198
+
199
+ ### Use It on Your Page
200
+
201
+ Add this markup to `index.html`:
202
+
203
+ ```html title="index.html"
204
+ <ui-toggle id="bookmark">
205
+ <button type="button">Bookmark</button>
206
+ </ui-toggle>
207
+
208
+ <script type="module" src="./bookmark-button.ts"></script>
209
+ ```
210
+
211
+ Open your page. You should see a button labeled Bookmark. Next, add styles so you can tell when it is pressed.
212
+
213
+ ### Add Styles
214
+
215
+ When the toggle is pressed, its button has a `data-pressed` attribute. Style the button inside `<ui-toggle>`.
216
+
217
+ With Tailwind CSS configured in your app, add utility classes to the button. The `data-pressed:` variant applies while the toggle is pressed:
218
+
219
+ ```html title="index.html"
220
+ <ui-toggle id="bookmark">
221
+ <button
222
+ type="button"
223
+ class="inline-flex h-8 items-center rounded-md border border-neutral-300 px-3 text-sm font-medium hover:bg-neutral-100 focus-visible:outline-2 focus-visible:outline-offset-2 data-pressed:bg-neutral-900 data-pressed:text-white"
224
+ >
225
+ Bookmark
226
+ </button>
227
+ </ui-toggle>
228
+
229
+ <script type="module" src="./bookmark-button.ts"></script>
230
+ ```
231
+
232
+ Keep the component import in `bookmark-button.ts`:
233
+
234
+ ```ts title="bookmark-button.ts"
235
+ import "@ui-sdk/web/toggle"
236
+ ```
237
+
238
+ Try the styled toggle:
239
+
240
+ ```ts title="first-component.ts"
241
+ import "@ui-sdk/web/toggle"
242
+
243
+ function mount(root: HTMLElement) {
244
+ root.innerHTML = `
245
+ <ui-toggle>
246
+ <button type="button" data-variant="outline">
247
+ Bookmark
248
+ </button>
249
+ </ui-toggle>
250
+ `
251
+ }
252
+
253
+ export { mount }
254
+ ```
255
+
256
+ Click Bookmark to turn it on or off. A filled background means it is on. You can also focus the button with Tab and press Enter or Space.
257
+
258
+ Keep the label the same in both states. `aria-pressed` tells a screen reader whether the toggle is on.
259
+
260
+ ### Keep Going
261
+
262
+ - [Toggle](./components/toggle.md) covers disabled states, controlled state, and groups of toggles.
263
+ - [Components](./README.md) lists the other components you can add with the same install.
264
+ - [Parts and Composition](./parts-and-composition.md) shows how to put a component with several parts together.
265
+ - [State and Events](./state-and-events.md) shows how to read changes and control state from your app.
266
+ - [Styling and Animation](./styling-and-animation.md) covers state attributes, CSS variables, and transitions.
267
+
268
+ ## Parts and Composition
269
+
270
+ A component is a set of custom elements. A Popover has a root to hold its state, a trigger to open it, and content to show.
271
+
272
+ ### Put Parts Together
273
+
274
+ Import the component, then nest its parts as shown in its Composition:
275
+
276
+ ```ts
277
+ import "@ui-sdk/web/popover"
278
+ ```
279
+
280
+ ```html
281
+ <ui-popover id="details">
282
+ <ui-popover-trigger><button>Details</button></ui-popover-trigger>
283
+ <ui-popover-content>
284
+ <ui-popover-title>Project Details</ui-popover-title>
285
+ <p>Last updated today.</p>
286
+ <ui-popover-close><button>Done</button></ui-popover-close>
287
+ </ui-popover-content>
288
+ </ui-popover>
289
+ ```
290
+
291
+ Triggers and Close parts wrap your own `<button>`. These wrappers use `display: contents`, so the button participates in layout directly.
292
+
293
+ Parts find their root by looking up the DOM. You can put your own layout elements between them. An external part can name its root with `for="id"`; [Controllers](./controllers.md) shows an example.
294
+
295
+ ### Choose the Element
296
+
297
+ Use `as-child` when the part should bind to its first child. For example, render a menu item as a link:
298
+
299
+ ```ts
300
+ import "@ui-sdk/web/menu"
301
+ ```
302
+
303
+ ```html
304
+ <ui-menu>
305
+ <ui-menu-trigger><button>Account</button></ui-menu-trigger>
306
+ <ui-menu-content>
307
+ <ui-menu-item as-child><a href="/settings">Settings</a></ui-menu-item>
308
+ </ui-menu-content>
309
+ </ui-menu>
310
+ ```
311
+
312
+ The link receives the part's role, attributes, and keyboard behavior. Triggers and Close parts already use their first button or link, so they don't need `as-child`.
313
+
314
+ ### Name the Control
315
+
316
+ Use a label or title part, such as `<ui-field-label>` or `<ui-dialog-title>`. For an icon button, put `aria-label` on the native button inside its wrapper.
317
+
318
+ Parts write the attributes they manage, including roles, IDs, and ARIA state. Other attributes and your own listeners remain. So does a name or typing hint you write on the element: `aria-label`, `aria-labelledby`, `aria-description`, `title`, `autocomplete`, `autocapitalize`, `autocorrect`, `inputmode` and `spellcheck`. Keep those managed attributes intact when changing the markup.
319
+
320
+ ### Respond to State
321
+
322
+ Use data attributes for styling and DOM events for changes to content. [State and Events](./state-and-events.md) shows how to listen and cancel a change.
323
+
324
+ ## State and Events
325
+
326
+ Set the starting state in HTML. Read and change it through the element's properties once it has upgraded.
327
+
328
+ ### Set the Starting Value
329
+
330
+ ```ts
331
+ import "@ui-sdk/web/toggle"
332
+ ```
333
+
334
+ ```html
335
+ <ui-toggle default-pressed><button type="button">Bold</button></ui-toggle>
336
+ ```
337
+
338
+ The attribute depends on the component: `default-pressed`, `default-checked`, `default-open`, or `default-value`. Leave a boolean attribute out to turn its option off. An option that's on by default, like `loop`, turns off with `"false"`. `disabled` and `required` work like they do on native controls: written at all, even as `"false"`, they're on.
339
+
340
+ ### Change a Value from Script
341
+
342
+ On a page with the toggle above, read or set its live `pressed` property:
343
+
344
+ ```ts
345
+ import "@ui-sdk/web/toggle"
346
+
347
+ await customElements.whenDefined("ui-toggle")
348
+ const toggle = document.querySelector("ui-toggle")!
349
+ toggle.pressed = true
350
+ console.log(toggle.pressed)
351
+ ```
352
+
353
+ Other elements expose `open`, `checked`, or `value`. These are live properties, not React-style controlled props. Changing an attribute such as `pressed` also updates state.
354
+
355
+ Setting a property or attribute fires no event, like setting a native input's `value`. So a page that renders the state back into the element never hears its own write. Changes from the person using it, from the element's `api`, or from an agent fire events.
356
+
357
+ ### Change an Option from Script
358
+
359
+ Options have properties too. On the same toggle, changing `disabled` also updates its attribute:
360
+
361
+ ```ts
362
+ import "@ui-sdk/web/toggle"
363
+
364
+ const toggle = document.querySelector("ui-toggle")!
365
+ toggle.disabled = true
366
+ ```
367
+
368
+ The button is now disabled, and `<ui-toggle>` has a `disabled` attribute. Set the property to `false` to remove it.
369
+
370
+ Other option types follow the same pattern:
371
+
372
+ | Option Type | Property and Attribute |
373
+ | ----------------------- | -------------------------------------------------------- |
374
+ | Boolean, off by default | `disabled = true` adds `disabled`; `false` removes it. |
375
+ | Boolean, on by default | `loop = false` writes `loop="false"`. |
376
+ | Text or number | Reads back as the same type, or `undefined` when absent. |
377
+ | List | Reads as an array; the attribute is comma-separated. |
378
+
379
+ Options an attribute cannot hold, such as a data menu’s `items`, are properties only.
380
+
381
+ ### Respond to a Change
382
+
383
+ Callbacks become lowercase DOM events, such as `pressedchange` and `openchange`. Each one fires on the element whose state changed and doesn't bubble, like `toggle` on `<details>`. Its `detail` holds the requested value and the change details:
384
+
385
+ | Detail | What It Tells You |
386
+ | -------- | --------------------------------------------------------------------------- |
387
+ | `value` | The requested value. |
388
+ | `reason` | What requested the change, such as `press` or `outside-press`. |
389
+ | `event` | The DOM event behind the change, when there is one. |
390
+ | `source` | `keyboard`, `mouse`, `touch`, `pen`, `virtual`, `programmatic`, or `agent`. |
391
+
392
+ Call `event.preventDefault()` to cancel the change. This example keeps the popover open when someone clicks outside:
393
+
394
+ ```html
395
+ <ui-popover id="details">
396
+ <ui-popover-trigger><button>Details</button></ui-popover-trigger>
397
+ <ui-popover-content>
398
+ <ui-popover-title>Project Details</ui-popover-title>
399
+ <p>Last updated today.</p>
400
+ <ui-popover-close><button>Done</button></ui-popover-close>
401
+ </ui-popover-content>
402
+ </ui-popover>
403
+ ```
404
+
405
+ ```ts
406
+ import "@ui-sdk/web/popover"
407
+
408
+ const popover = document.querySelector("ui-popover")!
409
+ popover.addEventListener("openchange", (event) => {
410
+ if (!event.detail.value && event.detail.reason === "outside-press") {
411
+ event.preventDefault()
412
+ }
413
+ })
414
+ ```
415
+
416
+ The Done button still closes it.
417
+
418
+ #### Listen to Many Elements at Once
419
+
420
+ Since events don't bubble, a dialog only hears its own `openchange`, never the one from an alert dialog inside it. To hear every element inside a container with one listener, listen in the capture phase. `event.target` is the element that changed:
421
+
422
+ ```html
423
+ <div id="formatting">
424
+ <ui-toggle><button type="button">Bold</button></ui-toggle>
425
+ <ui-toggle><button type="button">Italic</button></ui-toggle>
426
+ </div>
427
+ <output id="last-change"></output>
428
+ ```
429
+
430
+ ```ts
431
+ import "@ui-sdk/web/toggle"
432
+
433
+ const toolbar = document.querySelector("#formatting")!
434
+ const output = document.querySelector("#last-change")!
435
+
436
+ toolbar.addEventListener(
437
+ "pressedchange",
438
+ (event) => {
439
+ if (event instanceof CustomEvent && event.target instanceof HTMLElement) {
440
+ output.textContent = `${event.target.textContent}: ${event.detail.value}`
441
+ }
442
+ },
443
+ { capture: true }
444
+ )
445
+ ```
446
+
447
+ ### Empty and Multiple Values
448
+
449
+ Select, Tabs, and Radio Group report `null` when nothing is selected. A multiple Select or Combobox uses an array. Accordion and Toggle Group always use arrays. In attributes, lists are comma-separated, such as `default-value="bold,italic"`.
450
+
451
+ Add `debug` to a root to log changes while you work. [Controllers](./controllers.md) covers changing state from elsewhere.
452
+
453
+ ### Move an Element
454
+
455
+ An element keeps its state when you move it, like a native `<details>` keeps `open`. Take it off the page and put it back later, like a view your router keeps, and it picks up where it was: the same tab selected, the same value.
456
+
457
+ ## Styling and Animation
458
+
459
+ Parts render without styles. The component demos use plain CSS, and the same state attributes and CSS variables work with Tailwind CSS or any other stylesheet.
460
+
461
+ ### Style a Part
462
+
463
+ Style the element that receives the behavior. For a toggle, that is the button inside its wrapper. Import `@ui-sdk/web/toggle` on this page:
464
+
465
+ ```html
466
+ <ui-toggle>
467
+ <button
468
+ class="rounded-md border border-neutral-300 px-2.5 py-1.5 text-sm data-pressed:bg-neutral-100 data-disabled:opacity-50"
469
+ >
470
+ Bold
471
+ </button>
472
+ </ui-toggle>
473
+ ```
474
+
475
+ State attributes such as `data-pressed` and `data-disabled` are present or absent. Others have values, such as `data-side="top"`. Each component's Styling section lists its attributes and CSS variables.
476
+
477
+ ### CSS Modules
478
+
479
+ CSS Modules need a bundler that imports a class-name map. With plain CSS, use an ordinary class name and load the stylesheet instead. Import the module and pass its class to the button:
480
+
481
+ ```html
482
+ <ui-toggle id="bold"><button>Bold</button></ui-toggle>
483
+ ```
484
+
485
+ ```ts
486
+ import "@ui-sdk/web/toggle"
487
+ import styles from "./toggle.module.css"
488
+
489
+ const button = document.querySelector("#bold button")!
490
+ button.classList.add(styles.toggle)
491
+ ```
492
+
493
+ ```css title="toggle.module.css"
494
+ .toggle {
495
+ cursor: default;
496
+ border: 1px solid var(--input, #e5e5e5);
497
+ border-radius: 1.125rem;
498
+ background: transparent;
499
+ padding: 0.375rem 0.625rem;
500
+ color: inherit;
501
+ font: inherit;
502
+ font-size: 0.875rem;
503
+ }
504
+
505
+ .toggle[data-pressed] {
506
+ background: var(--muted, #f5f5f5);
507
+ }
508
+
509
+ .toggle[data-disabled] {
510
+ opacity: 0.5;
511
+ }
512
+
513
+ .toggle:focus-visible {
514
+ outline: 2px solid var(--ring, #a3a3a3);
515
+ outline-offset: 2px;
516
+ }
517
+ ```
518
+
519
+ ### Animate Opening and Closing
520
+
521
+ Import `@ui-sdk/web/popover`, then style `data-starting-style` for entry and `data-ending-style` for exit:
522
+
523
+ ```html
524
+ <ui-popover id="details">
525
+ <ui-popover-trigger><button>Details</button></ui-popover-trigger>
526
+ <ui-popover-content
527
+ class="origin-(--transform-origin) transition-[opacity,scale] duration-150 data-starting-style:scale-96 data-starting-style:opacity-0 data-ending-style:scale-96 data-ending-style:opacity-0 motion-reduce:transition-none"
528
+ >
529
+ <ui-popover-title>Project Details</ui-popover-title>
530
+ <p>Last updated today.</p>
531
+ <ui-popover-close><button>Done</button></ui-popover-close>
532
+ </ui-popover-content>
533
+ </ui-popover>
534
+ ```
535
+
536
+ The popup stays shown until its exit transition or animation finishes, then becomes hidden. Put exit styles on `data-ending-style`; `data-closed` describes state but doesn't wait for an exit.
537
+
538
+ Popups provide `--anchor-width` and `--available-height` for sizing. Use those and `data-slot` rather than generated IDs. [Popups](./popups.md) covers placement and containers.
539
+
540
+ ### The ui-sdk Layer
541
+
542
+ A few behaviors need a little CSS, like keeping a closed popup hidden or giving each element a default `display`, so ui-sdk adds it to the page in a cascade layer named `ui-sdk`. Declare that layer at the top of the stylesheet your page links, so your own styles win over it wherever the page puts them. With Tailwind, put it above `@import "tailwindcss"`:
543
+
544
+ ```css
545
+ @layer ui-sdk;
546
+ ```
547
+
548
+ ### Content Security Policy
549
+
550
+ A Content Security Policy without `'unsafe-inline'` in `style-src` blocks the CSS ui-sdk adds to the page. Give ui-sdk your policy's nonce with a meta tag in the page's `<head>`, next to where your server writes the policy:
551
+
552
+ ```html
553
+ <meta property="csp-nonce" nonce="your-nonce" />
554
+ ```
555
+
556
+ With Vite, set `html.cspNonce` in your Vite config to a placeholder, and Vite writes this tag for you. Your server replaces the placeholder with a new nonce on each request. Shadow roots need nothing: ui-sdk adopts constructed style sheets there, which no policy blocks.
557
+
558
+ If your policy has no nonce for styles, add the tag without the `nonce` attribute, and ui-sdk adopts constructed style sheets on the page too. If your code replaces `document.adoptedStyleSheets`, ui-sdk adds its sheets back as elements update or are added.
559
+
560
+ ## Controllers
561
+
562
+ An upgraded element exposes its controller as `api`. Use it when an action that changes the component lives somewhere else.
563
+
564
+ ### Use the Element's Controller
565
+
566
+ ```html
567
+ <button id="toggle-bold">Toggle Bold</button>
568
+ <ui-toggle id="bold"><button>Bold</button></ui-toggle>
569
+ ```
570
+
571
+ ```ts
572
+ import "@ui-sdk/web/toggle"
573
+
574
+ await customElements.whenDefined("ui-toggle")
575
+ const toggle = document.querySelector("ui-toggle")!
576
+ document
577
+ .querySelector("#toggle-bold")!
578
+ .addEventListener("click", () => toggle.api.toggle())
579
+ ```
580
+
581
+ Controller methods run the same change events as user input, including cancellation. Each component lists its methods, such as `open()`, `close()`, `setValue()`, or `focus()`.
582
+
583
+ You can also create a controller with the component's entry, such as `createPopoverController()` from `@ui-sdk/web/popover`, and assign it to a root's `controller` property. Before the element connects, its methods do nothing and return `undefined`. Once it's in the page, they run right away.
584
+
585
+ ### Share a Popup
586
+
587
+ An external trigger names its root with `for`. Several triggers can open the same popup:
588
+
589
+ ```ts
590
+ import "@ui-sdk/web/popover"
591
+ ```
592
+
593
+ ```html
594
+ <ui-popover-trigger for="editor"
595
+ ><button>Edit Notes</button></ui-popover-trigger
596
+ >
597
+ <ui-popover-trigger for="editor"
598
+ ><button>Edit Tasks</button></ui-popover-trigger
599
+ >
600
+ <ui-popover id="editor">
601
+ <ui-popover-content>
602
+ <ui-popover-title>Edit Item</ui-popover-title>
603
+ <ui-popover-close><button>Done</button></ui-popover-close>
604
+ </ui-popover-content>
605
+ </ui-popover>
606
+ ```
607
+
608
+ To distinguish the item, assign a trigger's `payload` property before inserting it into the page. The root emits `payloadchange` with that payload in `event.detail.value`.
609
+
610
+ ### Wait for a Popup to Close
611
+
612
+ `open()` returns a promise that settles when the popup closes:
613
+
614
+ ```ts
615
+ import "@ui-sdk/web/popover"
616
+
617
+ await customElements.whenDefined("ui-popover")
618
+ const editor = document.querySelector("ui-popover")!
619
+ const result = await editor.api.open({ payload: "Notes" })
620
+ console.log(result.reason, result.returnValue)
621
+ ```
622
+
623
+ For confirmations, [Alert Dialog](./components/alert-dialog.md) provides `createConfirm()`, which gives you a function you can await.
624
+
625
+ ## Popups
626
+
627
+ Popovers, menus, selects, and dialogs use the browser's top layer. They render in place, above the page, so you don't need a portal or Positioner part. Their events bubble through the elements around them like any other child's, so a click handler or a link wrapped around a popup also gets the clicks inside it. Keep popups outside clickable cards and links.
628
+
629
+ ### Place the Content
630
+
631
+ ```ts
632
+ import "@ui-sdk/web/popover"
633
+ ```
634
+
635
+ ```html
636
+ <ui-popover id="details">
637
+ <ui-popover-trigger><button>Details</button></ui-popover-trigger>
638
+ <ui-popover-content side="bottom" align="start" side-offset="8">
639
+ <ui-popover-title>Project Details</ui-popover-title>
640
+ <p>Last updated today.</p>
641
+ <ui-popover-close><button>Done</button></ui-popover-close>
642
+ </ui-popover-content>
643
+ </ui-popover>
644
+ ```
645
+
646
+ The popup flips or shifts when there isn't enough room.
647
+
648
+ | Attribute | What It Does |
649
+ | ------------------- | ---------------------------------------------------------------------------------------------------- |
650
+ | `side` | Chooses `top`, `bottom`, `left`, `right`, `start`, or `end`. Start and end follow reading direction. |
651
+ | `align` | Aligns to the start, center, or end of that side. |
652
+ | `side-offset` | Sets the gap from the anchor, in pixels. |
653
+ | `align-offset` | Shifts it along its side. |
654
+ | `collision-padding` | Leaves space from the window's edges. |
655
+
656
+ `anchor` accepts a selector for another element. For a point or rectangle, pass an anchor to the controller's `open()` method.
657
+
658
+ ### Control Dismissal
659
+
660
+ Set `dismissible="false"` on the root to stop Escape, outside presses, focus leaving, Back, and sheet swipes from closing it. The trigger, Close part, and controller can still close it.
661
+
662
+ The `dismissattempt` event reports refused dismissals through `event.detail.reason`. [State and Events](./state-and-events.md) shows how to cancel just one reason.
663
+
664
+ ### Modals and Sheets
665
+
666
+ `modal="true"` makes the rest of the page inert and locks scrolling. Each component documents its default.
667
+
668
+ Menu, Select, Combobox, Autocomplete, Popover, and Dialog support `presentation="auto"` to become a sheet on small touch screens. Use `presentation="sheet"` to always show a sheet.
669
+
670
+ ### Focus
671
+
672
+ Dialogs, alert dialogs, drawers, and popovers move focus into their content when they open. Where it lands depends on how they opened:
673
+
674
+ | Opened With | Focus Goes To |
675
+ | -------------------------------------------- | ------------------------------------------------------------ |
676
+ | Keyboard, screen reader, mouse, pen, or code | The element `initial-focus` picks. |
677
+ | Touch | The content itself, so no on-screen keyboard appears. |
678
+ | Hover | Nowhere. Focus stays where it is. |
679
+ | Rendered open | Nowhere, except in a dialog, which takes focus as it mounts. |
680
+
681
+ `initial-focus` defaults to `auto`: the first focusable element, or the content when it has none. A dialog tries an element with `autofocus` first, and an alert dialog tries its first Close part. Set it to `content` to focus the content itself, `none` to leave focus alone, or a CSS selector.
682
+
683
+ `modal` decides whether Tab can leave. With `modal="true"` or `modal="trap-focus"`, Tab and Shift+Tab wrap inside the content. With `modal="false"`, Tab moves from the trigger into a popover and on to the page after it, and focus leaving closes the popover. A non-modal dialog stays open.
684
+
685
+ When a popup closes while focus is inside it, focus returns to the trigger. If the trigger is gone or can't take focus, focus goes to the element focused before the popup opened, then where the popup it was opened from would return focus, then to the nearest focusable element after where the trigger was. A popup a controller opened returns to the element focused before it first. Set `final-focus` to `none` to leave focus alone, or to a selector to send it somewhere else. Closing because focus moved away never moves it back, and neither does pressing something focusable outside a non-modal popup.
686
+
687
+ Menus, selects, and comboboxes handle focus within their own lists, and tooltips never move it. Each component's page covers its own.
688
+
689
+ ### Hidden Content and Containers
690
+
691
+ Closed content stays in your markup, hidden. The `openchangecomplete` event fires after opening or closing finishes, including animations. `keep-mounted` is available for animation libraries that need the closed element rendered.
692
+
693
+ `container` takes a selector for the element to render into, looked up in the popup's own tree, so a popup in a shadow root finds it in that shadow root. Inside an iframe, a popup handles focus, Escape, and scroll locking in the iframe's own document. Keep a dialog containing a form outside a surrounding form, or move its content with `container`, to avoid nested forms.
694
+
695
+ [Styling and Animation](./styling-and-animation.md) covers transitions.
696
+
697
+ ### Toasts and Confirms
698
+
699
+ Place `<ui-overlay-host>` near your app's root, then call `toast()` from an event handler:
700
+
701
+ ```html
702
+ <ui-overlay-host></ui-overlay-host> <button id="save">Save</button>
703
+ ```
704
+
705
+ ```ts
706
+ import "@ui-sdk/web/overlays"
707
+ import { toast } from "@ui-sdk/web"
708
+
709
+ document
710
+ .querySelector("#save")!
711
+ .addEventListener("click", () => toast.success("Saved"))
712
+ ```
713
+
714
+ See [Toast](./components/toast.md) and [Alert Dialog](./components/alert-dialog.md) for complete examples.
715
+
716
+ ## Lists and Menus
717
+
718
+ Select, Combobox, Autocomplete, and Listbox accept items as a property. Use this when your choices already come from an array. You can also write their item parts in HTML.
719
+
720
+ ### Start with Strings
721
+
722
+ ```html
723
+ <ui-select>
724
+ <ui-select-trigger
725
+ ><button aria-label="Fruit">
726
+ <ui-select-value placeholder="Choose a fruit"></ui-select-value></button
727
+ ></ui-select-trigger>
728
+ <ui-select-content>
729
+ <ui-select-list></ui-select-list>
730
+ </ui-select-content>
731
+ </ui-select>
732
+ ```
733
+
734
+ ```ts
735
+ import "@ui-sdk/web/select"
736
+
737
+ const select = document.querySelector("ui-select")!
738
+ select.items = ["Apple", "Banana", "Pear"]
739
+ ```
740
+
741
+ Each string is both its value and its label. For different values and labels, use `{ value: "apple", label: "Apple" }`. Items can also have `disabled` and `keywords`.
742
+
743
+ Group choices with `{ type: "group", label, items }`, or separate them with `{ type: "separator" }`. Annotate the array with `ListData` and your editor checks every entry where you write it:
744
+
745
+ ```ts
746
+ import type { ListData } from "@ui-sdk/core/select"
747
+
748
+ const plans: ListData = [
749
+ { value: "free", label: "Free" },
750
+ { value: "pro", label: "Pro" },
751
+ { type: "separator" },
752
+ {
753
+ type: "group",
754
+ label: "Contact sales",
755
+ items: [{ value: "enterprise", label: "Enterprise" }],
756
+ },
757
+ ]
758
+ ```
759
+
760
+ For object values, set the `itemToValue` and `itemToLabel` properties, and write the type as `ListData<Plan>`. [Select](./components/select.md) has complete examples.
761
+
762
+ Trigger Menu uses a `triggers` property for its trigger definitions; see [Trigger Menu](./components/trigger-menu.md).
763
+
764
+ ### Search and Load Items
765
+
766
+ The `filter` attribute chooses a matching mode, such as `filter="startsWith"`. Assign a function to the `filter` property for your own matching. [Async Options](./utilities/async-options.md) covers loading from a server.
767
+
768
+ Data lists can virtualize large collections.
769
+
770
+ ### Define a Menu
771
+
772
+ ```html
773
+ <ui-menu>
774
+ <ui-menu-trigger><button>File</button></ui-menu-trigger>
775
+ </ui-menu>
776
+ ```
777
+
778
+ ```ts
779
+ import "@ui-sdk/web/menu"
780
+ import type { MenuData } from "@ui-sdk/web"
781
+
782
+ const fileMenu: MenuData = [
783
+ { value: "open", label: "Open", shortcut: "mod+o" },
784
+ { type: "separator" },
785
+ {
786
+ type: "submenu",
787
+ label: "Share",
788
+ items: [{ value: "email", label: "Email" }],
789
+ },
790
+ ]
791
+
792
+ const menu = document.querySelector("ui-menu")!
793
+ menu.items = fileMenu
794
+ menu.addEventListener("action", (event) => console.log(event.detail.value))
795
+ ```
796
+
797
+ A plain string is an action whose value and label are the string. [Menu](./components/menu.md) covers checkbox items and radio groups. Commands use `CommandData` and trigger menus `TriggerMenuData`, the same entries with fields of their own, and trees `TreeItemData[]`.
798
+
799
+ ### Style Parts from Data
800
+
801
+ Parts rendered from data start out bare, with no classes. To style them, put a `<template>` inside the element and write each part there once, the way you want it to look. Every part the element renders is a copy of the part with the same tag in its template, classes and content included.
802
+
803
+ ```html
804
+ <ui-select default-value="pro">
805
+ <ui-select-trigger>
806
+ <button><ui-select-value></ui-select-value></button>
807
+ </ui-select-trigger>
808
+ <template>
809
+ <ui-select-content class="rounded-2xl bg-white p-1 shadow-lg">
810
+ <ui-select-group>
811
+ <ui-select-group-label
812
+ class="px-2 py-1 text-xs text-neutral-500"
813
+ ></ui-select-group-label>
814
+ <ui-select-item
815
+ class="relative flex items-center rounded-xl py-1.5 ps-2 pe-8 data-highlighted:bg-neutral-100"
816
+ >
817
+ <ui-select-item-indicator class="absolute end-2"
818
+ >✓</ui-select-item-indicator
819
+ >
820
+ </ui-select-item>
821
+ </ui-select-group>
822
+ <ui-select-separator
823
+ class="-mx-1 my-1 h-px bg-neutral-200"
824
+ ></ui-select-separator>
825
+ </ui-select-content>
826
+ </template>
827
+ </ui-select>
828
+ ```
829
+
830
+ ```ts
831
+ const select = document.querySelector("ui-select")!
832
+ select.items = plans
833
+ ```
834
+
835
+ The element looks up each part by its tag, so the template only needs one of each, nested however reads best. Parts you leave out of it render bare.
836
+
837
+ - **Containers copy only themselves.** The content, the list, a group or a submenu's content takes the template's classes, and the data fills it.
838
+ - **Items copy everything inside them.** An indicator, an icon or a shortcut you put in the template's item shows in every item.
839
+ - **The label goes in the text part.** When the template's item has one, like `<ui-select-item-text>`, the label goes there, so you can style what a search matched with classes like `[&_mark]:font-semibold`. Without one, the label comes first in the item.
840
+
841
+ Select, Combobox, Autocomplete, Command, Listbox and Trigger Menu copy their content, list, groups, group labels, items and separators. A combobox with chips copies its chip too, with the remove button you write inside it:
842
+
843
+ ```html
844
+ <template>
845
+ <ui-combobox-chip
846
+ class="flex items-center gap-1 rounded-2xl bg-neutral-200 ps-1.5 pe-0.5 text-xs"
847
+ >
848
+ <ui-combobox-chip-remove>
849
+ <button type="button" class="opacity-50 hover:opacity-100">×</button>
850
+ </ui-combobox-chip-remove>
851
+ </ui-combobox-chip>
852
+ </template>
853
+ ```
854
+
855
+ A menu copies its content, items, checkbox items, radio items, groups, group labels, separators, submenu triggers and submenu content. Put the check in the template's checkbox item and the chevron in its submenu trigger.
856
+
857
+ A tree copies its items, indicators, checkboxes, labels and groups. A branch gets the indicator, and every item gets the checkbox when the tree is `checkable`. A link item's label becomes an `<a>` that takes the label's classes.
858
+
859
+ ```html
860
+ <ui-tree aria-label="Groceries">
861
+ <template>
862
+ <ui-tree-item class="flex flex-wrap items-center">
863
+ <ui-tree-item-indicator
864
+ class="size-4 before:content-['›'] data-expanded:rotate-90"
865
+ ></ui-tree-item-indicator>
866
+ <ui-tree-item-label
867
+ class="flex-1 rounded-xl px-2 data-selected:bg-neutral-100"
868
+ ></ui-tree-item-label>
869
+ <ui-tree-group class="basis-full ps-4"></ui-tree-group>
870
+ </ui-tree-item>
871
+ </template>
872
+ </ui-tree>
873
+ ```
874
+
875
+ ### Keys and Shortcuts
876
+
877
+ `loop` controls whether arrow keys wrap around a list. Components that support `orientation` use it to choose the navigation axis.
878
+
879
+ A trigger's `shortcut="mod+k"` opens it from elsewhere on the page. Data menus run item shortcuts while closed; a menu item written in HTML only displays its shortcut. [Shortcuts](./utilities/shortcuts.md) covers registering your own.
880
+
881
+ ## Forms
882
+
883
+ Give a control a `name` to include its value in a form. Once their modules load, controls such as Checkbox and Select participate in submission, validation, and reset through the browser’s form-associated custom element APIs.
884
+
885
+ ### Submit a Value
886
+
887
+ ```ts
888
+ import "@ui-sdk/web/checkbox"
889
+ ```
890
+
891
+ ```html
892
+ <form action="/preferences" method="post">
893
+ <label
894
+ ><ui-checkbox name="updates" value="yes" default-checked></ui-checkbox>
895
+ Email me updates</label
896
+ >
897
+ <button type="submit">Save</button>
898
+ <button type="reset">Reset</button>
899
+ </form>
900
+ ```
901
+
902
+ The checkbox submits `updates=yes` when checked. Reset restores its default value.
903
+
904
+ On controls that support them, `required`, `disabled`, `read-only`, and `form` follow native conventions. The elements expose `validity`, `checkValidity()`, and `setCustomValidity()`.
905
+
906
+ ### Respond to a Reset
907
+
908
+ A reset also fires `checkedchange` when it requests a different value. You can use the same listener for clicks and resets:
909
+
910
+ ```html
911
+ <form>
912
+ <label>
913
+ <ui-checkbox name="updates" value="yes" default-checked></ui-checkbox>
914
+ Email me updates
915
+ </label>
916
+ <button type="reset">Reset</button>
917
+ <output id="reason">No change yet</output>
918
+ </form>
919
+ ```
920
+
921
+ ```ts
922
+ import "@ui-sdk/web/checkbox"
923
+
924
+ const checkbox = document.querySelector("ui-checkbox")!
925
+ const reason = document.querySelector("#reason")!
926
+
927
+ checkbox.addEventListener("checkedchange", (event) => {
928
+ reason.textContent = event.detail.reason
929
+ })
930
+ ```
931
+
932
+ Run the script after the markup exists. Uncheck the box, then press Reset: `default-checked` restores `true`, and the output reads `reset`. If the checkbox is already at its default, no change event is needed.
933
+
934
+ The element owns its live state. If your application mirrors `checked` elsewhere, update that state from `checkedchange` too.
935
+
936
+ ### Label and Validate a Field
937
+
938
+ `ui-field` finds the control inside it and connects the label, description, and error:
939
+
940
+ ```ts
941
+ import "@ui-sdk/web/field"
942
+ import "@ui-sdk/web/input"
943
+ ```
944
+
945
+ ```html
946
+ <ui-field name="email">
947
+ <ui-field-label>Email</ui-field-label>
948
+ <ui-input>
949
+ <input type="email" required />
950
+ </ui-input>
951
+ <ui-field-description>We'll only use it for receipts.</ui-field-description>
952
+ <ui-field-error></ui-field-error>
953
+ </ui-field>
954
+ ```
955
+
956
+ The control is one of ours, such as `ui-input`, `ui-textarea`, `ui-checkbox` or `ui-select`. [Field](./components/field.md) shows the supported controls.
957
+
958
+ ### Handle a Submission
959
+
960
+ On a normal submit, the browser checks constraints such as `required` and `type="email"` before running your listener.
961
+
962
+ Your server may still reject a valid-looking value. Set the field’s `invalid` property to `true` and show the server’s message. When the user edits the field, clear the message and set `invalid` back to `undefined` so the field uses native validation again. This example expects a `/api/preferences` endpoint:
963
+
964
+ ```html
965
+ <form>
966
+ <ui-field name="email">
967
+ <ui-field-label>Email</ui-field-label>
968
+ <ui-input>
969
+ <input type="email" required />
970
+ </ui-input>
971
+ <ui-field-error></ui-field-error>
972
+ </ui-field>
973
+ <button type="submit">Save</button>
974
+ </form>
975
+ ```
976
+
977
+ ```ts
978
+ import "@ui-sdk/web/field"
979
+ import "@ui-sdk/web/input"
980
+
981
+ const form = document.querySelector("form")!
982
+ const field = form.querySelector("ui-field")!
983
+ const error = form.querySelector("ui-field-error")!
984
+
985
+ form.addEventListener("submit", async (event) => {
986
+ event.preventDefault()
987
+ const response = await fetch("/api/preferences", {
988
+ method: "POST",
989
+ body: new FormData(form),
990
+ })
991
+ field.invalid = response.ok ? undefined : true
992
+ error.textContent = response.ok ? "" : "That email is already taken."
993
+ })
994
+
995
+ form.addEventListener("input", () => {
996
+ field.invalid = undefined
997
+ error.textContent = ""
998
+ })
999
+ ```
1000
+
1001
+ An empty `<ui-field-error>` shows the browser's own message, so clearing the text brings it back. Without a listener, the native form submits normally.
1002
+
1003
+ For a form that must work before component scripts load, see [Server Rendering](./server-rendering.md#before-upgrade).
1004
+
1005
+ ## Server Rendering
1006
+
1007
+ Write custom elements in your server-rendered HTML. Import their modules in the browser to upgrade them and connect their behavior.
1008
+
1009
+ ### Send the Markup
1010
+
1011
+ ```html
1012
+ <ui-toggle default-pressed>
1013
+ <button type="button" aria-pressed="true">Bold</button>
1014
+ </ui-toggle>
1015
+ <script type="module" src="/src/components.ts"></script>
1016
+ ```
1017
+
1018
+ ```ts title="components.ts"
1019
+ import "@ui-sdk/web/toggle"
1020
+ ```
1021
+
1022
+ Your bundler resolves the package import. The browser upgrades `<ui-toggle>` when that module runs. Plain server-rendered HTML does not calculate the primitive’s ARIA state. Here, `aria-pressed="true"` describes the initial button before upgrade and matches `default-pressed`. After upgrade, ui-sdk keeps it in sync as the button toggles.
1023
+
1024
+ ### Before Upgrade
1025
+
1026
+ The Bold button is visible before the script loads, but it cannot toggle yet. Native behavior still works: a submit button can submit a form, and a native input can accept text. Keyboard navigation and state changes managed by ui-sdk need the component’s script.
1027
+
1028
+ Design the initial HTML accordingly. For a control that must submit before upgrade, include a native input or select using the fallback markup shown on its component page.
1029
+
1030
+ ### Wait for an Element
1031
+
1032
+ Run code that uses component properties after the markup exists and the element is defined:
1033
+
1034
+ ```ts
1035
+ import "@ui-sdk/web/toggle"
1036
+
1037
+ await customElements.whenDefined("ui-toggle")
1038
+ const toggle = document.querySelector("ui-toggle")!
1039
+ console.log(toggle.pressed)
1040
+ ```
1041
+
1042
+ Run this after the markup exists. `whenDefined` waits for the tag's registration; it doesn't wait for an element to be inserted into the document.
1043
+
1044
+ ### Saved Preferences and Storage
1045
+
1046
+ Preferences like a color scheme or an emoji skin tone are saved in the browser, so your server can't render them. [Preferences](./core.md#preferences) covers the preferences themselves; this is how they fit server-rendered HTML.
1047
+
1048
+ #### Show Them Before the Scripts Load
1049
+
1050
+ Write `preferencesScript([...])` from `@ui-sdk/core` into your HTML as `<ui-preferences>`'s first child. It runs as the browser parses the page, before any module, and puts every saved value on its `data-*` attribute, so CSS that selects those attributes is right from the first paint. Give the element its preferences once its module loads:
1051
+
1052
+ ```ts title="server.ts"
1053
+ import { preferencesScript } from "@ui-sdk/core"
1054
+ import { colorScheme, skinTone } from "./preferences"
1055
+
1056
+ const page = `<body>
1057
+ <ui-preferences>
1058
+ <script>${preferencesScript([colorScheme, skinTone])}</script>
1059
+ …
1060
+ </ui-preferences>
1061
+ <script type="module" src="/src/page.ts"></script>
1062
+ </body>`
1063
+ ```
1064
+
1065
+ ```ts title="page.ts"
1066
+ import "@ui-sdk/web/preferences"
1067
+ import { colorScheme, skinTone } from "./preferences"
1068
+
1069
+ document.querySelector("ui-preferences")!.preferences = [colorScheme, skinTone]
1070
+ ```
1071
+
1072
+ The script writes preferences made with `global: true` on `<html>`, and the others on `<ui-preferences>`. Once the element has its preferences, it keeps the attributes in step with every change. Add a `nonce` attribute to the script if your Content Security Policy needs one.
1073
+
1074
+ #### Read a Cookie on the Server
1075
+
1076
+ A preference kept in a cookie can render on your server too, content included. Pass the request's `Cookie` header to `createCookieStorage` while rendering that request, and read the value to write it into the HTML:
1077
+
1078
+ ```ts
1079
+ const skinTone = createPreference({
1080
+ values: ["default", "light", "medium", "dark"],
1081
+ defaultValue: "default",
1082
+ attribute: "data-skin-tone",
1083
+ storage: createCookieStorage("skin-tone", {
1084
+ cookie: request.headers.get("cookie"),
1085
+ }),
1086
+ })
1087
+
1088
+ const page = `<ui-preferences data-skin-tone="${skinTone.get().value}">…</ui-preferences>`
1089
+ ```
1090
+
1091
+ In the browser the same storage reads `document.cookie`.
1092
+
1093
+ #### Render the Storage Script Yourself
1094
+
1095
+ For one element that keeps its own value, like a toolbar's density, a storage's `script` returns the same kind of inline script for a single element. Write it right after the element, since it sets the element right before it:
1096
+
1097
+ ```ts title="server.ts"
1098
+ import { createLocalStorage } from "@ui-sdk/core"
1099
+
1100
+ const density = createLocalStorage("density")
1101
+ const script = density.script({
1102
+ attribute: "data-density",
1103
+ values: ["comfortable", "compact"],
1104
+ })
1105
+
1106
+ const toolbar = `<div class="toolbar" data-density="comfortable">…</div>
1107
+ <script>${script}</script>`
1108
+ ```
1109
+
1110
+ - **Shared by ID.** Every script with the same storage kind and ID reads the storage once, however many elements it follows, and the scripts are identical, so compression shrinks the repeats.
1111
+ - **Only safe values.** Any script on your site can write the saved value, so `script` only sets attributes that start with `data-`, and only to one of `values`.
1112
+ - **Not for cookies.** A cookie storage's `script` returns undefined, since your server reads the cookie and can write the value itself.
1113
+
1114
+ To run before the page's content at all, write `preferencesScript([...])` into `<head>`. Only global preferences make sense there, since the others go on the script's parent element.
1115
+
1116
+ ### Importing on the Server
1117
+
1118
+ Component modules skip registration when `customElements` is unavailable. Importing one on the server doesn't upgrade markup. Load it again in the browser.
1119
+
1120
+ Popups render in place, so your server template doesn't need a portal root. [Popups](./popups.md) covers placement, and [Forms](./forms.md) covers submission.
1121
+
1122
+ ## Rendering Primitives
1123
+
1124
+ This page renders the rating from [Compose Behaviors](./core.md#compose-behaviors). Start with that guide's [complete `rating.ts`](./core.md#complete-primitive): it already handles selection, arrow keys, and focus. Here we'll connect its `root` and `star` slots to custom elements.
1125
+
1126
+ ### Define the Elements
1127
+
1128
+ `createParts` registers one custom element for each slot you map. Save this beside `rating.ts`:
1129
+
1130
+ ```ts title="rating-web.ts"
1131
+ import { createParts, wrappedButton } from "@ui-sdk/web"
1132
+ import { rating } from "./rating"
1133
+
1134
+ createParts(rating, {
1135
+ root: "my-rating",
1136
+ star: { tag: "my-rating-star", target: wrappedButton, display: "contents" },
1137
+ })
1138
+ ```
1139
+
1140
+ The root creates the primitive instance. Each star finds that instance through the root above it. `wrappedButton` puts the star's behavior and attributes on a native button inside the custom element.
1141
+
1142
+ `createParts` also returns the element classes, named like the parts in other frameworks: `Root` and `Star`. Keep them with `const parts = createParts(rating, { ... })` to check an element, like `element instanceof parts.Star`. On the server nothing is registered, so they're `undefined`.
1143
+
1144
+ ### Render the Rating
1145
+
1146
+ Load `rating-web.ts` from your app's module entry, then use the registered tags:
1147
+
1148
+ ```html
1149
+ <my-rating default-value="4" aria-label="Rating">
1150
+ <my-rating-star value="1"
1151
+ ><button aria-label="1 star">★</button></my-rating-star
1152
+ >
1153
+ <my-rating-star value="2"
1154
+ ><button aria-label="2 stars">★</button></my-rating-star
1155
+ >
1156
+ <my-rating-star value="3"
1157
+ ><button aria-label="3 stars">★</button></my-rating-star
1158
+ >
1159
+ <my-rating-star value="4"
1160
+ ><button aria-label="4 stars">★</button></my-rating-star
1161
+ >
1162
+ <my-rating-star value="5"
1163
+ ><button aria-label="5 stars">★</button></my-rating-star
1164
+ >
1165
+ </my-rating>
1166
+ ```
1167
+
1168
+ The primitive's schema parses `default-value` as a number and each star's `value` as a string. Put the star's accessible label and styles on its button. The button receives `data-filled` when it contributes to the rating.
1169
+
1170
+ [Styling and Animation](./styling-and-animation.md) covers styling those attributes. The native buttons remain in the HTML before the elements upgrade; their rating behavior starts when the module loads.
1171
+
1172
+ ### Listen to Changes
1173
+
1174
+ The primitive's `onValueChange` callback becomes a `valuechange` event on the root:
1175
+
1176
+ ```ts
1177
+ import "./rating-web"
1178
+
1179
+ const element = document.querySelector("my-rating")!
1180
+ element.addEventListener("valuechange", (event) => {
1181
+ const { value } = (event as CustomEvent<{ value: number }>).detail
1182
+ console.log(`Rating: ${value}`)
1183
+ })
1184
+ ```
1185
+
1186
+ Call `event.preventDefault()` to cancel a change. These events don't bubble, so listen on the root itself. [State and Events](./state-and-events.md) explains the details and controlled values.
1187
+
1188
+ ### Add Logic to a Part
1189
+
1190
+ [Custom Parts](./custom-parts.md) builds on these same elements: choose a part's binding target, and expose API methods or live properties.
1191
+
1192
+ ## Custom Parts
1193
+
1194
+ Start with the rating elements from [Rendering Primitives](./rendering.md). Add configuration to `createParts` when you need a different target or a public API.
1195
+
1196
+ ### Choose the Target
1197
+
1198
+ By default, a part binds to its custom element. `target` chooses a child instead. `wrappedButton` finds a button or link; `wrappedChild("input")` finds an input.
1199
+
1200
+ Bind to the native element that should own focus and keyboard behavior. Put its label and classes on that element too.
1201
+
1202
+ ### Expose API Methods
1203
+
1204
+ The rating primitive has a `rate` method. Add it to the root's `api` list to let application code call it:
1205
+
1206
+ ```ts title="rating-web.ts"
1207
+ import { createParts, wrappedButton } from "@ui-sdk/web"
1208
+ import { rating } from "./rating"
1209
+
1210
+ createParts(rating, {
1211
+ root: { tag: "my-rating", api: ["rate"] },
1212
+ star: { tag: "my-rating-star", target: wrappedButton, display: "contents" },
1213
+ })
1214
+ ```
1215
+
1216
+ Use this definition instead of the earlier one. Once the root is connected, `element.api.rate(3)` asks the primitive to change its value. Its callback and cancellation still apply. The element exposes only the methods you list.
1217
+
1218
+ ### Expose a Live Property
1219
+
1220
+ A `state` mapping exposes a live getter and setter. Both call the primitive's API, so the primitive needs methods for reading and writing that value. The rating above only has `rate`; the built-in toggle has both `isPressed` and `setPressed`:
1221
+
1222
+ ```ts
1223
+ import { toggle } from "@ui-sdk/core/toggle"
1224
+ import { createParts, wrappedButton } from "@ui-sdk/web"
1225
+
1226
+ createParts(toggle, {
1227
+ root: {
1228
+ tag: "my-toggle",
1229
+ target: wrappedButton,
1230
+ display: "contents",
1231
+ state: {
1232
+ pressed: {
1233
+ get: (api) => api.isPressed(),
1234
+ set: (api, value: boolean) => api.setPressed(value),
1235
+ },
1236
+ },
1237
+ api: ["isPressed", "setPressed", "toggle"],
1238
+ },
1239
+ })
1240
+ ```
1241
+
1242
+ ```html
1243
+ <my-toggle><button>Bold</button></my-toggle>
1244
+ ```
1245
+
1246
+ The setter calls the primitive's API, so change callbacks and cancellation still apply. The `api` list exposes those methods on the element's controller.
1247
+
1248
+ ### Attributes and Properties
1249
+
1250
+ The primitive's schema supplies the default conversion:
1251
+
1252
+ | Kind | How It Reaches the Primitive |
1253
+ | ----------------------------- | -------------------------------------------------------------- |
1254
+ | `string`, `number`, `boolean` | Parsed from an attribute. |
1255
+ | `list` | A comma-separated attribute. |
1256
+ | `value` | A JavaScript property, for objects and other rich values. |
1257
+ | `callback` | A DOM event. |
1258
+ | `action` | A DOM event with `detail.waitUntil(promise)` for pending work. |
1259
+
1260
+ A boolean attribute is on when present, unless its value is `"false"`. Defaults determine the value when the attribute is absent. `disabled` and `required` follow native controls, which treat them as on whenever present.
1261
+
1262
+ Each attribute gets a property that reflects it. Name the booleans that are on by default in `defaults`, so their properties read `true` without the attribute and `false` writes `"false"`:
1263
+
1264
+ ```ts
1265
+ import { tabs } from "@ui-sdk/core/tabs"
1266
+ import { createParts, wrappedButton } from "@ui-sdk/web"
1267
+
1268
+ createParts(tabs, {
1269
+ root: { tag: "my-tabs", defaults: { loop: true } },
1270
+ list: "my-tabs-list",
1271
+ tab: { tag: "my-tabs-tab", target: wrappedButton },
1272
+ panel: "my-tabs-panel",
1273
+ })
1274
+ ```
1275
+
1276
+ Use `kinds` to override a schema kind for an element and `properties` to add properties absent from the schema. `context(el)` on a child part can supply slot props from surrounding markup; explicit attributes and properties take precedence.
1277
+
1278
+ ### Compose a Popup
1279
+
1280
+ A custom root can contain `ui-popover` or `ui-dialog`. Keep its `open` property in sync with your primitive and listen for `openchange`. That event doesn't bubble, so a listener on your root hears only the change you forward. [Compose Ours](./core.md#compose-ours) shows the pattern.
1281
+
1282
+ ## Components
1283
+
1284
+ Each page has the component's composition, examples, styling hooks and every prop.
1285
+
1286
+ - [Accordion](./components/accordion.md): Show and hide sections of content from their headings. Defined by `import "@ui-sdk/web/accordion"`.
1287
+ - [Alert Dialog](./components/alert-dialog.md): Ask someone to confirm an action before you continue. Defined by `import "@ui-sdk/web/alert-dialog"`.
1288
+ - [Autocomplete](./components/autocomplete.md): Suggest text while someone types, without limiting what they can enter. Defined by `import "@ui-sdk/web/autocomplete"`.
1289
+ - [Avatar](./components/avatar.md): Show an image with a fallback while it loads or if it fails. Defined by `import "@ui-sdk/web/avatar"`.
1290
+ - [Button](./components/button.md): Run an action, show that it's working, and keep it reachable when disabled. Defined by `import "@ui-sdk/web/button"`.
1291
+ - [Calendar](./components/calendar.md): Choose one date, several dates, or a range from a month grid. Defined by `import "@ui-sdk/web/calendar"`.
1292
+ - [Carousel](./components/carousel.md): Page through slides with native scrolling, keyboard controls, and optional rotation. Defined by `import "@ui-sdk/web/carousel"`.
1293
+ - [Checkbox](./components/checkbox.md): Choose one or more options, individually or as a group. Defined by `import "@ui-sdk/web/checkbox"`.
1294
+ - [Collapsible](./components/collapsible.md): Show and hide a panel, preserve its contents, and animate its height. Defined by `import "@ui-sdk/web/collapsible"`.
1295
+ - [Color Picker](./components/color-picker.md): Choose a color by dragging across an area and along sliders, typing any CSS color, or picking a preset. Defined by `import "@ui-sdk/web/color-picker"`.
1296
+ - [Combobox](./components/combobox.md): Search a list of options and choose one or several values. Defined by `import "@ui-sdk/web/combobox"`.
1297
+ - [Command](./components/command.md): Search commands, run actions, and open pages of more choices. Defined by `import "@ui-sdk/web/command"`.
1298
+ - [Date Input](./components/date-input.md): Type a date one part at a time, in the order your user's language writes it. Defined by `import "@ui-sdk/web/date-input"`.
1299
+ - [Date Picker](./components/date-picker.md): Type a date or choose one from a calendar, with support for ranges and forms. Defined by `import "@ui-sdk/web/date-picker"`.
1300
+ - [Dialog](./components/dialog.md): Open a focused task over the page. Defined by `import "@ui-sdk/web/dialog"`.
1301
+ - [Drawer](./components/drawer.md): A sheet you can open, drag, and resize from an edge of the screen. Defined by `import "@ui-sdk/web/drawer"`.
1302
+ - [Field](./components/field.md): Connect a form control to its label, description, and validation errors. Defined by `import "@ui-sdk/web/field"`.
1303
+ - [File Input](./components/file-input.md): Choose, drop, or paste files, validate them, and submit them with a form. Defined by `import "@ui-sdk/web/file-input"`.
1304
+ - [Hover Card](./components/hover-card.md): Preview a link on hover or keyboard focus while keeping its normal navigation. Defined by `import "@ui-sdk/web/hover-card"`.
1305
+ - [Image Cropper](./components/image-cropper.md): Choose the part of an image to keep by moving and resizing a selection, or by panning and zooming the image under it. Defined by `import "@ui-sdk/web/image-cropper"`.
1306
+ - [Input](./components/input.md): Enter text, add a clear button, and connect the value to your form or state. Defined by `import "@ui-sdk/web/input"`.
1307
+ - [Label](./components/label.md): Name any control with a label, and let a press on the label reach it. Defined by `import "@ui-sdk/web/label"`.
1308
+ - [Listbox](./components/listbox.md): Choose one or several options from an always-visible list. Defined by `import "@ui-sdk/web/listbox"`.
1309
+ - [Menu](./components/menu.md): Choose actions and settings from a button or a context menu. Defined by `import "@ui-sdk/web/menu"`.
1310
+ - [Menubar](./components/menubar.md): Move between an application’s menus with one tab stop. Defined by `import "@ui-sdk/web/menubar"`.
1311
+ - [Meter](./components/meter.md): Show a measurement within a known range, such as storage used or an account quota. Defined by `import "@ui-sdk/web/meter"`.
1312
+ - [Navigation Menu](./components/navigation-menu.md): Groups site links into panels that open from a row of triggers. Defined by `import "@ui-sdk/web/navigation-menu"`.
1313
+ - [Number Input](./components/number-input.md): Enter numbers, step through values, and format them for a locale. Defined by `import "@ui-sdk/web/number-input"`.
1314
+ - [OTP Input](./components/otp-input.md): Enter a one-time code with support for pasting, autofill, and native form validation. Defined by `import "@ui-sdk/web/otp-input"`.
1315
+ - [Pagination](./components/pagination.md): Navigate pages with buttons, links, or your router. Defined by `import "@ui-sdk/web/pagination"`.
1316
+ - [Password Input](./components/password-input.md): Enter a password, show or hide it, and use it in a native form. Defined by `import "@ui-sdk/web/password-input"`.
1317
+ - [Popover](./components/popover.md): Edit or explore something beside its trigger. Defined by `import "@ui-sdk/web/popover"`.
1318
+ - [Preferences](./components/preferences.md): Apply user preferences to data attributes so your styles can follow them. Defined by `import "@ui-sdk/web/preferences"`.
1319
+ - [Progress](./components/progress.md): Show a task’s progress, or an indeterminate state while its progress is unknown. Defined by `import "@ui-sdk/web/progress"`.
1320
+ - [Radio](./components/radio.md): Choose one option, move the selection with arrow keys, and submit it with a form. Defined by `import "@ui-sdk/web/radio"`.
1321
+ - [Rating](./components/rating.md): Pick a score from a row of stars, whole or half, with a preview as the pointer moves over them. Defined by `import "@ui-sdk/web/rating"`.
1322
+ - [Resizable](./components/resizable.md): Resize, collapse, and save panel layouts with pointer and keyboard controls. Defined by `import "@ui-sdk/web/resizable"`.
1323
+ - [Scroll Area](./components/scroll-area.md): Add styled scrollbars, edge indicators, and follow-to-end behavior to native scrolling. Defined by `import "@ui-sdk/web/scroll-area"`.
1324
+ - [Segmented Control](./components/segmented-control.md): Pick one of a few options from a row of buttons, with an indicator that slides to the choice. Defined by `import "@ui-sdk/web/segmented-control"`.
1325
+ - [Select](./components/select.md): Choose one value or several from a popup list. Defined by `import "@ui-sdk/web/select"`.
1326
+ - [Separator](./components/separator.md): Divide groups of content with a horizontal or vertical line. Defined by `import "@ui-sdk/web/separator"`.
1327
+ - [Slider](./components/slider.md): Adjust a number or a range with draggable thumbs and keyboard controls. Defined by `import "@ui-sdk/web/slider"`.
1328
+ - [Switch](./components/switch.md): Turn a setting on or off. Defined by `import "@ui-sdk/web/switch"`.
1329
+ - [Tabs](./components/tabs.md): Switch between related panels with one tab selected at a time. Defined by `import "@ui-sdk/web/tabs"`.
1330
+ - [Textarea](./components/textarea.md): Enter multiple lines of text, grow the field as you type, and connect it to a form. Defined by `import "@ui-sdk/web/textarea"`.
1331
+ - [Time Input](./components/time-input.md): Type a time one part at a time, on the clock your user's language uses.
1332
+ - [Timeline](./components/timeline.md): Show dated events in order, like an activity feed, a changelog or an order's progress, with arrow keys between them. Defined by `import "@ui-sdk/web/timeline"`.
1333
+ - [Toast](./components/toast.md): Show notifications with actions, timed dismissal, and screen reader announcements. Defined by `import "@ui-sdk/web/toast"`.
1334
+ - [Toggle](./components/toggle.md): Keep a button pressed, or group toggles with a shared value and arrow-key navigation. Defined by `import "@ui-sdk/web/toggle"`.
1335
+ - [Token Input](./components/token-input.md): Mix editable text with tokens for files, people, tags, and other references. Defined by `import "@ui-sdk/web/token-input"`.
1336
+ - [Toolbar](./components/toolbar.md): Give buttons, toggles, menus and fields one tab stop with arrow-key navigation. Defined by `import "@ui-sdk/web/toolbar"`.
1337
+ - [Tooltip](./components/tooltip.md): Name a control or add a short hint on hover and keyboard focus. Defined by `import "@ui-sdk/web/tooltip"`.
1338
+ - [Tree](./components/tree.md): Browse a hierarchy of files, pages, or choices with expandable branches. Defined by `import "@ui-sdk/web/tree"`.
1339
+ - [Trigger Menu](./components/trigger-menu.md): "Opens suggestions at the caret when you type a trigger such as @, /, or #." Defined by `import "@ui-sdk/web/trigger-menu"`.
1340
+ - [Waterfall](./components/waterfall.md): Show a trace or a page's requests as bars on a shared time axis, with spans that expand to show what they started. Defined by `import "@ui-sdk/web/waterfall"`.
1341
+
1342
+ ## Utilities
1343
+
1344
+ ### Async Options
1345
+
1346
+ `createAsyncOptions` loads items as someone types. It waits for typing to pause, cancels requests that are replaced, and keeps the previous results visible while loading.
1347
+
1348
+ Change the movie search and wait for the results. Use the arrow keys to choose a movie, then press Enter.
1349
+
1350
+ #### Load Results
1351
+
1352
+ Write the combobox, then connect its query events to a loader. This example expects `/api/movies` to return an array of movie titles:
1353
+
1354
+ ```html
1355
+ <ui-combobox filter="false">
1356
+ <ui-combobox-input
1357
+ ><input aria-label="Movie" placeholder="Search movies…"
1358
+ /></ui-combobox-input>
1359
+ <ui-combobox-content>
1360
+ <ui-combobox-list></ui-combobox-list>
1361
+ </ui-combobox-content>
1362
+ </ui-combobox>
1363
+ ```
1364
+
1365
+ ```ts
1366
+ import "@ui-sdk/web/combobox"
1367
+ import { createAsyncOptions } from "@ui-sdk/core/async-options"
1368
+ import type { UiComboboxElement } from "@ui-sdk/web"
1369
+
1370
+ function connectMovies(combobox: UiComboboxElement) {
1371
+ const movies = createAsyncOptions<string>(async (query, { signal }) => {
1372
+ const response = await fetch(`/api/movies?q=${encodeURIComponent(query)}`, {
1373
+ signal,
1374
+ })
1375
+ if (!response.ok) throw new Error("Couldn't load movies")
1376
+ return response.json()
1377
+ })
1378
+ function update() {
1379
+ const { items, status } = movies.getState()
1380
+ combobox.items = items
1381
+ combobox.status = status
1382
+ }
1383
+ const unsubscribe = movies.subscribe(update)
1384
+ update()
1385
+ const events = new AbortController()
1386
+ combobox.addEventListener(
1387
+ "querychange",
1388
+ (event) => movies.setQuery(event.detail.value),
1389
+ {
1390
+ signal: events.signal,
1391
+ }
1392
+ )
1393
+ combobox.addEventListener("loadmore", () => movies.loadMore(), {
1394
+ signal: events.signal,
1395
+ })
1396
+
1397
+ return () => {
1398
+ events.abort()
1399
+ unsubscribe()
1400
+ movies.cancel()
1401
+ }
1402
+ }
1403
+
1404
+ const disconnect = connectMovies(document.querySelector("ui-combobox")!)
1405
+ ```
1406
+
1407
+ Keep `disconnect` and call it when removing this view. It removes listeners, unsubscribes from state, and cancels pending work.
1408
+
1409
+ Pass `signal` to `fetch` so a newer search can cancel the request. `filter="false"` leaves matching and result order to the server. Set `items` and `status` as properties, as shown above.
1410
+
1411
+ Use the component's status and empty parts to show loading, error, and empty messages, as in the demo. The same pattern works with [Select](./components/select.md#async-search), [Autocomplete](./components/autocomplete.md#async-suggestions), and [Command](./components/command.md#async-results).
1412
+
1413
+ #### Choose When to Load
1414
+
1415
+ The helper's second argument sets its starting behavior:
1416
+
1417
+ | Option | What It Does | Default |
1418
+ | ----------- | ------------------------------------------------------- | ------- |
1419
+ | `debounce` | Milliseconds typing must pause before a request starts. | `200` |
1420
+ | `query` | The starting search text. | `""` |
1421
+ | `immediate` | Loads that text before anyone types. | `false` |
1422
+
1423
+ Add `{ immediate: true, debounce: 300 }` after the loader to load initial choices. If you set `query`, use the same text for the component's `default-query`.
1424
+
1425
+ #### Load More
1426
+
1427
+ For paged results, return `{ items, hasMore }`. The page number starts at `0`:
1428
+
1429
+ ```ts
1430
+ import type { AsyncOptionsContext } from "@ui-sdk/core/async-options"
1431
+
1432
+ async function loadMoviePages(
1433
+ query: string,
1434
+ { signal, page }: AsyncOptionsContext
1435
+ ) {
1436
+ const response = await fetch(
1437
+ `/api/movies?q=${encodeURIComponent(query)}&page=${page}`,
1438
+ { signal }
1439
+ )
1440
+ if (!response.ok) throw new Error("Couldn't load movies")
1441
+ const data: { titles: string[]; next: number | null } = await response.json()
1442
+ return { items: data.titles, hasMore: data.next !== null }
1443
+ }
1444
+
1445
+ export { loadMoviePages }
1446
+ ```
1447
+
1448
+ Pass `loadMoviePages` to `createAsyncOptions` in place of the inline loader above.
1449
+
1450
+ The `loadmore` listener above asks for another page when scrolling or keyboard navigation reaches the last item. The helper appends the new items.
1451
+
1452
+ Open the movie list and scroll down, or use Arrow Down to reach the last result. The next page is added to the list.
1453
+
1454
+ #### Retry a Search
1455
+
1456
+ `movies.getState()` holds `items`, `status`, `error`, and `hasMore`. Status is `"loading"` during a request, `"error"` after a failure, and `"idle"` otherwise.
1457
+
1458
+ Call `movies.reload()` from a retry button to load the current query again, starting at page `0`. Previous results stay available while it loads.
1459
+
1460
+ ### Shortcuts
1461
+
1462
+ `registerShortcut` connects keys to an action. For an element that already accepts `shortcut`, such as a dialog trigger, use that attribute directly.
1463
+
1464
+ Press N to create a note. Press Command+S on Mac or Control+S elsewhere to save. The buttons run the same actions.
1465
+
1466
+ #### Register a Shortcut
1467
+
1468
+ Keys are joined with `+`. `mod` means Command on Apple platforms and Control elsewhere:
1469
+
1470
+ ```html
1471
+ <button id="new-note">New note <kbd aria-hidden="true"></kbd></button>
1472
+ <p id="count" aria-live="polite">0 notes</p>
1473
+ ```
1474
+
1475
+ ```ts
1476
+ import { ariaKeyshortcuts, formatShortcut, registerShortcut } from "@ui-sdk/web"
1477
+
1478
+ const button = document.querySelector<HTMLButtonElement>("#new-note")!
1479
+ const count = document.querySelector("#count")!
1480
+ let total = 0
1481
+ function addNote() {
1482
+ total++
1483
+ count.textContent = `${total} notes`
1484
+ }
1485
+
1486
+ const unregister = registerShortcut("mod+enter", addNote, {
1487
+ description: "New note",
1488
+ })
1489
+ button.addEventListener("click", addNote)
1490
+ button.setAttribute("aria-keyshortcuts", ariaKeyshortcuts("mod+enter"))
1491
+ button.querySelector("kbd")!.textContent = formatShortcut("mod+enter")
1492
+
1493
+ function disconnect() {
1494
+ unregister()
1495
+ button.removeEventListener("click", addNote)
1496
+ }
1497
+ ```
1498
+
1499
+ Call `disconnect()` when removing this view. `registerShortcut` returns the function that unregisters it.
1500
+
1501
+ #### Show the Keys
1502
+
1503
+ `formatShortcut` produces display text for the current platform. `ariaKeyshortcuts` produces the matching `aria-keyshortcuts` value. The example hides the visual hint from screen readers because the button already exposes the shortcut.
1504
+
1505
+ Format browser-specific hints after the page loads. Compare the original keys, not the formatted display text.
1506
+
1507
+ #### While Someone Is Typing
1508
+
1509
+ Shortcuts with Command, Control, or Option can run in text fields. Plain letters and other keys stay available for typing. Set `allowInInput: true` when a shortcut should work there too, such as Escape to clear a search.
1510
+
1511
+ #### Limit a Shortcut to an Area
1512
+
1513
+ A named `scope` runs only while it is enabled. This connection turns it on when the textarea has focus and clears it on cleanup:
1514
+
1515
+ ```html
1516
+ <textarea id="note" aria-label="Note"></textarea>
1517
+ ```
1518
+
1519
+ ```ts
1520
+ import { registerShortcut, shortcuts } from "@ui-sdk/web"
1521
+
1522
+ function connectNote(note: HTMLTextAreaElement) {
1523
+ const unregister = registerShortcut(
1524
+ "escape",
1525
+ () => {
1526
+ note.value = ""
1527
+ },
1528
+ {
1529
+ scope: "note",
1530
+ allowInInput: true,
1531
+ }
1532
+ )
1533
+ const events = new AbortController()
1534
+ note.addEventListener("focus", () => shortcuts.setScope("note", true), {
1535
+ signal: events.signal,
1536
+ })
1537
+ note.addEventListener("blur", () => shortcuts.setScope("note", false), {
1538
+ signal: events.signal,
1539
+ })
1540
+ shortcuts.setScope("note", document.activeElement === note)
1541
+
1542
+ return () => {
1543
+ events.abort()
1544
+ shortcuts.setScope("note", false)
1545
+ unregister()
1546
+ }
1547
+ }
1548
+
1549
+ const disconnect = connectNote(
1550
+ document.querySelector<HTMLTextAreaElement>("#note")!
1551
+ )
1552
+ ```
1553
+
1554
+ Call `disconnect()` when removing the note. Scope names are shared, so choose one for each area you want to enable together. A `when` function can add a condition checked on each key press.
1555
+
1556
+ Focus the note and type some text. Press Command+Enter on Mac or Control+Enter elsewhere to save, or Escape to clear it. Move focus outside the note to turn off its shortcuts.
1557
+
1558
+ #### Inside a Popup
1559
+
1560
+ Page shortcuts pause while a modal is open. Pass `layer`, an element inside a popup or a function that returns one, to make a shortcut belong to that popup. It runs only while the popup is open, even though its closed content stays in the DOM.
1561
+
1562
+ When shortcuts share the same keys, the topmost open layer wins, then the most recently registered shortcut.
1563
+
1564
+ #### List Available Shortcuts
1565
+
1566
+ Call `shortcuts.list()` when a help screen opens. It returns the shortcuts that can run now, including their `keys`, formatted `hint`, and `description`. Add descriptions when registering shortcuts so the help screen can explain each action.