@threadlabs/looma 0.17.0 → 0.17.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 (225) hide show
  1. package/components/ui-avatar-group/ui-avatar-group.html +8 -5
  2. package/components/ui-avatar-group/ui-avatar-group.js +3 -3
  3. package/components/ui-badge/ui-badge.html +10 -5
  4. package/components/ui-breadcrumb-item/ui-breadcrumb-item.html +2 -1
  5. package/components/ui-button/ui-button.html +33 -26
  6. package/components/ui-button/ui-button.js +2 -2
  7. package/components/ui-callout/ui-callout.html +3 -1
  8. package/components/ui-card/ui-card.html +5 -3
  9. package/components/ui-checkbox/ui-checkbox.html +36 -8
  10. package/components/ui-combobox/ui-combobox.html +72 -45
  11. package/components/ui-combobox/ui-combobox.js +7 -2
  12. package/components/ui-context-menu/ui-context-menu.html +2 -2
  13. package/components/ui-description-item/ui-description-item.html +1 -1
  14. package/components/ui-description-list/ui-description-list.html +2 -0
  15. package/components/ui-dialog/ui-dialog.html +3 -23
  16. package/components/ui-disclosure/ui-disclosure.html +2 -2
  17. package/components/ui-editor-insert-table-grid/ui-editor-insert-table-grid.html +2 -2
  18. package/components/ui-editor-mention-menu/ui-editor-mention-menu.html +3 -3
  19. package/components/ui-editor-slash-menu/ui-editor-slash-menu.html +4 -4
  20. package/components/ui-editor-slash-menu-item/ui-editor-slash-menu-item.html +6 -1
  21. package/components/ui-editor-table-context-menu/ui-editor-table-context-menu.html +2 -2
  22. package/components/ui-editor-table-toolbar/ui-editor-table-toolbar.html +4 -4
  23. package/components/ui-editor-toolbar/ui-editor-toolbar.html +5 -5
  24. package/components/ui-floating-action-button/ui-floating-action-button.html +4 -2
  25. package/components/ui-icon-button/ui-icon-button.html +57 -17
  26. package/components/ui-icon-button/ui-icon-button.js +1 -1
  27. package/components/ui-list/ui-list.html +46 -6
  28. package/components/ui-list-item/ui-list-item.html +1 -1
  29. package/components/ui-menu/ui-menu.html +4 -3
  30. package/components/ui-menu/ui-menu.js +9 -3
  31. package/components/ui-menu-item/ui-menu-item.html +43 -6
  32. package/components/ui-meter/ui-meter.html +4 -1
  33. package/components/ui-nav-item/ui-nav-item.html +0 -1
  34. package/components/ui-page-header/ui-page-header.html +1 -0
  35. package/components/ui-popover/ui-popover.html +1 -1
  36. package/components/ui-radio/ui-radio.html +5 -2
  37. package/components/ui-scroll-area/ui-scroll-area.html +9 -14
  38. package/components/ui-scroll-area/ui-scroll-area.js +32 -0
  39. package/components/ui-search-result-row/ui-search-result-row.html +5 -3
  40. package/components/ui-search-shell/ui-search-shell.html +8 -8
  41. package/components/ui-section/ui-section.html +5 -3
  42. package/components/ui-separator/ui-separator.html +2 -2
  43. package/components/ui-sidebar/ui-sidebar.html +2 -2
  44. package/components/ui-spinner/ui-spinner.html +28 -8
  45. package/components/ui-status-message/ui-status-message.html +1 -1
  46. package/components/ui-switch/ui-switch.html +17 -8
  47. package/components/ui-table/ui-table.html +1 -1
  48. package/components/ui-tabs/ui-tabs.html +21 -12
  49. package/components/ui-toast/ui-toast.html +1 -1
  50. package/components/ui-toast-region/ui-toast-region.html +1 -1
  51. package/components/ui-tooltip/ui-tooltip.html +12 -12
  52. package/components/ui-tooltip/ui-tooltip.js +1 -1
  53. package/components/ui-top-bar/ui-top-bar.html +1 -1
  54. package/components/ui-tree/ui-tree.js +4 -1
  55. package/components/ui-tree-item/ui-tree-item.html +2 -1
  56. package/dist/index.js +15 -14
  57. package/package.json +1 -1
  58. package/styles/ui-avatar-group.css +8 -5
  59. package/styles/ui-badge.css +10 -5
  60. package/styles/ui-breadcrumb-item.css +2 -1
  61. package/styles/ui-button.css +19 -25
  62. package/styles/ui-callout.css +3 -1
  63. package/styles/ui-card.css +4 -2
  64. package/styles/ui-checkbox.css +38 -8
  65. package/styles/ui-combobox.css +27 -8
  66. package/styles/ui-context-menu.css +2 -2
  67. package/styles/ui-description-item.css +1 -1
  68. package/styles/ui-description-list.css +2 -0
  69. package/styles/ui-dialog.css +1 -22
  70. package/styles/ui-disclosure.css +2 -2
  71. package/styles/ui-editor-insert-table-grid.css +2 -2
  72. package/styles/ui-editor-mention-menu.css +3 -3
  73. package/styles/ui-editor-slash-menu-item.css +6 -1
  74. package/styles/ui-editor-slash-menu.css +4 -4
  75. package/styles/ui-editor-table-context-menu.css +2 -2
  76. package/styles/ui-editor-table-toolbar.css +4 -4
  77. package/styles/ui-editor-toolbar.css +5 -5
  78. package/styles/ui-floating-action-button.css +3 -1
  79. package/styles/ui-icon-button.css +53 -18
  80. package/styles/ui-list-item.css +1 -1
  81. package/styles/ui-list.css +41 -6
  82. package/styles/ui-menu-item.css +43 -5
  83. package/styles/ui-menu.css +3 -3
  84. package/styles/ui-meter.css +4 -1
  85. package/styles/ui-nav-item.css +0 -1
  86. package/styles/ui-page-header.css +1 -0
  87. package/styles/ui-popover.css +1 -1
  88. package/styles/ui-radio.css +5 -2
  89. package/styles/ui-scroll-area.css +10 -16
  90. package/styles/ui-search-result-row.css +3 -2
  91. package/styles/ui-search-shell.css +7 -7
  92. package/styles/ui-section.css +5 -3
  93. package/styles/ui-separator.css +2 -2
  94. package/styles/ui-sidebar.css +2 -2
  95. package/styles/ui-spinner.css +27 -6
  96. package/styles/ui-status-message.css +1 -1
  97. package/styles/ui-switch.css +17 -8
  98. package/styles/ui-table.css +1 -1
  99. package/styles/ui-tabs.css +60 -51
  100. package/styles/ui-toast-region.css +1 -1
  101. package/styles/ui-toast.css +1 -1
  102. package/styles/ui-tooltip.css +12 -12
  103. package/styles/ui-top-bar.css +1 -1
  104. package/styles/ui-tree-item.css +2 -1
  105. package/theme-dark.css +4 -4
  106. package/theme-light.css +3 -3
  107. package/tokens.css +9 -6
  108. package/vanilla/UiButton.d.ts +1 -0
  109. package/vanilla/UiButton.js +17 -19
  110. package/vanilla/UiCombobox.js +162 -159
  111. package/vanilla/UiDialog.js +2 -2
  112. package/vanilla/UiIconButton.d.ts +1 -0
  113. package/vanilla/UiIconButton.js +1 -4
  114. package/vanilla/UiList.d.ts +3 -1
  115. package/vanilla/UiList.js +36 -19
  116. package/vanilla/UiMenu.d.ts +1 -0
  117. package/vanilla/UiMenu.js +1 -1
  118. package/vanilla/UiMenuItem.js +10 -13
  119. package/vanilla/UiScrollArea.js +11 -6
  120. package/vanilla/UiSearchShell.js +2 -2
  121. package/vanilla/UiSpinner.js +2 -4
  122. package/vue/UiAvatarGroup.vue +8 -5
  123. package/vue/UiBadge.vue +10 -5
  124. package/vue/UiBreadcrumbItem.vue +2 -1
  125. package/vue/UiButton.d.ts +2 -0
  126. package/vue/UiButton.vue +31 -28
  127. package/vue/UiCallout.vue +3 -2
  128. package/vue/UiCard.vue +4 -2
  129. package/vue/UiCheckbox.vue +36 -8
  130. package/vue/UiCombobox.d.ts +1 -1
  131. package/vue/UiCombobox.vue +55 -35
  132. package/vue/UiContextMenu.vue +2 -2
  133. package/vue/UiDescriptionItem.vue +1 -1
  134. package/vue/UiDescriptionList.vue +2 -0
  135. package/vue/UiDialog.d.ts +3 -3
  136. package/vue/UiDialog.vue +3 -27
  137. package/vue/UiDisclosure.vue +2 -2
  138. package/vue/UiEditorInsertTableGrid.vue +2 -2
  139. package/vue/UiEditorMentionMenu.d.ts +1 -1
  140. package/vue/UiEditorMentionMenu.vue +4 -3
  141. package/vue/UiEditorSlashMenu.vue +6 -4
  142. package/vue/UiEditorSlashMenuItem.vue +6 -1
  143. package/vue/UiEditorTableContextMenu.vue +2 -2
  144. package/vue/UiEditorTableToolbar.vue +4 -4
  145. package/vue/UiEditorToolbar.vue +5 -5
  146. package/vue/UiFloatingActionButton.vue +3 -1
  147. package/vue/UiIconButton.d.ts +2 -0
  148. package/vue/UiIconButton.vue +61 -19
  149. package/vue/UiList.d.ts +7 -1
  150. package/vue/UiList.vue +57 -6
  151. package/vue/UiListItem.vue +1 -1
  152. package/vue/UiMenu.d.ts +2 -0
  153. package/vue/UiMenu.vue +6 -3
  154. package/vue/UiMenuItem.d.ts +3 -3
  155. package/vue/UiMenuItem.vue +47 -7
  156. package/vue/UiMeter.vue +4 -1
  157. package/vue/UiNavItem.vue +0 -1
  158. package/vue/UiPageHeader.vue +1 -0
  159. package/vue/UiPopover.vue +1 -1
  160. package/vue/UiRadio.vue +5 -2
  161. package/vue/UiScrollArea.vue +24 -18
  162. package/vue/UiSearchResultRow.vue +3 -2
  163. package/vue/UiSearchShell.vue +8 -8
  164. package/vue/UiSection.vue +5 -3
  165. package/vue/UiSeparator.vue +2 -2
  166. package/vue/UiSidebar.vue +2 -2
  167. package/vue/UiSpinner.vue +28 -7
  168. package/vue/UiStatusMessage.vue +1 -1
  169. package/vue/UiSwitch.vue +20 -9
  170. package/vue/UiTable.vue +1 -1
  171. package/vue/UiTabs.vue +25 -12
  172. package/vue/UiToast.vue +1 -1
  173. package/vue/UiToastRegion.vue +1 -1
  174. package/vue/UiTooltip.vue +12 -12
  175. package/vue/UiTopBar.vue +1 -1
  176. package/vue/UiTreeItem.vue +2 -1
  177. package/vue/chunks/UiAvatarGroup.js +1 -1
  178. package/vue/chunks/UiBadge.js +1 -1
  179. package/vue/chunks/UiBreadcrumbItem.js +1 -1
  180. package/vue/chunks/UiButton.js +28 -23
  181. package/vue/chunks/UiCallout.js +1 -1
  182. package/vue/chunks/UiCard.js +1 -1
  183. package/vue/chunks/UiCheckbox.js +1 -1
  184. package/vue/chunks/UiCombobox.js +80 -77
  185. package/vue/chunks/UiContextMenu.js +1 -1
  186. package/vue/chunks/UiDescriptionItem.js +1 -1
  187. package/vue/chunks/UiDescriptionList.js +1 -1
  188. package/vue/chunks/UiDialog.js +7 -4
  189. package/vue/chunks/UiDisclosure.js +1 -1
  190. package/vue/chunks/UiEditorInsertTableGrid.js +1 -1
  191. package/vue/chunks/UiEditorMentionMenu.js +1 -1
  192. package/vue/chunks/UiEditorSlashMenu.js +1 -1
  193. package/vue/chunks/UiEditorSlashMenuItem.js +1 -1
  194. package/vue/chunks/UiEditorTableContextMenu.js +1 -1
  195. package/vue/chunks/UiEditorTableToolbar.js +1 -1
  196. package/vue/chunks/UiEditorToolbar.js +1 -1
  197. package/vue/chunks/UiFloatingActionButton.js +1 -1
  198. package/vue/chunks/UiIconButton.js +13 -8
  199. package/vue/chunks/UiList.js +17 -5
  200. package/vue/chunks/UiListItem.js +1 -1
  201. package/vue/chunks/UiMenu.js +10 -2
  202. package/vue/chunks/UiMenuItem.js +1 -4
  203. package/vue/chunks/UiMeter.js +1 -1
  204. package/vue/chunks/UiNavItem.js +1 -1
  205. package/vue/chunks/UiPageHeader.js +1 -1
  206. package/vue/chunks/UiPopover.js +1 -1
  207. package/vue/chunks/UiRadio.js +1 -1
  208. package/vue/chunks/UiScrollArea.js +22 -3
  209. package/vue/chunks/UiSearchResultRow.js +1 -1
  210. package/vue/chunks/UiSearchShell.js +2 -2
  211. package/vue/chunks/UiSection.js +1 -1
  212. package/vue/chunks/UiSeparator.js +1 -1
  213. package/vue/chunks/UiSidebar.js +1 -1
  214. package/vue/chunks/UiSpinner.js +7 -5
  215. package/vue/chunks/UiStatusMessage.js +1 -1
  216. package/vue/chunks/UiSwitch.js +1 -1
  217. package/vue/chunks/UiTable.js +1 -1
  218. package/vue/chunks/UiTabs.js +1 -1
  219. package/vue/chunks/UiToast.js +1 -1
  220. package/vue/chunks/UiToastRegion.js +1 -1
  221. package/vue/chunks/UiTooltip.js +1 -1
  222. package/vue/chunks/UiTopBar.js +1 -1
  223. package/vue/chunks/UiTreeItem.js +1 -1
  224. package/vue/components.css +1217 -1032
  225. package/vue/index.js +1 -1
@@ -37,10 +37,9 @@
37
37
  --_ui-avatar-group-overlap-shadow: var(--ui-avatar-group-overlap-shadow);
38
38
  --_ui-avatar-group-edge-ring: var(--ui-avatar-group-edge-ring);
39
39
 
40
- /* Each avatar is ringed in the surface colour, so where one overlaps the next it reads as a
41
- * whole circle set on top, not a shape cut into the one beneath. */
42
- --_overlap-shadow: 0 0 0 0 transparent;
43
- --_edge-ring: 0 0 0 2px var(--ui-surface);
40
+ /* A one-pixel shadow on the leading edge separates overlapping circles without cutting
41
+ * a white gap out of the avatar underneath. */
42
+ --_overlap-shadow: -1px 0 0 0 rgb(0 0 0 / 0.22);
44
43
 
45
44
  display: flex;
46
45
  align-items: center;
@@ -59,6 +58,10 @@
59
58
  margin-inline-start: 0;
60
59
  }
61
60
 
61
+ :host:dir(rtl) {
62
+ --_overlap-shadow: 1px 0 0 0 rgb(0 0 0 / 0.22);
63
+ }
64
+
62
65
  .overflow {
63
66
  margin-inline-start: -0.5rem;
64
67
  display: inline-flex;
@@ -72,7 +75,7 @@
72
75
  color: var(--ui-text-primary);
73
76
  box-shadow:
74
77
  var(--_ui-avatar-group-overlap-shadow, var(--_overlap-shadow)),
75
- var(--_ui-avatar-group-edge-ring, var(--_edge-ring));
78
+ var(--_ui-avatar-group-edge-ring, 0 0 0 0 transparent);
76
79
  font: inherit;
77
80
  font-size: var(--ui-font-size-xs);
78
81
  font-weight: 600;
@@ -11,10 +11,10 @@ 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
- // Later avatars cast a crisp shadow back onto the one they overlap; the first overlaps nothing.
14
+ // Later avatars darken only their leading edge; the first overlaps nothing.
15
15
  avatar.style.boxShadow = index === 0
16
- ? "var(--ui-avatar-group-edge-ring, var(--_edge-ring))"
17
- : "var(--ui-avatar-group-overlap-shadow, var(--_overlap-shadow)), var(--ui-avatar-group-edge-ring, var(--_edge-ring))";
16
+ ? "var(--ui-avatar-group-edge-ring, none)"
17
+ : "var(--ui-avatar-group-overlap-shadow, var(--_overlap-shadow)), var(--ui-avatar-group-edge-ring, 0 0 0 0 transparent)";
18
18
  });
19
19
  const authoredVisible = Math.min(avatars.length, visible);
20
20
  const suppliedTotal = Number(host.state.total);
@@ -135,27 +135,32 @@
135
135
  }
136
136
 
137
137
  :host-state([variant="subtle"][tone="accent"]) {
138
- --_badge-surface: var(--ui-accent-soft);
138
+ --_badge-surface: color-mix(in srgb, var(--ui-accent-solid) 22%, var(--ui-surface-default));
139
+ --_badge-border: color-mix(in srgb, var(--ui-accent-solid) 28%, var(--ui-surface-default));
139
140
  --_badge-text: var(--ui-accent-subtle-text, var(--ui-accent-solid));
140
141
  }
141
142
 
142
143
  :host-state([variant="subtle"][tone="danger"]) {
143
- --_badge-surface: var(--ui-danger-soft);
144
+ --_badge-surface: color-mix(in srgb, var(--ui-danger-solid) 22%, var(--ui-surface-default));
145
+ --_badge-border: color-mix(in srgb, var(--ui-danger-solid) 28%, var(--ui-surface-default));
144
146
  --_badge-text: var(--ui-danger-subtle-text, var(--ui-danger-solid));
145
147
  }
146
148
 
147
149
  :host-state([variant="subtle"][tone="success"]) {
148
- --_badge-surface: var(--ui-success-soft);
150
+ --_badge-surface: color-mix(in srgb, var(--ui-success-solid) 22%, var(--ui-surface-default));
151
+ --_badge-border: color-mix(in srgb, var(--ui-success-solid) 28%, var(--ui-surface-default));
149
152
  --_badge-text: var(--ui-success-subtle-text, var(--ui-success-solid));
150
153
  }
151
154
 
152
155
  :host-state([variant="subtle"][tone="warning"]) {
153
- --_badge-surface: var(--ui-warning-soft);
156
+ --_badge-surface: color-mix(in srgb, var(--ui-warning-solid) 22%, var(--ui-surface-default));
157
+ --_badge-border: color-mix(in srgb, var(--ui-warning-solid) 28%, var(--ui-surface-default));
154
158
  --_badge-text: var(--ui-warning-subtle-text, var(--ui-warning-solid));
155
159
  }
156
160
 
157
161
  :host-state([variant="subtle"][tone="info"]) {
158
- --_badge-surface: var(--ui-info-soft);
162
+ --_badge-surface: color-mix(in srgb, var(--ui-info-solid) 22%, var(--ui-surface-default));
163
+ --_badge-border: color-mix(in srgb, var(--ui-info-solid) 28%, var(--ui-surface-default));
159
164
  --_badge-text: var(--ui-info-subtle-text, var(--ui-info-solid));
160
165
  }
161
166
 
@@ -15,12 +15,13 @@
15
15
  align-items: center;
16
16
  gap: var(--ui-space-2);
17
17
  min-inline-size: 0;
18
+ margin: 0;
18
19
  }
19
20
 
20
21
  .separator {
21
22
  display: inline-flex;
22
23
  color: var(--ui-text-muted);
23
- font-size: var(--ui-font-size-xs);
24
+ font-size: var(--ui-font-size-sm);
24
25
  }
25
26
 
26
27
  :host:first-child .separator {
@@ -11,11 +11,12 @@
11
11
  >Which colour the button speaks in. It applies to every variant, so an outline, a ghost, and a solid in the same tone are the same action at three volumes:
12
12
  tone="danger" variant="outline" is a destructive secondary action. neutral is the quiet one, for a button that should not claim a colour.</prop>
13
13
  <prop name="disabled" type="boolean" default="false"
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
15
- states aria-disabled="true" instead, because a link has no native disabled state.</prop>
16
- <prop name="pending" type="boolean" default="false"
14
+ >Makes the button unavailable: it cannot be pressed, uses a flat neutral palette, and loses its raised look. A disabled link (as="a" with href) drops its
15
+ href and states aria-disabled="true" instead, because a link has no native disabled state.</prop>
16
+ <prop name="loading" type="boolean" default="false"
17
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
18
  activation or form submission until cleared.</prop>
19
+ <prop name="pending" type="boolean" default="false">Deprecated alias for loading.</prop>
19
20
  <prop name="align" type="center | start" default="center"
20
21
  >Content alignment. start lays the content out like a list option: from the start edge, with start-aligned text.</prop>
21
22
  <prop name="stretch" type="boolean" default="false">Fills the inline size of its container.</prop>
@@ -34,18 +35,24 @@
34
35
  <template $match>
35
36
  <a
36
37
  $when="as = 'a'"
37
- :href="{ true: null, false: href }[format('%s', disabled or pending)]"
38
+ :href="{ true: null, false: href }[format('%s', disabled or loading or pending)]"
38
39
  :target="target"
39
40
  :rel="rel"
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)]"
41
+ :aria-disabled="{ true: 'true', false: null }[format('%s', disabled or loading or pending)]"
42
+ :aria-busy="loading or pending"
43
+ :tabindex="{ true: '0', false: null }[format('%s', (loading or pending) and not disabled)]"
43
44
  >
44
- <ui-spinner $if="pending" size="sm" class="pending-spinner"></ui-spinner>
45
+ <span $if="loading or pending" class="spinner-wrap"><ui-spinner size="sm"></ui-spinner></span>
45
46
  <slot></slot>
46
47
  </a>
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>
48
+ <button
49
+ $else
50
+ :type="type"
51
+ :disabled="disabled"
52
+ :aria-disabled="{ true: 'true', false: null }[format('%s', loading or pending)]"
53
+ :aria-busy="loading or pending"
54
+ >
55
+ <span $if="loading or pending" class="spinner-wrap"><ui-spinner size="sm"></ui-spinner></span>
49
56
  <slot></slot>
50
57
  </button>
51
58
  </template>
@@ -177,12 +184,15 @@
177
184
  background-color var(--ui-motion-fast) var(--ui-motion-ease),
178
185
  border-color var(--ui-motion-fast) var(--ui-motion-ease),
179
186
  color var(--ui-motion-fast) var(--ui-motion-ease),
180
- box-shadow var(--ui-motion-fast) var(--ui-motion-ease);
187
+ box-shadow var(--ui-motion-base) var(--ui-motion-ease);
181
188
  margin: 0;
182
189
  }
183
190
 
184
- .pending-spinner {
191
+ .spinner-wrap {
192
+ display: inline-flex;
193
+ flex: none;
185
194
  margin-inline-end: var(--ui-space-2);
195
+ vertical-align: middle;
186
196
  }
187
197
 
188
198
  @supports (text-box-trim: trim-both) {
@@ -312,12 +322,12 @@
312
322
  var(--ui-focus-halo);
313
323
  }
314
324
 
315
- /* Disabled keeps the shape and washes out the rest: every variant keeps its own structure, so a
316
- disabled outline is still an outline and a disabled ghost still has no edge. One filter fades
317
- fill, border, and text together (--ui-disabled-filter), and the theme decides which way is
318
- toward the page. The highlight and shadow go, because nothing that cannot be pressed should
319
- look raised. */
320
- :host:is(:disabled, [aria-disabled="true"]) {
325
+ /* A loading action stays visually active; only an explicitly disabled action takes the
326
+ neutral disabled palette. Its flat surface and muted ink make the state unambiguous. */
327
+ :host:is(:disabled, [aria-disabled="true"]:not([aria-busy="true"])) {
328
+ border-color: var(--ui-disabled-border);
329
+ background-color: var(--ui-disabled-surface);
330
+ color: var(--ui-disabled-text);
321
331
  filter: var(--ui-button-disabled-filter, var(--ui-disabled-filter));
322
332
  opacity: var(--ui-button-disabled-opacity, 1);
323
333
  cursor: not-allowed;
@@ -325,16 +335,13 @@
325
335
  box-shadow: none;
326
336
  }
327
337
 
328
- :host-state([pending]):focus-visible {
338
+ :is(:host-state([loading]), :host-state([pending])):focus-visible {
329
339
  box-shadow: var(--ui-focus-halo);
330
340
  }
331
341
 
332
- /* Ghost has no hover to fall back on, so disabled is the one state it has to state outright:
333
- it takes a surface rather than reading as plain text. The surface is a wash of the faded tone,
334
- the way hover washes the live one, so it stays light in every tone. An opaque mix toward the
335
- hue came out a mid-grey slab for neutral, whose hue is the ink: louder than the enabled button. */
336
- :host-state([variant="ghost"]):is(:disabled, [aria-disabled="true"]) {
337
- background-color: oklch(from var(--_tone) l c h / 14%);
342
+ /* Ghost keeps no border, but gains the same neutral surface as other disabled buttons. */
343
+ :host-state([variant="ghost"]):is(:disabled, [aria-disabled="true"]:not([aria-busy="true"])) {
344
+ background-color: var(--ui-disabled-surface);
338
345
  border-color: transparent;
339
346
  }
340
347
 
@@ -393,7 +400,7 @@
393
400
  transform: none;
394
401
  }
395
402
 
396
- :host-state([variant="link"]):is(:disabled, [aria-disabled="true"]) {
403
+ :host-state([variant="link"]):is(:disabled, [aria-disabled="true"]:not([aria-busy="true"])) {
397
404
  background: none;
398
405
  border-color: transparent;
399
406
  }
@@ -1,7 +1,7 @@
1
- // Pending actions retain focus but cannot activate, navigate, submit, or notify click consumers.
1
+ // Loading actions retain focus but cannot activate, navigate, submit, or notify click consumers.
2
2
  export default function controller(host) {
3
3
  const onClick = (event) => {
4
- if (!host.state.pending) return;
4
+ if (!host.state.loading && !host.state.pending) return;
5
5
  event.preventDefault();
6
6
  event.stopImmediatePropagation();
7
7
  };
@@ -103,7 +103,9 @@
103
103
  gap: var(--ui-callout-gap, calc(var(--ui-space-2) * 0.75));
104
104
  align-items: start;
105
105
  max-width: 100%;
106
- padding: var(--ui-callout-padding-block, var(--ui-space-2)) var(--ui-callout-padding-inline, var(--ui-space-3));
106
+ padding-block: var(--ui-callout-padding-block, var(--ui-space-2));
107
+ padding-inline-start: var(--ui-callout-padding-inline, var(--ui-space-2));
108
+ padding-inline-end: var(--ui-callout-padding-inline, var(--ui-space-3));
107
109
  border: var(--ui-callout-border-width, 1px) solid color-mix(in srgb, var(--ui-callout-border, var(--_callout-border)) 35%, transparent);
108
110
  border-inline-start: var(--ui-callout-leading-border-width, 4px) solid var(--ui-callout-border, var(--_callout-border));
109
111
  border-radius: var(--ui-radius-md);
@@ -5,7 +5,7 @@
5
5
  elevated floats above the page with a shadow, for a view's one card, such as sign-in.</prop>
6
6
  <prop name="padding" type="sm | md | lg" default="md"
7
7
  >The space inside the card: sm is snug (0.75rem), md is the default (1.5rem), and lg is roomy (2rem).</prop>
8
- <prop name="tone" type="neutral | danger" default="neutral">danger draws an outlined card's border in the danger colour.</prop>
8
+ <prop name="tone" type="neutral | danger" default="neutral">danger adds a soft danger surface and a strong leading edge for consequential content.</prop>
9
9
  <prop name="as" type="div | section | article | aside" default="div"
10
10
  >The native element the card renders. Choose the one that names the content: a section of a page, a self-contained article, or an aside.</prop>
11
11
  </defs>
@@ -25,7 +25,7 @@
25
25
  box-sizing: border-box;
26
26
  min-inline-size: 0;
27
27
  padding: var(--ui-space-5);
28
- border: 1px solid var(--ui-border-default);
28
+ border: 1px solid var(--ui-border-strong);
29
29
  border-radius: var(--ui-radius-lg);
30
30
  background: var(--ui-surface-raised);
31
31
  color: var(--ui-text-primary);
@@ -51,7 +51,9 @@
51
51
  }
52
52
 
53
53
  :host-state([tone="danger"]) {
54
- border-color: var(--ui-danger);
54
+ border-color: color-mix(in srgb, var(--ui-danger) 45%, var(--ui-border-default));
55
+ border-inline-start: 4px solid var(--ui-danger);
56
+ background: var(--ui-danger-soft);
55
57
  }
56
58
 
57
59
  /* A component that sets its display still honours the hidden attribute on its root. */
@@ -98,10 +98,10 @@
98
98
  inline-size: var(--_ui-checkbox-size, 1.125rem);
99
99
  block-size: var(--_ui-checkbox-size, 1.125rem);
100
100
  margin: 0.125em 0 0;
101
- border: 1px solid var(--ui-control-border-hover, var(--ui-border-strong));
101
+ border: 1px solid var(--ui-control-border, var(--ui-border-strong));
102
102
  border-radius: var(--ui-radius-sm);
103
103
  background: var(--ui-control-surface, var(--ui-surface-default));
104
- box-shadow: inset 0 1px 2px color-mix(in srgb, var(--ui-text-primary) 8%, transparent);
104
+ box-shadow: var(--ui-control-inset);
105
105
  cursor: pointer;
106
106
  transition:
107
107
  background-color var(--ui-motion-fast) var(--ui-motion-ease),
@@ -113,28 +113,30 @@
113
113
  content: "";
114
114
  position: absolute;
115
115
  opacity: 0;
116
+ transition: opacity 120ms var(--ui-motion-ease);
116
117
  }
117
118
 
118
119
  input:checked,
119
120
  input:indeterminate {
120
121
  border-color: var(--ui-accent-solid);
121
122
  background: var(--ui-accent-solid);
122
- box-shadow: inset 0 1px 2px color-mix(in srgb, #000 16%, transparent);
123
+ box-shadow: var(--ui-control-inset);
123
124
  }
124
125
 
125
126
  /* Centre the tick's box, then lift it: a rotated L's ink sits ~0.11rem below its box centre. */
126
127
  input:checked::after {
127
128
  inset: 0;
128
129
  margin: auto;
129
- inline-size: 0.32rem;
130
- block-size: 0.58rem;
130
+ inline-size: 0.28rem;
131
+ block-size: 0.5rem;
131
132
  /* Longhands, not `border: solid var(...)`: lowering drops a shorthand that omits a
132
133
  component, which left the tick with no style and no colour. */
133
134
  border-style: solid;
134
135
  border-color: var(--ui-text-on-accent);
135
136
  border-width: 0 0.125rem 0.125rem 0;
136
137
  opacity: 1;
137
- transform: translateY(-0.11rem) rotate(45deg);
138
+ transform: translate(0.07rem, -0.09rem) rotate(45deg);
139
+ animation: ui-checkbox-check 180ms var(--ui-motion-ease-out, cubic-bezier(0.16, 1, 0.3, 1)) both;
138
140
  }
139
141
 
140
142
  input:indeterminate::after {
@@ -146,6 +148,28 @@
146
148
  transform: translateY(-50%);
147
149
  }
148
150
 
151
+ @keyframes ui-checkbox-check {
152
+ from {
153
+ opacity: 0;
154
+ transform: translate(0.07rem, -0.09rem) rotate(45deg) scale(0.55);
155
+ }
156
+ 65% {
157
+ opacity: 1;
158
+ transform: translate(0.07rem, -0.09rem) rotate(45deg) scale(1.12);
159
+ }
160
+ to {
161
+ opacity: 1;
162
+ transform: translate(0.07rem, -0.09rem) rotate(45deg) scale(1);
163
+ }
164
+ }
165
+
166
+ @media (prefers-reduced-motion: reduce) {
167
+ input::after {
168
+ transition: none;
169
+ animation: none;
170
+ }
171
+ }
172
+
149
173
  input:hover:enabled {
150
174
  border-color: var(--ui-accent-solid);
151
175
  }
@@ -163,9 +187,13 @@
163
187
  color: var(--ui-disabled-text);
164
188
  cursor: not-allowed;
165
189
  }
190
+ :host-state([disabled]) .description {
191
+ color: var(--ui-disabled-text);
192
+ }
166
193
  :host-state([disabled]) input {
167
- border-color: var(--ui-border-default);
168
- background: var(--ui-control-surface-disabled, var(--ui-surface-muted));
194
+ border-color: var(--ui-disabled-border);
195
+ background: var(--ui-disabled-surface);
196
+ box-shadow: none;
169
197
  cursor: not-allowed;
170
198
  }
171
199
 
@@ -13,16 +13,17 @@
13
13
  when none is chosen. A disabled combobox sends nothing.</prop>
14
14
  <prop name="value" type="string | null"
15
15
  >The selected value in single mode, which a named combobox submits with its form. The field shows the matching option's label, and a form reset returns to
16
- this value.</prop>
16
+ this value. If omitted, the first authored option with selected sets the initial value.</prop>
17
17
  <prop
18
18
  name="items"
19
19
  type="list(object({ id: string, value: string, label: string, group?: string, disabled?: boolean, description?: string, tag?: object({ label: string, tone?: neutral | accent | info | success | warning | danger, color?: string }) }))"
20
- >The selected items in multiple mode, as JSON; a named combobox submits each one's value. A form reset returns to these items, or to none when unset.</prop>
20
+ >The selected items in multiple mode, as JSON; a named combobox submits each one's value. A form reset returns to these items, or to authored selected
21
+ options when unset.</prop>
21
22
  <prop name="selectedValues" type="list(string)"
22
- >The selected option values in multiple mode, when the consumer controls the selection. Once set, the consumer owns it: each value shows as a badge with its
23
- authored option's label, adding or removing one reports selected-values-change with the new list, and the badges follow selectedValues, not the user,
24
- unless it is updated to match. Setting it reports nothing. It takes precedence over items; leave it unset to let the combobox keep its own
25
- selection.</prop>
23
+ >The selected option values in multiple mode, when the consumer controls the selection. Without it or items, authored options with selected set the initial
24
+ selection. Once set, the consumer owns it: each value shows as a badge with its authored option's label, adding or removing one reports
25
+ selected-values-change with the new list, and the badges follow selectedValues, not the user, unless it is updated to match. Setting it reports nothing.
26
+ It takes precedence over items; leave it unset to let the combobox keep its own selection.</prop>
26
27
  <prop name="multiple" type="boolean" default="false"
27
28
  >Lets the user choose several options. Each choice becomes a removable badge before the input, the list stays open between picks, and choosing a checked
28
29
  option removes it. value-change then reports the whole list of items, and selected-values-change the list of their values; use selectedValues or items,
@@ -66,8 +67,8 @@
66
67
  >Adds a chevron button that opens and closes the full, unfiltered list of options, for users who would rather browse than type. ArrowDown opens the same
67
68
  list with or without it.</prop>
68
69
  <prop name="clearable" type="boolean" default="false"
69
- >Adds a clear button to the field that empties the input and clears the selection, reported as value-change with kind "clear". In multiple mode it clears
70
- only the typed text; chosen items stay.</prop>
70
+ >Adds a clear button to the field that empties the input and clears the selection, reported as value-change with kind "clear". It is disabled when there is
71
+ nothing to clear. In multiple mode it clears only the typed text; chosen items stay.</prop>
71
72
  <prop name="help" type="string" default=""
72
73
  >Help text for the field. When set, a help button appears beside the field and opens a tooltip with this text; while it is open, the input is described by
73
74
  it.</prop>
@@ -144,42 +145,50 @@
144
145
  *</template></label>
145
146
  <div class="control">
146
147
  <div class="field" $ref="field">
147
- <slot name="start"></slot>
148
- <template $if="multiple">
149
- <button
150
- $each="item, index of internalItems"
151
- $key="item.id"
152
- class="item"
153
- type="button"
154
- tabindex="-1"
155
- :data-index="index"
156
- :disabled="disabled or readonly or item.disabled"
157
- :aria-label="format('%s, press Delete or Backspace to remove', item.label)"
148
+ <div class="entry">
149
+ <slot name="start"></slot>
150
+ <template $if="multiple">
151
+ <button
152
+ $each="item, index of internalItems"
153
+ $key="item.id"
154
+ class="item"
155
+ type="button"
156
+ tabindex="-1"
157
+ :data-index="index"
158
+ :disabled="disabled or readonly or item.disabled"
159
+ :aria-label="format('%s, press Delete or Backspace to remove', item.label)"
160
+ >
161
+ <slot :name="format('item-%s', item.id)"><ui-badge $value="item.label"></ui-badge></slot>
162
+ </button>
163
+ </template>
164
+ <input
165
+ $ref="input"
166
+ :id="format('%s-input', uid)"
167
+ role="combobox"
168
+ aria-autocomplete="list"
169
+ autocomplete="off"
170
+ :aria-controls="format('%s-listbox', uid)"
171
+ :aria-expanded="expanded"
172
+ :aria-invalid="invalid or validationStatus = 'error'"
173
+ :aria-busy="loading or validationStatus = 'pending'"
174
+ .value="display"
175
+ :placeholder="placeholder"
176
+ :disabled="disabled"
177
+ :readonly="readonly"
178
+ :required="required and not (multiple and internalItems[0])"
158
179
  >
159
- <slot :name="format('item-%s', item.id)"><ui-badge $value="item.label"></ui-badge></slot>
160
- </button>
161
- </template>
162
- <input
163
- $ref="input"
164
- :id="format('%s-input', uid)"
165
- role="combobox"
166
- aria-autocomplete="list"
167
- autocomplete="off"
168
- :aria-controls="format('%s-listbox', uid)"
169
- :aria-expanded="expanded"
170
- :aria-invalid="invalid or validationStatus = 'error'"
171
- :aria-busy="loading or validationStatus = 'pending'"
172
- .value="display"
173
- :placeholder="placeholder"
174
- :disabled="disabled"
175
- :readonly="readonly"
176
- :required="required and not (multiple and internalItems[0])"
177
- >
180
+ </div>
178
181
  <input $if="name and not multiple" type="hidden" :name="name" :value="submitted" :disabled="disabled">
179
182
  <template $if="name and multiple">
180
183
  <input $each="item of internalItems" $key="item.id" type="hidden" :name="name" :value="item.value" :disabled="disabled">
181
184
  </template>
182
- <button $if="clearable" type="button" data-combobox-action="clear" aria-label="Clear" :disabled="disabled or readonly">
185
+ <button
186
+ $if="clearable"
187
+ type="button"
188
+ data-combobox-action="clear"
189
+ aria-label="Clear"
190
+ :disabled="disabled or readonly or (not raw and (multiple or selected = null))"
191
+ >
183
192
  <svg class="affordance-icon" aria-hidden="true" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round">
184
193
  <path d="M7 7l10 10M17 7 7 17"></path>
185
194
  </svg>
@@ -401,6 +410,11 @@
401
410
  var(--ui-control-inset),
402
411
  var(--ui-control-focus-halo-shadow);
403
412
  }
413
+ .entry {
414
+ display: flex;
415
+ flex: 1 1 auto;
416
+ min-inline-size: 0;
417
+ }
404
418
  /* A ghost icon button: the affordance is the mark, not a box around it. */
405
419
  .help {
406
420
  flex: none;
@@ -520,7 +534,7 @@
520
534
  overflow: auto;
521
535
  margin: 0;
522
536
  padding: var(--ui-space-1);
523
- border: 1px solid var(--_ui-combobox-popup-border, var(--ui-overlay-border, var(--ui-border-default)));
537
+ border: 1px solid var(--_ui-combobox-popup-border, var(--ui-overlay-border, var(--ui-border-strong)));
524
538
  border-radius: var(--_ui-combobox-popup-radius, var(--ui-overlay-radius, var(--ui-radius-md)));
525
539
  background: var(--_ui-combobox-popup-surface, var(--ui-overlay-surface, var(--ui-surface-elevated)));
526
540
  color: var(--ui-text-primary);
@@ -544,6 +558,10 @@
544
558
  color: var(--ui-disabled-text);
545
559
  cursor: not-allowed;
546
560
  }
561
+ .option[aria-disabled="true"] .primary,
562
+ .option[aria-disabled="true"] .secondary {
563
+ color: var(--ui-disabled-text);
564
+ }
547
565
  .primary {
548
566
  display: block;
549
567
  color: var(--_ui-combobox-option-text, var(--ui-text-primary));
@@ -640,21 +658,29 @@
640
658
  );
641
659
  }
642
660
 
643
- /* Multiple mode: chips wrap before the cursor on one flexible field line. */
661
+ /* Multiple mode keeps one control height. Chips and the input scroll within the entry,
662
+ while the clear and disclosure controls remain fixed at the end of the field. */
644
663
  :host-state([multiple]) .field {
645
- flex-wrap: wrap;
646
664
  align-items: center;
647
- gap: var(--ui-space-1);
648
665
  min-block-size: var(--_ui-combobox-item-min-block-size, 2.5rem);
649
666
  padding: var(--ui-space-1);
650
667
  }
668
+ :host-state([multiple]) .entry {
669
+ align-items: center;
670
+ gap: var(--ui-space-1);
671
+ overflow-x: auto;
672
+ scrollbar-width: none;
673
+ }
674
+ :host-state([multiple]) .entry::-webkit-scrollbar {
675
+ display: none;
676
+ }
651
677
  :host-state([multiple]) input {
652
678
  flex: 1 0 var(--_ui-combobox-item-input-min-inline-size, 5rem);
653
679
  width: auto;
654
680
  min-block-size: 1.75rem;
655
681
  padding: var(--ui-space-0-5) var(--ui-space-1);
656
682
  }
657
- /* A small multiple field starts at the small control height; chosen items still wrap it taller. */
683
+ /* Size variants keep their control height as selections are added. */
658
684
  :host-state([multiple]):host-state([size="sm"]) .field {
659
685
  min-block-size: 0;
660
686
  padding: var(--ui-space-0-5);
@@ -673,6 +699,7 @@
673
699
  .item {
674
700
  appearance: none;
675
701
  display: inline-flex;
702
+ flex: none;
676
703
  min-inline-size: 0;
677
704
  max-inline-size: min(100%, var(--_ui-combobox-item-item-max-inline-size, 12rem));
678
705
  overflow: hidden;
@@ -689,7 +716,7 @@
689
716
  outline-offset: 1px;
690
717
  }
691
718
  .item:disabled {
692
- opacity: 0.6;
719
+ color: var(--ui-disabled-text);
693
720
  }
694
721
 
695
722
  /* A component that sets its display still honours the hidden attribute on its root. */
@@ -58,6 +58,10 @@ export default function controller(host) {
58
58
  // "none": the consumer already narrowed the options (a server search), so every one is listed.
59
59
  filterByLabel: host.state.filter !== "none",
60
60
  });
61
+ const authoredDefaults = () => {
62
+ const values = new Set(Array.from(authored.querySelectorAll("option[selected]"), (option) => option.value));
63
+ return config().options.filter((option) => values.has(option.value));
64
+ };
61
65
  // Selected items work uncontrolled: the component keeps them and reports every change. A consumer
62
66
  // that owns `items` stays in charge, because the prop resyncs whatever it sets. A consumer that sets
63
67
  // `selectedValues` controls the selection: the chips follow it, and a user's change is only reported.
@@ -466,7 +470,7 @@ export default function controller(host) {
466
470
  host.state.selected = null;
467
471
  host.state.raw = host.state.query ?? "";
468
472
  } else {
469
- host.state.selected = host.state.value ?? null;
473
+ host.state.selected = host.state.value !== undefined ? host.state.value : authoredDefaults()[0]?.value ?? null;
470
474
  host.state.raw = host.state.query ?? "";
471
475
  if (host.state.query === undefined && !host.state.raw && host.state.selected !== null) {
472
476
  host.state.raw = config().options?.find((row) => row.value === host.state.selected)?.label ?? host.state.selected;
@@ -499,7 +503,7 @@ export default function controller(host) {
499
503
  const stopReset = afterFormReset(input, () => {
500
504
  close();
501
505
  lastSelection = null;
502
- host.state.internalItems = controlledValues() ? itemsForValues() : Array.isArray(host.state.items) ? host.state.items : [];
506
+ host.state.internalItems = controlledValues() ? itemsForValues() : Array.isArray(host.state.items) ? host.state.items : authoredDefaults();
503
507
  applyDefaults();
504
508
  input.value = host.state.display;
505
509
  resetValidation();
@@ -522,6 +526,7 @@ export default function controller(host) {
522
526
  let lastSelectedValues = host.state.selectedValues;
523
527
  if (controlledValues()) host.state.internalItems = itemsForValues();
524
528
  else if (Array.isArray(lastItems)) host.state.internalItems = lastItems;
529
+ else host.state.internalItems = authoredDefaults();
525
530
  const stop = host.effect(() => {
526
531
  // A consumer that sets `items` owns them; otherwise the component keeps its own. selectedValues wins.
527
532
  if (host.state.items !== lastItems) {
@@ -57,7 +57,7 @@
57
57
  box-sizing: border-box;
58
58
  padding: var(--ui-space-1);
59
59
  background: var(--_ui-context-menu-surface, var(--ui-overlay-surface, var(--ui-surface-elevated)));
60
- border: 1px solid var(--_ui-context-menu-border, var(--ui-overlay-border, var(--ui-border-default)));
60
+ border: 1px solid var(--_ui-context-menu-border, var(--ui-overlay-border, var(--ui-border-strong)));
61
61
  border-radius: var(--_ui-context-menu-radius, var(--ui-overlay-radius, var(--ui-radius-lg)));
62
62
  box-shadow: var(--_ui-context-menu-shadow, var(--ui-overlay-shadow, var(--ui-elevation-overlay, var(--ui-shadow-lg))));
63
63
  outline: 0;
@@ -78,7 +78,7 @@
78
78
  block-size: 0;
79
79
  margin: var(--ui-space-1) 0;
80
80
  border: 0;
81
- border-block-start: 1px solid var(--ui-border-default);
81
+ border-block-start: 1px solid var(--ui-border-divider);
82
82
  }
83
83
 
84
84
  .menu:popover-open {
@@ -19,7 +19,7 @@
19
19
  padding: var(--_ui-description-padding, 0);
20
20
  border-width: var(--_ui-description-border-width, 0);
21
21
  border-style: solid;
22
- border-color: var(--ui-border-default);
22
+ border-color: var(--_ui-description-border-color, var(--ui-border-default));
23
23
  border-radius: var(--ui-radius-md);
24
24
  font-size: var(--ui-font-size-sm);
25
25
  }