@threadlabs/looma 0.14.8 → 0.15.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (336) hide show
  1. package/components/shared/icons.js +5 -0
  2. package/components/shared/menu-navigation.js +49 -0
  3. package/components/shared/overlay.js +33 -60
  4. package/components/ui-avatar/ui-avatar.html +19 -2
  5. package/components/ui-avatar/ui-avatar.js +7 -1
  6. package/components/ui-avatar-group/ui-avatar-group.html +8 -3
  7. package/components/ui-avatar-group/ui-avatar-group.js +6 -2
  8. package/components/ui-badge/ui-badge.html +70 -4
  9. package/components/ui-button/ui-button.html +21 -5
  10. package/components/ui-button/ui-button.js +10 -0
  11. package/components/ui-callout/ui-callout.html +13 -5
  12. package/components/ui-checkbox/ui-checkbox.html +11 -0
  13. package/components/ui-combobox/ui-combobox.html +46 -10
  14. package/components/ui-combobox/ui-combobox.js +5 -5
  15. package/components/ui-context-menu/ui-context-menu.html +33 -5
  16. package/components/ui-context-menu/ui-context-menu.js +26 -17
  17. package/components/ui-dialog/ui-dialog.html +51 -8
  18. package/components/ui-dialog/ui-dialog.js +28 -7
  19. package/components/ui-disclosure/ui-disclosure.html +14 -5
  20. package/components/ui-disclosure/ui-disclosure.js +51 -4
  21. package/components/ui-editable/ui-editable.html +17 -3
  22. package/components/ui-editable/ui-editable.js +7 -0
  23. package/components/ui-editor-insert-table-grid/ui-editor-insert-table-grid.html +48 -14
  24. package/components/ui-editor-insert-table-grid/ui-editor-insert-table-grid.js +40 -6
  25. package/components/ui-editor-mention-menu/ui-editor-mention-menu.html +22 -9
  26. package/components/ui-editor-mention-menu/ui-editor-mention-menu.js +33 -11
  27. package/components/ui-editor-mention-menu-item/ui-editor-mention-menu-item.html +67 -0
  28. package/components/ui-editor-slash-menu/ui-editor-slash-menu.html +33 -30
  29. package/components/ui-editor-slash-menu/ui-editor-slash-menu.js +42 -8
  30. package/components/ui-editor-slash-menu-group/ui-editor-slash-menu-group.html +31 -0
  31. package/components/ui-editor-slash-menu-item/ui-editor-slash-menu-item.html +75 -0
  32. package/components/ui-editor-table-context-menu/ui-editor-table-context-menu.html +16 -5
  33. package/components/ui-editor-table-context-menu/ui-editor-table-context-menu.js +10 -2
  34. package/components/ui-editor-table-overlay/ui-editor-table-overlay.html +5 -5
  35. package/components/ui-editor-table-overlay/ui-editor-table-overlay.js +5 -2
  36. package/components/ui-editor-table-toolbar/ui-editor-table-toolbar.html +14 -4
  37. package/components/ui-editor-table-toolbar/ui-editor-table-toolbar.js +2 -2
  38. package/components/ui-editor-toolbar/ui-editor-toolbar.html +1 -1
  39. package/components/ui-editor-toolbar/ui-editor-toolbar.js +46 -0
  40. package/components/ui-floating-action-button/ui-floating-action-button.html +1 -1
  41. package/components/ui-form-field/ui-form-field.html +16 -3
  42. package/components/ui-form-field/ui-form-field.js +1 -1
  43. package/components/ui-grid/ui-grid.html +17 -2
  44. package/components/ui-icon/ui-icon.html +5 -0
  45. package/components/ui-icon-button/ui-icon-button.html +25 -6
  46. package/components/ui-icon-button/ui-icon-button.js +11 -1
  47. package/components/ui-input/ui-input.html +3 -3
  48. package/components/ui-input-group/ui-input-group.html +18 -4
  49. package/components/ui-listbox/ui-listbox.html +81 -0
  50. package/components/ui-listbox/ui-listbox.js +27 -0
  51. package/components/ui-menu/ui-menu.html +33 -5
  52. package/components/ui-menu/ui-menu.js +18 -12
  53. package/components/ui-menu-group/ui-menu-group.html +27 -0
  54. package/components/ui-menu-item/ui-menu-item.html +80 -8
  55. package/components/ui-menu-item/ui-menu-item.js +47 -0
  56. package/components/ui-popover/ui-popover.html +33 -5
  57. package/components/ui-popover/ui-popover.js +9 -1
  58. package/components/ui-radio-group/ui-radio-group.html +13 -1
  59. package/components/ui-search-shell/ui-search-shell.html +6 -6
  60. package/components/ui-search-shell/ui-search-shell.js +21 -5
  61. package/components/ui-select/ui-select.html +14 -4
  62. package/components/ui-switch/ui-switch.html +2 -2
  63. package/components/ui-switcher/ui-switcher.html +8 -1
  64. package/components/ui-switcher/ui-switcher.js +15 -0
  65. package/components/ui-tabs/ui-tabs.html +17 -12
  66. package/components/ui-tabs/ui-tabs.js +43 -7
  67. package/components/ui-textarea/ui-textarea.html +28 -5
  68. package/components/ui-toast/ui-toast.html +88 -0
  69. package/components/ui-toast/ui-toast.js +68 -0
  70. package/components/ui-toast-region/ui-toast-region.html +102 -19
  71. package/components/ui-toast-region/ui-toast-region.js +10 -18
  72. package/components/ui-tooltip/ui-tooltip.html +51 -7
  73. package/components/ui-tooltip/ui-tooltip.js +29 -3
  74. package/components/ui-tree/ui-tree.html +12 -0
  75. package/components/ui-tree/ui-tree.js +191 -12
  76. package/components/ui-tree-item/ui-tree-item.html +87 -8
  77. package/components/ui-tree-item/ui-tree-item.js +3 -1
  78. package/dist/index.js +38 -32
  79. package/editor/chunks/extensions.js +82 -2
  80. package/editor/extensions/table-commands.d.ts +8 -0
  81. package/editor/icons.d.ts +5 -0
  82. package/editor/index.js +7 -2
  83. package/editor/insert-table-grid.d.ts +1 -1
  84. package/editor/mention-menu.d.ts +2 -0
  85. package/editor/slash-menu.d.ts +3 -0
  86. package/editor/table-context-menu.d.ts +1 -1
  87. package/editor/table-overlay.d.ts +2 -2
  88. package/package.json +1 -1
  89. package/styles/ui-avatar-group.css +1 -2
  90. package/styles/ui-avatar.css +11 -1
  91. package/styles/ui-badge.css +73 -1
  92. package/styles/ui-button.css +9 -1
  93. package/styles/ui-callout.css +1 -1
  94. package/styles/ui-checkbox.css +9 -0
  95. package/styles/ui-combobox.css +38 -7
  96. package/styles/ui-context-menu.css +32 -4
  97. package/styles/ui-dialog.css +42 -5
  98. package/styles/ui-editor-insert-table-grid.css +48 -11
  99. package/styles/ui-editor-mention-menu-item.css +57 -0
  100. package/styles/ui-editor-mention-menu.css +11 -1
  101. package/styles/ui-editor-slash-menu-group.css +21 -0
  102. package/styles/ui-editor-slash-menu-item.css +62 -0
  103. package/styles/ui-editor-slash-menu.css +10 -21
  104. package/styles/ui-editor-table-context-menu.css +1 -1
  105. package/styles/ui-floating-action-button.css +1 -1
  106. package/styles/ui-form-field.css +14 -1
  107. package/styles/ui-grid.css +14 -1
  108. package/styles/ui-icon-button.css +17 -5
  109. package/styles/ui-input-group.css +18 -4
  110. package/styles/ui-input.css +3 -3
  111. package/styles/ui-listbox.css +69 -0
  112. package/styles/ui-menu-group.css +19 -0
  113. package/styles/ui-menu-item.css +48 -9
  114. package/styles/ui-menu.css +32 -4
  115. package/styles/ui-popover.css +28 -4
  116. package/styles/ui-radio-group.css +9 -0
  117. package/styles/ui-search-shell.css +5 -5
  118. package/styles/ui-select.css +14 -4
  119. package/styles/ui-switch.css +2 -2
  120. package/styles/ui-switcher.css +4 -0
  121. package/styles/ui-textarea.css +27 -4
  122. package/styles/ui-toast-region.css +88 -4
  123. package/styles/ui-toast.css +74 -0
  124. package/styles/ui-tooltip.css +46 -6
  125. package/styles/ui-tree-item.css +72 -4
  126. package/styles/ui-tree.css +8 -0
  127. package/tokens.css +3 -0
  128. package/vanilla/UiAvatar.d.ts +2 -0
  129. package/vanilla/UiAvatar.js +8 -1
  130. package/vanilla/UiAvatarGroup.d.ts +2 -0
  131. package/vanilla/UiAvatarGroup.js +1 -1
  132. package/vanilla/UiBadge.d.ts +1 -1
  133. package/vanilla/UiBadge.js +1 -1
  134. package/vanilla/UiButton.d.ts +1 -0
  135. package/vanilla/UiButton.js +22 -6
  136. package/vanilla/UiCallout.d.ts +1 -1
  137. package/vanilla/UiCallout.js +1 -1
  138. package/vanilla/UiCheckbox.d.ts +1 -0
  139. package/vanilla/UiCheckbox.js +12 -9
  140. package/vanilla/UiCombobox.d.ts +3 -0
  141. package/vanilla/UiCombobox.js +30 -15
  142. package/vanilla/UiContextMenu.d.ts +1 -1
  143. package/vanilla/UiContextMenu.js +1 -1
  144. package/vanilla/UiDialog.d.ts +5 -2
  145. package/vanilla/UiDialog.js +1 -1
  146. package/vanilla/UiDisclosure.d.ts +2 -0
  147. package/vanilla/UiDisclosure.js +32 -18
  148. package/vanilla/UiEditable.d.ts +5 -0
  149. package/vanilla/UiEditable.js +7 -3
  150. package/vanilla/UiEditorInsertTableGrid.js +1 -6
  151. package/vanilla/UiEditorMentionMenu.d.ts +4 -2
  152. package/vanilla/UiEditorMentionMenu.js +49 -31
  153. package/vanilla/UiEditorMentionMenuItem.d.ts +16 -0
  154. package/vanilla/UiEditorMentionMenuItem.js +56 -0
  155. package/vanilla/UiEditorSlashMenu.d.ts +4 -3
  156. package/vanilla/UiEditorSlashMenu.js +59 -46
  157. package/vanilla/UiEditorSlashMenuGroup.d.ts +14 -0
  158. package/vanilla/UiEditorSlashMenuGroup.js +48 -0
  159. package/vanilla/UiEditorSlashMenuItem.d.ts +18 -0
  160. package/vanilla/UiEditorSlashMenuItem.js +57 -0
  161. package/vanilla/UiEditorTableContextMenu.d.ts +5 -2
  162. package/vanilla/UiEditorTableContextMenu.js +1 -2
  163. package/vanilla/UiEditorTableOverlay.d.ts +1 -1
  164. package/vanilla/UiEditorTableOverlay.js +5 -5
  165. package/vanilla/UiEditorTableToolbar.d.ts +4 -2
  166. package/vanilla/UiEditorTableToolbar.js +1 -2
  167. package/vanilla/UiEditorToolbar.js +2 -0
  168. package/vanilla/UiGrid.d.ts +1 -0
  169. package/vanilla/UiGrid.js +8 -7
  170. package/vanilla/UiIcon.js +1 -1
  171. package/vanilla/UiIconButton.d.ts +1 -0
  172. package/vanilla/UiIconButton.js +11 -2
  173. package/vanilla/UiListbox.d.ts +20 -0
  174. package/vanilla/UiListbox.js +45 -0
  175. package/vanilla/UiMenu.d.ts +1 -1
  176. package/vanilla/UiMenu.js +1 -1
  177. package/vanilla/UiMenuGroup.d.ts +14 -0
  178. package/vanilla/UiMenuGroup.js +35 -0
  179. package/vanilla/UiMenuItem.d.ts +5 -1
  180. package/vanilla/UiMenuItem.js +91 -30
  181. package/vanilla/UiPopover.d.ts +1 -1
  182. package/vanilla/UiPopover.js +1 -1
  183. package/vanilla/UiRadioGroup.d.ts +1 -0
  184. package/vanilla/UiRadioGroup.js +6 -3
  185. package/vanilla/UiSwitcher.d.ts +1 -0
  186. package/vanilla/UiSwitcher.js +7 -9
  187. package/vanilla/UiTabs.d.ts +1 -0
  188. package/vanilla/UiTabs.js +9 -1
  189. package/vanilla/UiTextarea.d.ts +1 -0
  190. package/vanilla/UiTextarea.js +4 -1
  191. package/vanilla/UiToast.d.ts +16 -0
  192. package/vanilla/UiToast.js +53 -0
  193. package/vanilla/UiToastRegion.d.ts +1 -4
  194. package/vanilla/UiToastRegion.js +3 -11
  195. package/vanilla/UiTooltip.d.ts +1 -1
  196. package/vanilla/UiTooltip.js +1 -1
  197. package/vanilla/UiTree.d.ts +2 -0
  198. package/vanilla/UiTree.js +1 -1
  199. package/vanilla/UiTreeItem.d.ts +1 -0
  200. package/vanilla/UiTreeItem.js +45 -31
  201. package/vanilla/index.js +6 -0
  202. package/vue/UiAvatar.d.ts +4 -0
  203. package/vue/UiAvatar.vue +23 -3
  204. package/vue/UiAvatarGroup.d.ts +3 -0
  205. package/vue/UiAvatarGroup.vue +7 -4
  206. package/vue/UiBadge.d.ts +2 -2
  207. package/vue/UiBadge.vue +74 -2
  208. package/vue/UiButton.d.ts +5 -3
  209. package/vue/UiButton.vue +41 -7
  210. package/vue/UiCallout.d.ts +2 -2
  211. package/vue/UiCallout.vue +3 -2
  212. package/vue/UiCheckbox.d.ts +2 -0
  213. package/vue/UiCheckbox.vue +16 -0
  214. package/vue/UiCombobox.d.ts +11 -3
  215. package/vue/UiCombobox.vue +60 -11
  216. package/vue/UiContextMenu.d.ts +2 -0
  217. package/vue/UiContextMenu.vue +47 -6
  218. package/vue/UiDialog.d.ts +18 -4
  219. package/vue/UiDialog.vue +86 -11
  220. package/vue/UiDisclosure.d.ts +8 -2
  221. package/vue/UiDisclosure.vue +38 -14
  222. package/vue/UiEditable.d.ts +10 -0
  223. package/vue/UiEditable.vue +14 -2
  224. package/vue/UiEditorInsertTableGrid.vue +52 -15
  225. package/vue/UiEditorMentionMenu.d.ts +24 -2
  226. package/vue/UiEditorMentionMenu.vue +27 -11
  227. package/vue/UiEditorMentionMenuItem.d.ts +24 -0
  228. package/vue/UiEditorMentionMenuItem.js +2 -0
  229. package/vue/UiEditorMentionMenuItem.vue +88 -0
  230. package/vue/UiEditorSlashMenu.d.ts +24 -1
  231. package/vue/UiEditorSlashMenu.vue +33 -33
  232. package/vue/UiEditorSlashMenuGroup.d.ts +18 -0
  233. package/vue/UiEditorSlashMenuGroup.js +2 -0
  234. package/vue/UiEditorSlashMenuGroup.vue +43 -0
  235. package/vue/UiEditorSlashMenuItem.d.ts +26 -0
  236. package/vue/UiEditorSlashMenuItem.js +2 -0
  237. package/vue/UiEditorSlashMenuItem.vue +101 -0
  238. package/vue/UiEditorTableContextMenu.d.ts +9 -3
  239. package/vue/UiEditorTableContextMenu.vue +53 -3
  240. package/vue/UiEditorTableOverlay.d.ts +2 -2
  241. package/vue/UiEditorTableOverlay.vue +8 -6
  242. package/vue/UiEditorTableToolbar.d.ts +7 -3
  243. package/vue/UiEditorTableToolbar.vue +50 -2
  244. package/vue/UiEditorToolbar.vue +14 -1
  245. package/vue/UiFloatingActionButton.vue +1 -1
  246. package/vue/UiFormField.vue +28 -3
  247. package/vue/UiGrid.d.ts +1 -0
  248. package/vue/UiGrid.vue +26 -2
  249. package/vue/UiIcon.vue +16 -0
  250. package/vue/UiIconButton.d.ts +5 -3
  251. package/vue/UiIconButton.vue +40 -6
  252. package/vue/UiInput.vue +10 -3
  253. package/vue/UiInputGroup.vue +32 -11
  254. package/vue/UiListbox.d.ts +33 -0
  255. package/vue/UiListbox.js +2 -0
  256. package/vue/UiListbox.vue +131 -0
  257. package/vue/UiMenu.d.ts +2 -0
  258. package/vue/UiMenu.vue +43 -6
  259. package/vue/UiMenuGroup.d.ts +18 -0
  260. package/vue/UiMenuGroup.js +2 -0
  261. package/vue/UiMenuGroup.vue +39 -0
  262. package/vue/UiMenuItem.d.ts +34 -2
  263. package/vue/UiMenuItem.vue +145 -9
  264. package/vue/UiPopover.d.ts +2 -2
  265. package/vue/UiPopover.vue +44 -5
  266. package/vue/UiRadioGroup.d.ts +2 -0
  267. package/vue/UiRadioGroup.vue +17 -0
  268. package/vue/UiSearchShell.vue +12 -5
  269. package/vue/UiSelect.vue +18 -4
  270. package/vue/UiSwitch.vue +2 -2
  271. package/vue/UiSwitcher.d.ts +1 -0
  272. package/vue/UiSwitcher.vue +22 -1
  273. package/vue/UiTabs.d.ts +6 -2
  274. package/vue/UiTabs.vue +14 -12
  275. package/vue/UiTextarea.d.ts +2 -0
  276. package/vue/UiTextarea.vue +32 -4
  277. package/vue/UiToast.d.ts +34 -0
  278. package/vue/UiToast.js +2 -0
  279. package/vue/UiToast.vue +173 -0
  280. package/vue/UiToastRegion.d.ts +2 -18
  281. package/vue/UiToastRegion.vue +123 -48
  282. package/vue/UiTooltip.d.ts +2 -2
  283. package/vue/UiTooltip.vue +67 -12
  284. package/vue/UiTree.d.ts +10 -0
  285. package/vue/UiTree.vue +27 -0
  286. package/vue/UiTreeItem.d.ts +2 -0
  287. package/vue/UiTreeItem.vue +96 -4
  288. package/vue/chunks/UiAvatar.js +19 -5
  289. package/vue/chunks/UiAvatarGroup.js +10 -4
  290. package/vue/chunks/UiBadge.js +1 -1
  291. package/vue/chunks/UiButton.js +58 -11
  292. package/vue/chunks/UiCallout.js +2 -2
  293. package/vue/chunks/UiCheckbox.js +12 -2
  294. package/vue/chunks/UiCombobox.js +29 -13
  295. package/vue/chunks/UiContextMenu.js +6 -2
  296. package/vue/chunks/UiDialog.js +25 -8
  297. package/vue/chunks/UiDisclosure.js +36 -12
  298. package/vue/chunks/UiEditable.js +22 -8
  299. package/vue/chunks/UiEditorInsertTableGrid.js +11 -14
  300. package/vue/chunks/UiEditorMentionMenu.js +30 -19
  301. package/vue/chunks/UiEditorMentionMenuItem.js +36 -0
  302. package/vue/chunks/UiEditorSlashMenu.js +33 -35
  303. package/vue/chunks/UiEditorSlashMenuGroup.js +26 -0
  304. package/vue/chunks/UiEditorSlashMenuItem.js +51 -0
  305. package/vue/chunks/UiEditorTableContextMenu.js +39 -5
  306. package/vue/chunks/UiEditorTableOverlay.js +6 -6
  307. package/vue/chunks/UiEditorTableToolbar.js +37 -4
  308. package/vue/chunks/UiEditorToolbar.js +18 -4
  309. package/vue/chunks/UiFloatingActionButton.js +1 -1
  310. package/vue/chunks/UiFormField.js +13 -9
  311. package/vue/chunks/UiGrid.js +6 -2
  312. package/vue/chunks/UiIcon.js +40 -2
  313. package/vue/chunks/UiIconButton.js +35 -4
  314. package/vue/chunks/UiInput.js +1 -1
  315. package/vue/chunks/UiInputGroup.js +1 -1
  316. package/vue/chunks/UiListbox.js +70 -0
  317. package/vue/chunks/UiMenu.js +6 -2
  318. package/vue/chunks/UiMenuGroup.js +25 -0
  319. package/vue/chunks/UiMenuItem.js +128 -7
  320. package/vue/chunks/UiPopover.js +1 -1
  321. package/vue/chunks/UiRadioGroup.js +8 -2
  322. package/vue/chunks/UiSearchShell.js +1 -1
  323. package/vue/chunks/UiSelect.js +1 -1
  324. package/vue/chunks/UiSwitch.js +1 -1
  325. package/vue/chunks/UiSwitcher.js +20 -3
  326. package/vue/chunks/UiTabs.js +4 -3
  327. package/vue/chunks/UiTextarea.js +7 -2
  328. package/vue/chunks/UiToast.js +81 -0
  329. package/vue/chunks/UiToastRegion.js +25 -49
  330. package/vue/chunks/UiTooltip.js +3 -7
  331. package/vue/chunks/UiTree.js +8 -4
  332. package/vue/chunks/UiTreeItem.js +28 -10
  333. package/vue/components.css +1840 -851
  334. package/vue/editor/index.js +263 -2
  335. package/vue/index.d.ts +6 -0
  336. package/vue/index.js +9 -3
@@ -8,10 +8,12 @@ export const icons = {
8
8
  "book-user": [["path",{"d":"M15 13a3 3 0 1 0-6 0"}],["path",{"d":"M4 19.5v-15A2.5 2.5 0 0 1 6.5 2H19a1 1 0 0 1 1 1v18a1 1 0 0 1-1 1H6.5a1 1 0 0 1 0-5H20"}],["circle",{"cx":"12","cy":"8","r":"2"}]],
9
9
  "braces": [["path",{"d":"M8 3H7a2 2 0 0 0-2 2v5a2 2 0 0 1-2 2 2 2 0 0 1 2 2v5c0 1.1.9 2 2 2h1"}],["path",{"d":"M16 21h1a2 2 0 0 0 2-2v-5c0-1.1.9-2 2-2a2 2 0 0 1-2-2V5a2 2 0 0 0-2-2h-1"}]],
10
10
  "calculator": [["rect",{"width":"16","height":"20","x":"4","y":"2","rx":"2"}],["line",{"x1":"8","x2":"16","y1":"6","y2":"6"}],["line",{"x1":"16","x2":"16","y1":"14","y2":"18"}],["path",{"d":"M16 10h.01"}],["path",{"d":"M12 10h.01"}],["path",{"d":"M8 10h.01"}],["path",{"d":"M12 14h.01"}],["path",{"d":"M8 14h.01"}],["path",{"d":"M12 18h.01"}],["path",{"d":"M8 18h.01"}]],
11
+ "check": [["path",{"d":"M20 6 9 17l-5-5"}]],
11
12
  "chevron-down": [["path",{"d":"m6 9 6 6 6-6"}]],
12
13
  "chevron-left": [["path",{"d":"m15 18-6-6 6-6"}]],
13
14
  "chevron-right": [["path",{"d":"m9 18 6-6-6-6"}]],
14
15
  "circle-check": [["circle",{"cx":"12","cy":"12","r":"10"}],["path",{"d":"m9 12 2 2 4-4"}]],
16
+ "circle-dot": [["circle",{"cx":"12","cy":"12","r":"10"}],["circle",{"cx":"12","cy":"12","r":"1"}]],
15
17
  "circle-x": [["circle",{"cx":"12","cy":"12","r":"10"}],["path",{"d":"m15 9-6 6"}],["path",{"d":"m9 9 6 6"}]],
16
18
  "code-xml": [["path",{"d":"m18 16 4-4-4-4"}],["path",{"d":"m6 8-4 4 4 4"}],["path",{"d":"m14.5 4-5 16"}]],
17
19
  "columns": [["rect",{"width":"18","height":"18","x":"3","y":"3","rx":"2"}],["path",{"d":"M9 3v18"}],["path",{"d":"M15 3v18"}]],
@@ -31,6 +33,7 @@ export const icons = {
31
33
  "info": [["circle",{"cx":"12","cy":"12","r":"10"}],["path",{"d":"M12 16v-4"}],["path",{"d":"M12 8h.01"}]],
32
34
  "italic": [["line",{"x1":"19","x2":"10","y1":"4","y2":"4"}],["line",{"x1":"14","x2":"5","y1":"20","y2":"20"}],["line",{"x1":"15","x2":"9","y1":"4","y2":"20"}]],
33
35
  "layout-dashboard": [["rect",{"width":"7","height":"9","x":"3","y":"3","rx":"1"}],["rect",{"width":"7","height":"5","x":"14","y":"3","rx":"1"}],["rect",{"width":"7","height":"9","x":"14","y":"12","rx":"1"}],["rect",{"width":"7","height":"5","x":"3","y":"16","rx":"1"}]],
36
+ "link": [["path",{"d":"M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"}],["path",{"d":"M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"}]],
34
37
  "list": [["path",{"d":"M3 5h.01"}],["path",{"d":"M3 12h.01"}],["path",{"d":"M3 19h.01"}],["path",{"d":"M8 5h13"}],["path",{"d":"M8 12h13"}],["path",{"d":"M8 19h13"}]],
35
38
  "list-ordered": [["path",{"d":"M11 5h10"}],["path",{"d":"M11 12h10"}],["path",{"d":"M11 19h10"}],["path",{"d":"M4 4h1v5"}],["path",{"d":"M4 9h2"}],["path",{"d":"M6.5 20H3.4c0-1 2.6-1.925 2.6-3.5a1.5 1.5 0 0 0-2.6-1.02"}]],
36
39
  "list-todo": [["path",{"d":"M13 5h8"}],["path",{"d":"M13 12h8"}],["path",{"d":"M13 19h8"}],["path",{"d":"m3 17 2 2 4-4"}],["rect",{"x":"3","y":"4","width":"6","height":"6","rx":"1"}]],
@@ -58,5 +61,7 @@ export const icons = {
58
61
  "truck": [["path",{"d":"M14 18V6a2 2 0 0 0-2-2H4a2 2 0 0 0-2 2v11a1 1 0 0 0 1 1h2"}],["path",{"d":"M15 18H9"}],["path",{"d":"M19 18h2a1 1 0 0 0 1-1v-3.65a1 1 0 0 0-.22-.624l-3.48-4.35A1 1 0 0 0 17.52 8H14"}],["circle",{"cx":"17","cy":"18","r":"2"}],["circle",{"cx":"7","cy":"18","r":"2"}]],
59
62
  "underline": [["path",{"d":"M6 4v6a6 6 0 0 0 12 0V4"}],["line",{"x1":"4","x2":"20","y1":"20","y2":"20"}]],
60
63
  "undo": [["path",{"d":"M9 14 4 9l5-5"}],["path",{"d":"M4 9h10.5a5.5 5.5 0 0 1 5.5 5.5a5.5 5.5 0 0 1-5.5 5.5H11"}]],
64
+ "user-round": [["circle",{"cx":"12","cy":"8","r":"5"}],["path",{"d":"M20 21a8 8 0 0 0-16 0"}]],
61
65
  "users": [["path",{"d":"M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2"}],["path",{"d":"M16 3.128a4 4 0 0 1 0 7.744"}],["path",{"d":"M22 21v-2a4 4 0 0 0-3-3.87"}],["circle",{"cx":"9","cy":"7","r":"4"}]],
66
+ "x": [["path",{"d":"M18 6 6 18"}],["path",{"d":"m6 6 12 12"}]],
62
67
  };
@@ -0,0 +1,49 @@
1
+ const itemSelector = '[role="menuitem"], [role="menuitemcheckbox"], [role="menuitemradio"]';
2
+ const searches = new WeakMap();
3
+
4
+ export function menuItemFrom(target) {
5
+ return target?.closest?.(itemSelector) ?? null;
6
+ }
7
+
8
+ export function menuItems(menu) {
9
+ return Array.from(menu.querySelectorAll(itemSelector)).filter((item) =>
10
+ item.getAttribute("aria-disabled") !== "true" && !item.hasAttribute("disabled") && !item.closest("[hidden]"));
11
+ }
12
+
13
+ function label(item) {
14
+ return (item.querySelector(".label")?.textContent ?? item.textContent ?? "").trim().toLocaleLowerCase();
15
+ }
16
+
17
+ /** Move within one open menu, including named groups; only enabled items receive focus. */
18
+ export function navigateMenu(event, menu) {
19
+ const items = menuItems(menu);
20
+ if (!items.length) return false;
21
+ const current = menuItemFrom(event.target);
22
+ const index = items.indexOf(current);
23
+ let next = -1;
24
+ switch (event.key) {
25
+ case "ArrowDown": next = index < 0 ? 0 : (index + 1) % items.length; break;
26
+ case "ArrowUp": next = index < 0 ? items.length - 1 : (index - 1 + items.length) % items.length; break;
27
+ case "Home": next = 0; break;
28
+ case "End": next = items.length - 1; break;
29
+ default: {
30
+ if (event.key.length !== 1 || event.key === " " || event.altKey || event.ctrlKey || event.metaKey) return false;
31
+ const now = Date.now();
32
+ const previous = searches.get(menu);
33
+ const query = `${previous && now - previous.time < 700 ? previous.query : ""}${event.key.toLocaleLowerCase()}`;
34
+ searches.set(menu, { query, time: now });
35
+ const find = (prefix) => {
36
+ for (let offset = 1; offset <= items.length; offset += 1) {
37
+ const candidate = (index + offset + items.length) % items.length;
38
+ if (label(items[candidate]).startsWith(prefix)) return candidate;
39
+ }
40
+ return -1;
41
+ };
42
+ next = find(query);
43
+ if (next < 0 && query.length > 1) next = find(event.key.toLocaleLowerCase());
44
+ }
45
+ }
46
+ event.preventDefault();
47
+ if (next >= 0) items[next].focus();
48
+ return true;
49
+ }
@@ -1,6 +1,4 @@
1
1
  const stacks = new WeakMap();
2
- let anchorSequence = 0;
3
- const anchorBindings = new WeakMap();
4
2
 
5
3
  function stateFor(document) {
6
4
  let state = stacks.get(document);
@@ -18,7 +16,7 @@ function syncScrollLock(document, state) {
18
16
 
19
17
  function requestClose(document, reason, trigger) {
20
18
  const record = stateFor(document).records.at(-1);
21
- if (!record || (record.dismissible === false && (reason === "escape" || reason === "light-dismiss"))) return false;
19
+ if (!record || (record.dismissible === false && (reason === "escape" || reason === "light-dismiss")) || record.canClose?.(reason) === false) return false;
22
20
  record.requestClose(reason, trigger);
23
21
  return true;
24
22
  }
@@ -102,25 +100,6 @@ function hide(surface) {
102
100
  surface.hidden = true;
103
101
  }
104
102
 
105
- function bindAnchor(anchor, name) {
106
- const binding = anchorBindings.get(anchor) ?? { original: anchor.style.getPropertyValue("anchor-name"), names: new Set() };
107
- binding.names.add(name);
108
- anchorBindings.set(anchor, binding);
109
- anchor.style.setProperty("anchor-name", [binding.original, ...binding.names].filter(Boolean).join(", "));
110
- }
111
-
112
- function unbindAnchor(anchor, name) {
113
- const binding = anchorBindings.get(anchor);
114
- if (!binding) return;
115
- binding.names.delete(name);
116
- if (binding.names.size) anchor.style.setProperty("anchor-name", [binding.original, ...binding.names].filter(Boolean).join(", "));
117
- else {
118
- if (binding.original) anchor.style.setProperty("anchor-name", binding.original);
119
- else anchor.style.removeProperty("anchor-name");
120
- anchorBindings.delete(anchor);
121
- }
122
- }
123
-
124
103
  function viewport(owner) {
125
104
  const visual = owner.visualViewport;
126
105
  const left = visual?.offsetLeft ?? 0;
@@ -151,13 +130,33 @@ function layoutRect(surface) {
151
130
 
152
131
  function fallbackPosition(surface, anchor, placement, gap, viewportGap, surfaceRect = layoutRect(surface)) {
153
132
  const bounds = viewport(surface.ownerDocument.defaultView);
154
- const preferTop = placement.startsWith("top");
155
- const preferEnd = placement.endsWith("end");
156
- const below = bounds.bottom - anchor.bottom - viewportGap;
157
- const above = anchor.top - bounds.top - viewportGap;
158
- const useTop = preferTop ? !(above < surfaceRect.height + gap && below > above) : below < surfaceRect.height + gap && above > below;
159
- const top = useTop ? anchor.top - surfaceRect.height - gap : anchor.bottom + gap;
160
- const left = preferEnd ? anchor.right - surfaceRect.width : anchor.left;
133
+ const [preferred, align = "center"] = placement.split("-");
134
+ const side = ["top", "bottom", "left", "right"].includes(preferred) ? preferred : "bottom";
135
+ const opposite = { top: "bottom", bottom: "top", left: "right", right: "left" }[side];
136
+ const available = {
137
+ top: anchor.top - bounds.top - viewportGap,
138
+ bottom: bounds.bottom - anchor.bottom - viewportGap,
139
+ left: anchor.left - bounds.left - viewportGap,
140
+ right: bounds.right - anchor.right - viewportGap,
141
+ };
142
+ const needed = (side === "top" || side === "bottom" ? surfaceRect.height : surfaceRect.width) + gap;
143
+ const actual = available[side] < needed && available[opposite] > available[side] ? opposite : side;
144
+ surface.dataset.uiActualPlacement = actual;
145
+ surface.dataset.uiActualAlign = align;
146
+ const rtl = getComputedStyle(surface).direction === "rtl";
147
+ let left;
148
+ let top;
149
+ if (actual === "top" || actual === "bottom") {
150
+ top = actual === "top" ? anchor.top - surfaceRect.height - gap : anchor.bottom + gap;
151
+ left = align === "start"
152
+ ? (rtl ? anchor.right - surfaceRect.width : anchor.left)
153
+ : align === "end"
154
+ ? (rtl ? anchor.left : anchor.right - surfaceRect.width)
155
+ : (anchor.left + anchor.right - surfaceRect.width) / 2;
156
+ } else {
157
+ left = actual === "left" ? anchor.left - surfaceRect.width - gap : anchor.right + gap;
158
+ top = align === "start" ? anchor.top : align === "end" ? anchor.bottom - surfaceRect.height : (anchor.top + anchor.bottom - surfaceRect.height) / 2;
159
+ }
161
160
  const shift = clamp({ left, top, right: left + surfaceRect.width, bottom: top + surfaceRect.height, width: surfaceRect.width, height: surfaceRect.height }, bounds, viewportGap);
162
161
  surface.style.left = `${Math.round(left + shift.x)}px`;
163
162
  surface.style.top = `${Math.round(top + shift.y)}px`;
@@ -168,10 +167,8 @@ function fallbackPosition(surface, anchor, placement, gap, viewportGap, surfaceR
168
167
  export function createAnchoredSurface(surface, options = {}) {
169
168
  const owner = surface.ownerDocument.defaultView;
170
169
  let placement = options.placement ?? "bottom-start";
171
- const gap = Math.max(0, options.gap ?? 4);
170
+ const gap = () => Math.max(0, Number(typeof options.gap === "function" ? options.gap() : options.gap ?? 4) || 0);
172
171
  const viewportGap = Math.max(0, options.viewportGap ?? 8);
173
- const nativeAnchor = owner.CSS?.supports?.("anchor-name: --ui-anchor-test") && owner.CSS.supports("position-anchor: --ui-anchor-test") && owner.CSS.supports("top: anchor(bottom)");
174
- const anchorName = `--ui-anchor-${++anchorSequence}`;
175
172
  let anchor = options.anchor ?? null;
176
173
  let point = null;
177
174
  let open = false;
@@ -179,40 +176,17 @@ export function createAnchoredSurface(surface, options = {}) {
179
176
  let abort = null;
180
177
  let sizeObserver = null;
181
178
  surface.setAttribute("popover", "manual");
182
- surface.dataset.uiPositioning = nativeAnchor ? "anchor" : "fallback";
183
179
  surface.style.position = "fixed";
184
180
  surface.style.margin = "0";
185
- const bind = (next) => {
186
- if (anchor) unbindAnchor(anchor, anchorName);
187
- anchor = next;
188
- if (anchor) {
189
- bindAnchor(anchor, anchorName);
190
- surface.style.setProperty("position-anchor", anchorName);
191
- } else surface.style.removeProperty("position-anchor");
192
- };
193
181
  const position = () => {
194
182
  frame = null;
195
183
  if (!open) return;
196
184
  if (point) {
197
- fallbackPosition(surface, { left: point.x, right: point.x, top: point.y, bottom: point.y, width: 0, height: 0 }, "bottom-start", gap, viewportGap);
185
+ fallbackPosition(surface, { left: point.x, right: point.x, top: point.y, bottom: point.y, width: 0, height: 0 }, "bottom-start", gap(), viewportGap);
198
186
  return;
199
187
  }
200
188
  if (!anchor) return;
201
- let rect;
202
- if (nativeAnchor) {
203
- const blockEnd = placement.startsWith("bottom");
204
- const inlineEnd = placement.endsWith("end");
205
- surface.style.top = blockEnd ? `calc(anchor(bottom) + ${gap}px)` : "auto";
206
- surface.style.bottom = blockEnd ? "auto" : `calc(anchor(top) + ${gap}px)`;
207
- surface.style.left = inlineEnd ? "auto" : "anchor(left)";
208
- surface.style.right = inlineEnd ? "anchor(right)" : "auto";
209
- surface.style.setProperty("position-try-fallbacks", "flip-block, flip-inline");
210
- rect = layoutRect(surface);
211
- if (!rect.width || !rect.height) return;
212
- const shift = clamp(rect, viewport(owner), viewportGap);
213
- if (!shift.x && !shift.y) return;
214
- }
215
- fallbackPosition(surface, anchor.getBoundingClientRect(), placement, gap, viewportGap, rect);
189
+ fallbackPosition(surface, anchor.getBoundingClientRect(), placement, gap(), viewportGap);
216
190
  };
217
191
  const schedule = () => {
218
192
  if (open && frame === null) frame = owner.requestAnimationFrame(position);
@@ -238,15 +212,14 @@ export function createAnchoredSurface(surface, options = {}) {
238
212
  sizeObserver.observe(surface);
239
213
  };
240
214
  const stopSize = () => { sizeObserver?.disconnect(); sizeObserver = null; };
241
- bind(anchor);
242
215
  return {
243
- setAnchor(next) { point = null; bind(next); syncListeners(); schedule(); },
216
+ setAnchor(next) { point = null; anchor = next; syncListeners(); schedule(); },
244
217
  setPlacement(next) { placement = next || "bottom-start"; schedule(); },
245
218
  show() { point = null; open = true; syncListeners(); show(surface); position(); schedule(); observeSize(); },
246
219
  showAtPoint(next) { point = next; open = true; syncListeners(); show(surface); position(); schedule(); observeSize(); },
247
220
  hide() { open = false; point = null; syncListeners(); stopSize(); if (frame !== null) owner.cancelAnimationFrame(frame); frame = null; hide(surface); },
248
221
  refresh: schedule,
249
- destroy() { open = false; abort?.abort(); abort = null; stopSize(); if (frame !== null) owner.cancelAnimationFrame(frame); frame = null; hide(surface); bind(null); },
222
+ destroy() { open = false; abort?.abort(); abort = null; stopSize(); if (frame !== null) owner.cancelAnimationFrame(frame); frame = null; hide(surface); anchor = null; },
250
223
  };
251
224
  }
252
225
 
@@ -1,3 +1,4 @@
1
+ <link rel="component" href="../ui-icon/ui-icon.html">
1
2
  <template component="ui-avatar" controller="./ui-avatar.js">
2
3
  <defs>
3
4
  <prop name="src" type="string"
@@ -14,17 +15,23 @@
14
15
  circle clips anything wider.</prop>
15
16
  <prop name="size" type="sm | md" default="md"
16
17
  >md is 2.5rem, for a person on their own (a profile, a comment); sm is 1.75rem with smaller initials, for a row of people in a toolbar or header.</prop>
18
+ <prop name="shape" type="circle | square" default="circle">Circle by default; square uses the theme's medium radius.</prop>
19
+ <prop name="decorative" type="boolean" default="false">Hides the avatar from assistive technology when adjacent text already identifies the person.</prop>
17
20
  <prop name="active" type="boolean" default="false"
18
21
  >Marks the person as active now, such as editing the same page: a ring in the success colour around the avatar. The ring is not announced, so say what it
19
22
  means in alt ("Ada Lovelace, editing").</prop>
20
23
  <state name="hasImage" :value="false"></state>
21
24
  <state name="hasAuthoredImage" :value="false"></state>
22
25
  <state name="initials" :value="'?'"></state>
26
+ <state name="showGlyph" :value="false"></state>
23
27
  </defs>
24
28
  <span>
25
29
  <slot></slot>
26
30
  <img class="image" $ref="image" :src="src" alt="" aria-hidden="true" :hidden="hasAuthoredImage or not hasImage">
27
- <span class="fallback" :aria-hidden="hasImage" :hidden="hasImage" $value="initials"></span>
31
+ <span class="fallback" :aria-hidden="hasImage" :hidden="hasImage">
32
+ <span :hidden="showGlyph" $value="initials"></span>
33
+ <span :hidden="not showGlyph"><ui-icon name="user-round"></ui-icon></span>
34
+ </span>
28
35
  </span>
29
36
  <style>
30
37
  /* This component's hooks style the element they are set on, and never a nested one. */
@@ -32,6 +39,10 @@
32
39
  syntax: "*";
33
40
  inherits: false;
34
41
  }
42
+ @property --ui-avatar-radius {
43
+ syntax: "*";
44
+ inherits: false;
45
+ }
35
46
  @property --ui-avatar-surface {
36
47
  syntax: "*";
37
48
  inherits: false;
@@ -46,12 +57,13 @@
46
57
  }
47
58
 
48
59
  :host {
60
+ --_ui-default-icon-size: 1.25rem;
49
61
  display: inline-grid;
50
62
  place-items: center;
51
63
  overflow: hidden;
52
64
  inline-size: 2.5rem;
53
65
  block-size: 2.5rem;
54
- border-radius: 999px;
66
+ border-radius: var(--ui-avatar-radius, var(--ui-radius-round));
55
67
  border: 1px solid var(--ui-avatar-border, var(--ui-border-strong));
56
68
  background: var(--ui-avatar-surface, var(--ui-accent-soft));
57
69
  color: var(--ui-avatar-text, var(--ui-accent-subtle-text, var(--ui-accent-solid)));
@@ -62,11 +74,16 @@
62
74
  }
63
75
 
64
76
  :host-state([size="sm"]) {
77
+ --_ui-default-icon-size: 1rem;
65
78
  inline-size: 1.75rem;
66
79
  block-size: 1.75rem;
67
80
  font-size: var(--ui-font-size-xs);
68
81
  }
69
82
 
83
+ :host-state([shape="square"]) {
84
+ border-radius: var(--ui-avatar-radius, var(--ui-radius-md));
85
+ }
86
+
70
87
  /* An outline, not a dot: the avatar clips its content to the circle, and an outline is drawn
71
88
  outside the box, so the ring survives the clip and the overlap in an Avatar Group. */
72
89
  :host-state([active]) {
@@ -29,11 +29,17 @@ export default function controller(host) {
29
29
  authored.hidden = !host.state.hasImage;
30
30
  element.removeAttribute("role");
31
31
  element.removeAttribute("aria-label");
32
- } else {
32
+ } else if (!host.state.decorative) {
33
33
  element.setAttribute("role", "img");
34
34
  element.setAttribute("aria-label", host.state.alt || host.state.name || "Avatar");
35
35
  }
36
+ if (host.state.decorative) {
37
+ element.setAttribute("aria-hidden", "true");
38
+ element.removeAttribute("role");
39
+ element.removeAttribute("aria-label");
40
+ } else element.removeAttribute("aria-hidden");
36
41
  host.state.initials = host.state.fallback || toInitials(host.state.name || host.state.alt);
42
+ host.state.showGlyph = !String(host.state.name || "").trim() && !String(host.state.alt || "").trim() && !String(host.state.fallback || "").trim();
37
43
  };
38
44
  const observer = new MutationObserver(apply);
39
45
  observer.observe(element, { childList: true });
@@ -3,15 +3,21 @@
3
3
  <prop name="max" type="number" default="5"
4
4
  >How many avatars to show. The rest are hidden and counted in a +N badge after the last visible one. Fractions round down, and 0 hides every avatar behind
5
5
  the badge.</prop>
6
+ <prop name="total" type="integer"
7
+ >The true number of people when only some avatars are authored. The overflow badge counts the people beyond the visible avatars; it never counts fewer than
8
+ the authored avatars.</prop>
9
+ <prop name="overflowLabel" type="string" default="{count} more"
10
+ >Accessible label for the overflow badge. Replace {count} with the number of people beyond the visible avatars.</prop>
6
11
  <prop name="label" type="string" default="People"
7
12
  >The group's accessible name, announced for the set of avatars as a whole. Name who they are rather than repeating the default.</prop>
8
13
  <prop name="size" type="sm | md" default="md"
9
14
  >The size of the avatars inside, matched by the +N badge and the overlap. Give each Avatar the same size.</prop>
10
15
  <state name="overflowCount" :value="0"></state>
16
+ <state name="overflowAnnouncement" :value="''"></state>
11
17
  </defs>
12
18
  <div role="group" :aria-label="label">
13
19
  <slot></slot>
14
- <span class="overflow" $ref="overflow" role="img" :hidden="not overflowCount" :aria-label="format('%s more', overflowCount)"><template
20
+ <span class="overflow" $ref="overflow" role="img" :hidden="not overflowCount" :aria-label="overflowAnnouncement"><template
15
21
  $value="format('+%s', overflowCount)"
16
22
  ></template></span>
17
23
  </div>
@@ -47,7 +53,6 @@
47
53
  :slotted(*) {
48
54
  margin-inline-start: -0.5rem;
49
55
  flex-shrink: 0;
50
- border-radius: 999px;
51
56
  }
52
57
 
53
58
  :slotted(*:first-child) {
@@ -61,7 +66,7 @@
61
66
  justify-content: center;
62
67
  inline-size: 2.5rem;
63
68
  block-size: 2.5rem;
64
- border-radius: 999px;
69
+ border-radius: var(--ui-radius-round);
65
70
  border: 1px solid var(--ui-border-strong);
66
71
  background: var(--ui-surface-elevated);
67
72
  color: var(--ui-text-primary);
@@ -11,13 +11,17 @@ export default function controller(host) {
11
11
  avatars.forEach((avatar, index) => {
12
12
  avatar.style.display = index >= visible ? "none" : "";
13
13
  avatar.style.marginInlineStart = index === 0 ? "0px" : overlap;
14
- avatar.style.borderRadius = "999px";
15
14
  // Later avatars cast a crisp shadow back onto the one they overlap; the first overlaps nothing.
16
15
  avatar.style.boxShadow = index === 0
17
16
  ? "var(--ui-avatar-group-edge-ring, var(--_edge-ring))"
18
17
  : "var(--ui-avatar-group-overlap-shadow, var(--_overlap-shadow)), var(--ui-avatar-group-edge-ring, var(--_edge-ring))";
19
18
  });
20
- host.state.overflowCount = Math.max(0, avatars.length - visible);
19
+ const authoredVisible = Math.min(avatars.length, visible);
20
+ const suppliedTotal = Number(host.state.total);
21
+ const total = host.state.total === undefined || !Number.isFinite(suppliedTotal) ? avatars.length : Math.max(avatars.length, suppliedTotal);
22
+ const count = Math.max(0, total - authoredVisible);
23
+ host.state.overflowCount = count;
24
+ host.state.overflowAnnouncement = String(host.state.overflowLabel || "{count} more").replaceAll("{count}", String(count));
21
25
  };
22
26
  const observer = new MutationObserver(update);
23
27
  observer.observe(element, { childList: true });
@@ -1,8 +1,8 @@
1
1
  <template component="ui-badge">
2
2
  <defs>
3
- <prop name="variant" type="solid | subtle" default="subtle"
4
- >Emphasis. subtle sets the text in its tone over a soft wash of it; solid fills with the tone and uses contrasting text, for a status that has to stand out.
5
- The neutral tone looks the same in both.</prop>
3
+ <prop name="variant" type="solid | subtle | outline" default="subtle"
4
+ >Emphasis. subtle sets toned text over a soft wash; solid fills with the tone and uses contrasting text; outline keeps the surface with a toned edge.
5
+ Neutral looks the same in solid and subtle.</prop>
6
6
  <prop name="tone" type="neutral | accent | info | success | warning | danger" default="neutral"
7
7
  >Which colour the badge speaks in. neutral is a quiet grey for plain labels; accent draws the eye in the accent colour; info, success, warning, and danger
8
8
  mark a status, so match the tone to what the badge says.</prop>
@@ -58,6 +58,7 @@
58
58
  --_badge-text: var(--ui-text-secondary);
59
59
  /* Every tone's edge is its fill, so it is unseen until forced colors draw it. */
60
60
  --_badge-border: var(--ui-surface-subtle);
61
+ --_badge-outline-surface: var(--ui-badge-surface, var(--_badge-surface));
61
62
 
62
63
  display: inline-flex;
63
64
  align-items: center;
@@ -66,7 +67,7 @@
66
67
  padding: var(--ui-badge-padding-block, var(--ui-space-0-5)) var(--ui-badge-padding-inline, var(--ui-space-2));
67
68
  border: 1px solid var(--ui-badge-border, var(--_badge-border));
68
69
  border-radius: var(--ui-radius-round);
69
- background: var(--ui-badge-surface, var(--_badge-surface));
70
+ background: var(--_badge-outline-surface);
70
71
  color: var(--ui-badge-text, var(--_badge-text));
71
72
  font-family: var(--ui-font-family-sans);
72
73
  font-size: var(--ui-badge-font-size, var(--ui-font-size-sm));
@@ -149,6 +150,37 @@
149
150
  --_badge-text: var(--ui-info-subtle-text, var(--ui-info-solid));
150
151
  }
151
152
 
153
+ :host-state([variant="outline"]) {
154
+ --_badge-surface: var(--ui-surface-default);
155
+ --_badge-border: var(--ui-border-strong);
156
+ --_badge-text: var(--ui-text-secondary);
157
+ }
158
+
159
+ :host-state([variant="outline"][tone="accent"]) {
160
+ --_badge-border: var(--ui-accent-solid);
161
+ --_badge-text: var(--ui-accent-subtle-text, var(--ui-accent-solid));
162
+ }
163
+
164
+ :host-state([variant="outline"][tone="danger"]) {
165
+ --_badge-border: var(--ui-danger-solid);
166
+ --_badge-text: var(--ui-danger-subtle-text, var(--ui-danger-solid));
167
+ }
168
+
169
+ :host-state([variant="outline"][tone="success"]) {
170
+ --_badge-border: var(--ui-success-solid);
171
+ --_badge-text: var(--ui-success-subtle-text, var(--ui-success-solid));
172
+ }
173
+
174
+ :host-state([variant="outline"][tone="warning"]) {
175
+ --_badge-border: var(--ui-warning-solid);
176
+ --_badge-text: var(--ui-warning-subtle-text, var(--ui-warning-solid));
177
+ }
178
+
179
+ :host-state([variant="outline"][tone="info"]) {
180
+ --_badge-border: var(--ui-info-solid);
181
+ --_badge-text: var(--ui-info-subtle-text, var(--ui-info-solid));
182
+ }
183
+
152
184
  /* A tag is flat at the start and points at the end. The point is cut from the box, so it has no
153
185
  outline to follow: the edge is the fill. The end padding grows by the point's depth so the
154
186
  label never runs into it. */
@@ -167,6 +199,40 @@
167
199
  clip-path: polygon(var(--_badge-point) 0, 100% 0, 100% 100%, var(--_badge-point) 100%, 0 50%);
168
200
  }
169
201
 
202
+ /* The tag clips its point, including the native border. Draw the outer shape in the border
203
+ colour and an inset inner shape in the surface colour to keep a visible diagonal edge. */
204
+ :host-state([variant="outline"][shape="tag"]) {
205
+ position: relative;
206
+ isolation: isolate;
207
+ background: var(--ui-badge-border, var(--_badge-border));
208
+ }
209
+
210
+ :host-state([variant="outline"][shape="tag"])::before {
211
+ content: "";
212
+ position: absolute;
213
+ inset: 1px;
214
+ z-index: -1;
215
+ background: var(--_badge-outline-surface);
216
+ clip-path: polygon(0 0, calc(100% - var(--_badge-point)) 0, 100% 50%, calc(100% - var(--_badge-point)) 100%, 0 100%);
217
+ pointer-events: none;
218
+ }
219
+
220
+ :host-state([variant="outline"][shape="tag"]):dir(rtl)::before {
221
+ clip-path: polygon(var(--_badge-point) 0, 100% 0, 100% 100%, var(--_badge-point) 100%, 0 50%);
222
+ }
223
+
224
+ @media (forced-colors: active) {
225
+ :host-state([variant="outline"][shape="tag"]) {
226
+ forced-color-adjust: none;
227
+ background: CanvasText;
228
+ color: CanvasText;
229
+ }
230
+
231
+ :host-state([variant="outline"][shape="tag"])::before {
232
+ background: Canvas;
233
+ }
234
+ }
235
+
170
236
  /* A component that sets its display still honours the hidden attribute on its root. */
171
237
  :host[hidden] {
172
238
  display: none;
@@ -1,4 +1,5 @@
1
- <template component="ui-button">
1
+ <link rel="component" href="../ui-spinner/ui-spinner.html">
2
+ <template component="ui-button" controller="./ui-button.js">
2
3
  <defs>
3
4
  <prop name="variant" type="outline | solid | danger | ghost | link" default="outline"
4
5
  >Emphasis: how loudly the button asks. solid fills with its tone, outline draws its tone at the edge over a wash of it, ghost carries only text until it is
@@ -12,6 +13,9 @@
12
13
  <prop name="disabled" type="boolean" default="false"
13
14
  >Makes the button unavailable: it cannot be pressed, its tone fades, and it loses its raised look. A disabled link (as="a" with href) drops its href and
14
15
  states aria-disabled="true" instead, because a link has no native disabled state.</prop>
16
+ <prop name="pending" type="boolean" default="false"
17
+ >Keeps the action focusable and its label visible while work is in progress. It shows a decorative spinner, states aria-disabled and aria-busy, and prevents
18
+ activation or form submission until cleared.</prop>
15
19
  <prop name="align" type="center | start" default="center"
16
20
  >Content alignment. start lays the content out like a list option: from the start edge, with start-aligned text.</prop>
17
21
  <prop name="stretch" type="boolean" default="false">Fills the inline size of its container.</prop>
@@ -30,14 +34,18 @@
30
34
  <template $match>
31
35
  <a
32
36
  $when="as = 'a'"
33
- :href="{ true: null, false: href }[format('%s', disabled)]"
37
+ :href="{ true: null, false: href }[format('%s', disabled or pending)]"
34
38
  :target="target"
35
39
  :rel="rel"
36
- :aria-disabled="{ true: 'true', false: null }[format('%s', disabled and href)]"
40
+ :aria-disabled="{ true: 'true', false: null }[format('%s', disabled or pending)]"
41
+ :aria-busy="pending"
42
+ :tabindex="{ true: '0', false: null }[format('%s', pending and not disabled)]"
37
43
  >
44
+ <ui-spinner $if="pending" size="sm" class="pending-spinner"></ui-spinner>
38
45
  <slot></slot>
39
46
  </a>
40
- <button $else :type="type" :disabled="disabled">
47
+ <button $else :type="type" :disabled="disabled" :aria-disabled="{ true: 'true', false: null }[format('%s', pending)]" :aria-busy="pending">
48
+ <ui-spinner $if="pending" size="sm" class="pending-spinner"></ui-spinner>
41
49
  <slot></slot>
42
50
  </button>
43
51
  </template>
@@ -148,7 +156,7 @@
148
156
  border-style: solid;
149
157
  border-width: 1px;
150
158
  border-color: var(--ui-button-border, var(--_tone));
151
- border-radius: var(--ui-button-radius, var(--ui-radius-md));
159
+ border-radius: var(--ui-button-radius, var(--ui-action-radius, var(--ui-radius-md)));
152
160
  background-color: var(--ui-button-surface, oklch(from var(--_tone) l c h / 5%));
153
161
  color: var(--ui-button-text, var(--_tone-text));
154
162
  font: inherit;
@@ -167,6 +175,10 @@
167
175
  margin: 0;
168
176
  }
169
177
 
178
+ .pending-spinner {
179
+ margin-inline-end: var(--ui-space-2);
180
+ }
181
+
170
182
  @supports (text-box-trim: trim-both) {
171
183
  :host {
172
184
  text-box-trim: trim-both;
@@ -307,6 +319,10 @@
307
319
  box-shadow: none;
308
320
  }
309
321
 
322
+ :host-state([pending]):focus-visible {
323
+ box-shadow: var(--ui-focus-halo);
324
+ }
325
+
310
326
  /* Ghost has no hover to fall back on, so disabled is the one state it has to state outright:
311
327
  it takes a surface rather than reading as plain text. The surface is a wash of the faded tone,
312
328
  the way hover washes the live one, so it stays light in every tone. An opaque mix toward the
@@ -0,0 +1,10 @@
1
+ // Pending actions retain focus but cannot activate, navigate, submit, or notify click consumers.
2
+ export default function controller(host) {
3
+ const onClick = (event) => {
4
+ if (!host.state.pending) return;
5
+ event.preventDefault();
6
+ event.stopImmediatePropagation();
7
+ };
8
+ host.element.addEventListener("click", onClick, true);
9
+ return () => host.element.removeEventListener("click", onClick, true);
10
+ }
@@ -1,8 +1,8 @@
1
1
  <template component="ui-callout">
2
2
  <defs>
3
- <prop name="tone" type="info | note | warning | success | danger" default="info"
4
- >Which kind of message the callout carries, shown by its icon and colour: info (the default) for neutral information, note for an aside in muted grey,
5
- warning for a caution, success for a confirmation, and danger for an error or a destructive consequence.</prop>
3
+ <prop name="tone" type="info | neutral | note | warning | success | danger" default="info"
4
+ >Which kind of message the callout carries, shown by its icon and colour: info (the default) for information, neutral for a muted aside, warning for a
5
+ caution, success for a confirmation, and danger for a harmful consequence. note remains as an alias for neutral.</prop>
6
6
  </defs>
7
7
  <div role="note">
8
8
  <span class="icon" aria-hidden="true">
@@ -11,7 +11,15 @@
11
11
  <path d="M12 16v-4"></path>
12
12
  <path d="M12 8h.01"></path>
13
13
  </svg>
14
- <svg $if="tone = 'note'" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
14
+ <svg
15
+ $if="tone = 'neutral' or tone = 'note'"
16
+ viewBox="0 0 24 24"
17
+ fill="none"
18
+ stroke="currentColor"
19
+ stroke-width="2"
20
+ stroke-linecap="round"
21
+ stroke-linejoin="round"
22
+ >
15
23
  <path d="M13.5 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8.5L13.5 2Z"></path>
16
24
  <path d="M13 2v7h7"></path>
17
25
  <path d="m9.5 17.5 4-4 2 2-4 4H9.5v-2Z"></path>
@@ -125,7 +133,7 @@
125
133
  min-inline-size: 0;
126
134
  }
127
135
 
128
- :host-state([tone="note"]) {
136
+ :host-state([tone="neutral"]), :host-state([tone="note"]) {
129
137
  --_callout-surface: var(--ui-surface-muted);
130
138
  --_callout-border: var(--ui-border-strong);
131
139
  --_callout-icon: var(--ui-text-secondary);