@threadlabs/looma 0.6.4 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (259) hide show
  1. package/components/ui-button/ui-button.html +103 -81
  2. package/components/ui-checkbox/ui-checkbox.html +4 -1
  3. package/components/ui-combobox/ui-combobox.html +40 -3
  4. package/components/ui-combobox/ui-combobox.js +27 -5
  5. package/components/ui-select/ui-select.html +3 -5
  6. package/dist/index.js +1 -1
  7. package/package.json +2 -2
  8. package/styles/ui-affordance-scope.css +1 -1
  9. package/styles/ui-avatar-group.css +1 -1
  10. package/styles/ui-avatar.css +1 -1
  11. package/styles/ui-badge.css +1 -1
  12. package/styles/ui-button.css +112 -80
  13. package/styles/ui-callout.css +1 -1
  14. package/styles/ui-checkbox.css +5 -2
  15. package/styles/ui-cluster.css +1 -1
  16. package/styles/ui-combobox.css +38 -1
  17. package/styles/ui-container.css +1 -1
  18. package/styles/ui-context-menu.css +1 -1
  19. package/styles/ui-dialog.css +1 -1
  20. package/styles/ui-disclosure.css +1 -1
  21. package/styles/ui-editable.css +1 -1
  22. package/styles/ui-editor-insert-table-grid.css +1 -1
  23. package/styles/ui-editor-mention-menu.css +1 -1
  24. package/styles/ui-editor-slash-menu.css +1 -1
  25. package/styles/ui-editor-table-context-menu.css +1 -1
  26. package/styles/ui-editor-table-overlay.css +1 -1
  27. package/styles/ui-editor-table-toolbar.css +1 -1
  28. package/styles/ui-editor-toolbar.css +1 -1
  29. package/styles/ui-floating-action-button.css +1 -1
  30. package/styles/ui-form-field.css +1 -1
  31. package/styles/ui-grid.css +1 -1
  32. package/styles/ui-icon-button.css +1 -1
  33. package/styles/ui-icon.css +1 -1
  34. package/styles/ui-input.css +1 -1
  35. package/styles/ui-menu-item.css +1 -1
  36. package/styles/ui-menu.css +1 -1
  37. package/styles/ui-popover.css +1 -1
  38. package/styles/ui-radio-group.css +1 -1
  39. package/styles/ui-radio.css +1 -1
  40. package/styles/ui-reel.css +1 -1
  41. package/styles/ui-search-result-row.css +1 -1
  42. package/styles/ui-search-shell.css +1 -1
  43. package/styles/ui-select.css +4 -5
  44. package/styles/ui-separator.css +1 -1
  45. package/styles/ui-sidebar.css +1 -1
  46. package/styles/ui-stack.css +1 -1
  47. package/styles/ui-switch.css +1 -1
  48. package/styles/ui-switcher.css +1 -1
  49. package/styles/ui-tabs.css +1 -1
  50. package/styles/ui-textarea.css +1 -1
  51. package/styles/ui-toast-region.css +1 -1
  52. package/styles/ui-tooltip.css +1 -1
  53. package/styles/ui-top-bar.css +1 -1
  54. package/styles/ui-tree-item.css +1 -1
  55. package/styles/ui-tree.css +1 -1
  56. package/theme-dark.css +6 -30
  57. package/theme-high-contrast.css +30 -20
  58. package/theme-light.css +2 -16
  59. package/tokens.css +33 -24
  60. package/vanilla/UiAffordanceScope.d.ts +1 -1
  61. package/vanilla/UiAffordanceScope.js +1 -1
  62. package/vanilla/UiAvatar.d.ts +1 -1
  63. package/vanilla/UiAvatar.js +1 -1
  64. package/vanilla/UiAvatarGroup.d.ts +1 -1
  65. package/vanilla/UiAvatarGroup.js +1 -1
  66. package/vanilla/UiBadge.d.ts +1 -1
  67. package/vanilla/UiBadge.js +1 -1
  68. package/vanilla/UiButton.d.ts +2 -2
  69. package/vanilla/UiButton.js +2 -2
  70. package/vanilla/UiCallout.d.ts +1 -1
  71. package/vanilla/UiCallout.js +1 -1
  72. package/vanilla/UiCheckbox.d.ts +1 -1
  73. package/vanilla/UiCheckbox.js +1 -1
  74. package/vanilla/UiCluster.d.ts +1 -1
  75. package/vanilla/UiCluster.js +1 -1
  76. package/vanilla/UiCombobox.d.ts +1 -1
  77. package/vanilla/UiCombobox.js +2 -2
  78. package/vanilla/UiContainer.d.ts +1 -1
  79. package/vanilla/UiContainer.js +1 -1
  80. package/vanilla/UiContextMenu.d.ts +1 -1
  81. package/vanilla/UiContextMenu.js +1 -1
  82. package/vanilla/UiDialog.d.ts +1 -1
  83. package/vanilla/UiDialog.js +1 -1
  84. package/vanilla/UiDisclosure.d.ts +1 -1
  85. package/vanilla/UiDisclosure.js +1 -1
  86. package/vanilla/UiEditable.d.ts +1 -1
  87. package/vanilla/UiEditable.js +1 -1
  88. package/vanilla/UiEditorInsertTableGrid.d.ts +1 -1
  89. package/vanilla/UiEditorInsertTableGrid.js +1 -1
  90. package/vanilla/UiEditorMentionMenu.d.ts +1 -1
  91. package/vanilla/UiEditorMentionMenu.js +1 -1
  92. package/vanilla/UiEditorSlashMenu.d.ts +1 -1
  93. package/vanilla/UiEditorSlashMenu.js +1 -1
  94. package/vanilla/UiEditorTableContextMenu.d.ts +1 -1
  95. package/vanilla/UiEditorTableContextMenu.js +1 -1
  96. package/vanilla/UiEditorTableOverlay.d.ts +1 -1
  97. package/vanilla/UiEditorTableOverlay.js +1 -1
  98. package/vanilla/UiEditorTableToolbar.d.ts +1 -1
  99. package/vanilla/UiEditorTableToolbar.js +1 -1
  100. package/vanilla/UiEditorToolbar.d.ts +1 -1
  101. package/vanilla/UiEditorToolbar.js +1 -1
  102. package/vanilla/UiFloatingActionButton.d.ts +1 -1
  103. package/vanilla/UiFloatingActionButton.js +1 -1
  104. package/vanilla/UiFormField.d.ts +1 -1
  105. package/vanilla/UiFormField.js +1 -1
  106. package/vanilla/UiGrid.d.ts +1 -1
  107. package/vanilla/UiGrid.js +1 -1
  108. package/vanilla/UiIcon.d.ts +1 -1
  109. package/vanilla/UiIcon.js +1 -1
  110. package/vanilla/UiIconButton.d.ts +1 -1
  111. package/vanilla/UiIconButton.js +1 -1
  112. package/vanilla/UiInput.d.ts +1 -1
  113. package/vanilla/UiInput.js +1 -1
  114. package/vanilla/UiMenu.d.ts +1 -1
  115. package/vanilla/UiMenu.js +1 -1
  116. package/vanilla/UiMenuItem.d.ts +1 -1
  117. package/vanilla/UiMenuItem.js +1 -1
  118. package/vanilla/UiPopover.d.ts +1 -1
  119. package/vanilla/UiPopover.js +1 -1
  120. package/vanilla/UiRadio.d.ts +1 -1
  121. package/vanilla/UiRadio.js +1 -1
  122. package/vanilla/UiRadioGroup.d.ts +1 -1
  123. package/vanilla/UiRadioGroup.js +1 -1
  124. package/vanilla/UiReel.d.ts +1 -1
  125. package/vanilla/UiReel.js +1 -1
  126. package/vanilla/UiSearchResultRow.d.ts +1 -1
  127. package/vanilla/UiSearchResultRow.js +1 -1
  128. package/vanilla/UiSearchShell.d.ts +1 -1
  129. package/vanilla/UiSearchShell.js +1 -1
  130. package/vanilla/UiSelect.d.ts +1 -2
  131. package/vanilla/UiSelect.js +4 -7
  132. package/vanilla/UiSeparator.d.ts +1 -1
  133. package/vanilla/UiSeparator.js +1 -1
  134. package/vanilla/UiSidebar.d.ts +1 -1
  135. package/vanilla/UiSidebar.js +1 -1
  136. package/vanilla/UiStack.d.ts +1 -1
  137. package/vanilla/UiStack.js +1 -1
  138. package/vanilla/UiSwitch.d.ts +1 -1
  139. package/vanilla/UiSwitch.js +1 -1
  140. package/vanilla/UiSwitcher.d.ts +1 -1
  141. package/vanilla/UiSwitcher.js +1 -1
  142. package/vanilla/UiTabs.d.ts +1 -1
  143. package/vanilla/UiTabs.js +1 -1
  144. package/vanilla/UiTextarea.d.ts +1 -1
  145. package/vanilla/UiTextarea.js +1 -1
  146. package/vanilla/UiToastRegion.d.ts +1 -1
  147. package/vanilla/UiToastRegion.js +1 -1
  148. package/vanilla/UiTooltip.d.ts +1 -1
  149. package/vanilla/UiTooltip.js +1 -1
  150. package/vanilla/UiTopBar.d.ts +1 -1
  151. package/vanilla/UiTopBar.js +1 -1
  152. package/vanilla/UiTree.d.ts +1 -1
  153. package/vanilla/UiTree.js +1 -1
  154. package/vanilla/UiTreeItem.d.ts +1 -1
  155. package/vanilla/UiTreeItem.js +1 -1
  156. package/vue/UiAffordanceScope.vue +8 -84
  157. package/vue/UiAvatar.d.ts +1 -1
  158. package/vue/UiAvatar.vue +9 -106
  159. package/vue/UiAvatarGroup.vue +9 -96
  160. package/vue/UiBadge.vue +1 -1
  161. package/vue/UiButton.d.ts +2 -2
  162. package/vue/UiButton.vue +104 -97
  163. package/vue/UiCallout.vue +1 -1
  164. package/vue/UiCheckbox.vue +26 -124
  165. package/vue/UiCluster.vue +1 -1
  166. package/vue/UiCombobox.d.ts +1 -1
  167. package/vue/UiCombobox.vue +270 -479
  168. package/vue/UiContainer.vue +1 -1
  169. package/vue/UiContextMenu.vue +34 -141
  170. package/vue/UiDialog.vue +25 -126
  171. package/vue/UiDisclosure.vue +26 -138
  172. package/vue/UiEditable.vue +46 -158
  173. package/vue/UiEditorInsertTableGrid.vue +19 -138
  174. package/vue/UiEditorMentionMenu.vue +17 -124
  175. package/vue/UiEditorSlashMenu.vue +17 -109
  176. package/vue/UiEditorTableContextMenu.vue +34 -122
  177. package/vue/UiEditorTableOverlay.vue +61 -167
  178. package/vue/UiEditorTableToolbar.vue +34 -142
  179. package/vue/UiEditorToolbar.vue +1 -1
  180. package/vue/UiFloatingActionButton.vue +1 -1
  181. package/vue/UiFormField.vue +7 -79
  182. package/vue/UiGrid.vue +1 -1
  183. package/vue/UiIcon.vue +8 -84
  184. package/vue/UiIconButton.vue +7 -79
  185. package/vue/UiInput.vue +7 -79
  186. package/vue/UiMenu.vue +42 -127
  187. package/vue/UiMenuItem.vue +1 -1
  188. package/vue/UiPopover.vue +25 -121
  189. package/vue/UiRadio.vue +22 -123
  190. package/vue/UiRadioGroup.d.ts +1 -1
  191. package/vue/UiRadioGroup.vue +32 -119
  192. package/vue/UiReel.vue +1 -1
  193. package/vue/UiSearchResultRow.vue +1 -1
  194. package/vue/UiSearchShell.vue +25 -121
  195. package/vue/UiSelect.d.ts +0 -2
  196. package/vue/UiSelect.vue +9 -85
  197. package/vue/UiSeparator.vue +1 -1
  198. package/vue/UiSidebar.vue +32 -133
  199. package/vue/UiStack.vue +1 -1
  200. package/vue/UiSwitch.vue +22 -123
  201. package/vue/UiSwitcher.vue +1 -1
  202. package/vue/UiTabs.vue +24 -128
  203. package/vue/UiTextarea.vue +7 -79
  204. package/vue/UiToastRegion.vue +37 -126
  205. package/vue/UiTooltip.vue +25 -124
  206. package/vue/UiTopBar.vue +1 -1
  207. package/vue/UiTree.vue +56 -137
  208. package/vue/UiTreeItem.vue +28 -144
  209. package/vue/chunks/UiAffordanceScope.js +9 -78
  210. package/vue/chunks/UiAvatar.js +13 -89
  211. package/vue/chunks/UiAvatarGroup.js +10 -80
  212. package/vue/chunks/UiBadge.js +1 -1
  213. package/vue/chunks/UiButton.js +3 -3
  214. package/vue/chunks/UiCallout.js +1 -1
  215. package/vue/chunks/UiCheckbox.js +17 -95
  216. package/vue/chunks/UiCluster.js +1 -1
  217. package/vue/chunks/UiCombobox.js +115 -326
  218. package/vue/chunks/UiContainer.js +1 -1
  219. package/vue/chunks/UiContextMenu.js +21 -113
  220. package/vue/chunks/UiDialog.js +20 -99
  221. package/vue/chunks/UiDisclosure.js +22 -112
  222. package/vue/chunks/UiEditable.js +32 -128
  223. package/vue/chunks/UiEditorInsertTableGrid.js +22 -116
  224. package/vue/chunks/UiEditorMentionMenu.js +18 -116
  225. package/vue/chunks/UiEditorSlashMenu.js +15 -101
  226. package/vue/chunks/UiEditorTableContextMenu.js +12 -91
  227. package/vue/chunks/UiEditorTableOverlay.js +29 -120
  228. package/vue/chunks/UiEditorTableToolbar.js +16 -111
  229. package/vue/chunks/UiEditorToolbar.js +1 -1
  230. package/vue/chunks/UiFloatingActionButton.js +1 -1
  231. package/vue/chunks/UiFormField.js +8 -73
  232. package/vue/chunks/UiGrid.js +1 -1
  233. package/vue/chunks/UiIcon.js +9 -78
  234. package/vue/chunks/UiIconButton.js +8 -73
  235. package/vue/chunks/UiInput.js +8 -73
  236. package/vue/chunks/UiMenu.js +25 -111
  237. package/vue/chunks/UiMenuItem.js +1 -1
  238. package/vue/chunks/UiPopover.js +18 -105
  239. package/vue/chunks/UiRadio.js +17 -95
  240. package/vue/chunks/UiRadioGroup.js +23 -105
  241. package/vue/chunks/UiReel.js +1 -1
  242. package/vue/chunks/UiSearchResultRow.js +1 -1
  243. package/vue/chunks/UiSearchShell.js +17 -94
  244. package/vue/chunks/UiSelect.js +8 -79
  245. package/vue/chunks/UiSeparator.js +1 -1
  246. package/vue/chunks/UiSidebar.js +23 -108
  247. package/vue/chunks/UiStack.js +1 -1
  248. package/vue/chunks/UiSwitch.js +17 -95
  249. package/vue/chunks/UiSwitcher.js +1 -1
  250. package/vue/chunks/UiTabs.js +22 -103
  251. package/vue/chunks/UiTextarea.js +8 -73
  252. package/vue/chunks/UiToastRegion.js +27 -112
  253. package/vue/chunks/UiTooltip.js +18 -106
  254. package/vue/chunks/UiTopBar.js +1 -1
  255. package/vue/chunks/UiTree.js +31 -109
  256. package/vue/chunks/UiTreeItem.js +29 -119
  257. package/vue/chunks/host.js +101 -0
  258. package/vue/components.css +844 -797
  259. package/vue/host.ts +154 -0
@@ -1,12 +1,12 @@
1
1
  <template component="ui-button">
2
2
  <defs>
3
3
  <prop name="variant" type="outline | solid | danger | ghost | link" default="outline"
4
- >Visual emphasis and semantic intent. link is an inline text action that reads as a link.</prop>
4
+ >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
5
+ hovered, and link is an inline text action that reads as a link. danger is solid in the danger tone, kept for the call sites that name it.</prop>
5
6
  <prop name="size" type="sm | md | lg" default="md">Control size.</prop>
6
- <prop name="tone" type="neutral | accent" default="neutral"
7
- >Intent of an outline, ghost, or link button. accent tints it with the accent colour, for a secondary action that still reads as the primary path. For a
8
- product whose buttons are accent-tinted by default, point --ui-button-surface, --ui-button-border, and --ui-button-text at the accent values in your theme
9
- rather than annotating every call site, and keep them as var() references so they follow a later change of accent.</prop>
7
+ <prop name="tone" type="accent | neutral | danger | success | warning | info" default="accent"
8
+ >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:
9
+ tone="danger" variant="outline" is a destructive secondary action. neutral is the quiet one, for a button that should not claim a colour.</prop>
10
10
  <prop name="disabled" type="boolean" default="false">disabled token.</prop>
11
11
  <prop name="align" type="center | start" default="center"
12
12
  >Content alignment. start lays the content out like a list option: from the start edge, with start-aligned text.</prop>
@@ -15,15 +15,23 @@
15
15
  <button type="button" :disabled="disabled"><slot></slot></button>
16
16
  <style>
17
17
  :host {
18
+ --_hue: var(--ui-accent);
19
+ --_hue-hover: var(--ui-accent-hover);
20
+ --_hue-active: var(--ui-accent-active);
21
+ --_hue-text: var(--ui-accent-active);
22
+ --_on-hue: var(--ui-on-accent);
23
+
24
+ --_tone: var(--_hue);
25
+ --_tone-hover: var(--_hue-hover);
26
+ --_tone-active: var(--_hue-active);
27
+ --_tone-text: var(--_hue-text);
28
+ --_on-tone: var(--_on-hue);
29
+
18
30
  display: inline-flex;
19
31
  align-items: center;
20
32
  justify-content: center;
21
33
  vertical-align: middle;
22
- color: var(--ui-action-secondary-text, var(--ui-text-primary));
23
- }
24
-
25
- :host-state([variant="solid"]) {
26
- color: var(--ui-action-primary-text, var(--ui-text-on-accent));
34
+ color: var(--_tone-text);
27
35
  }
28
36
 
29
37
  :host-state([align="start"]) {
@@ -39,23 +47,24 @@
39
47
  :host {
40
48
  appearance: none;
41
49
  min-block-size: var(--ui-button-min-block-size, var(--ui-control-size-md));
42
- border: 1px solid var(--ui-button-border, var(--ui-border-strong));
50
+ border-style: solid;
51
+ border-width: 1px;
52
+ border-color: var(--ui-button-border, var(--_tone));
43
53
  border-radius: var(--ui-button-radius, var(--ui-radius-md));
44
- background: var(--ui-button-surface, var(--ui-surface-elevated));
45
- color: var(--ui-button-text, var(--ui-action-secondary-text, var(--ui-text-primary)));
54
+ background-color: var(--ui-button-surface, oklch(from var(--_tone) l c h / 5%));
55
+ color: var(--ui-button-text, var(--_tone-text));
46
56
  font: inherit;
47
57
  font-weight: var(--ui-font-medium);
48
58
  line-height: var(--ui-line-height-tight);
49
59
  padding: var(--ui-space-2) var(--ui-space-3);
50
60
  background-image: var(--ui-raised-highlight);
51
- box-shadow: var(--ui-button-shadow, var(--ui-raised, var(--ui-shadow-xs)));
61
+ box-shadow: var(--ui-button-shadow, var(--ui-raised));
52
62
  cursor: pointer;
53
63
  transition:
54
64
  background-color var(--ui-motion-fast) var(--ui-motion-ease),
55
65
  border-color var(--ui-motion-fast) var(--ui-motion-ease),
56
66
  color var(--ui-motion-fast) var(--ui-motion-ease),
57
- box-shadow var(--ui-motion-fast) var(--ui-motion-ease),
58
- transform var(--ui-motion-fast) var(--ui-motion-ease);
67
+ box-shadow var(--ui-motion-fast) var(--ui-motion-ease);
59
68
  margin: 0;
60
69
  }
61
70
 
@@ -79,128 +88,141 @@
79
88
  }
80
89
 
81
90
  :host:hover:enabled {
82
- background: var(--ui-action-secondary-surface-hover, var(--ui-surface-muted));
83
- border-color: var(--ui-control-border-hover, var(--ui-border-strong));
91
+ border-color: var(--ui-button-hover-border, var(--_tone-hover));
92
+ background-color: var(--ui-button-hover-surface, oklch(from var(--_tone) l c h / 12%));
84
93
  }
85
94
 
95
+ /* Pressed reads as pressed in: the highlight goes, and the shadow moves inside. */
86
96
  :host:active:enabled {
87
- background: var(--ui-surface-subtle);
88
- }
89
-
90
- :host-state([variant="solid"]) {
91
- border-color: var(--ui-action-primary-surface, var(--ui-accent-solid));
92
- background: var(--ui-action-primary-surface, var(--ui-accent-solid));
93
- color: var(--ui-action-primary-text, var(--ui-text-on-accent));
94
- box-shadow: var(--ui-button-solid-shadow, inset 0 1px rgb(255 255 255 / 0.16), var(--ui-shadow-xs));
97
+ background-color: var(--ui-button-active-surface, oklch(from var(--_tone) l c h / 18%));
98
+ background-image: none;
99
+ box-shadow: var(--ui-pressed);
95
100
  }
96
101
 
97
- :host-state([variant="solid"]):hover:enabled {
98
- background: var(--ui-action-primary-surface-hover, var(--ui-accent-hover));
99
- border-color: var(--ui-action-primary-surface-hover, var(--ui-accent-hover));
100
- box-shadow: var(--ui-shadow-sm);
101
- transform: translateY(-1px);
102
+ :host-state([variant="solid"]), :host-state([variant="danger"]) {
103
+ border-color: var(--_tone);
104
+ background-color: var(--_tone);
105
+ color: var(--_on-tone);
106
+ box-shadow: var(--ui-button-solid-shadow, var(--ui-raised));
102
107
  }
103
108
 
104
- :host-state([variant="solid"]):active:enabled {
105
- background: var(--ui-action-primary-surface-active, var(--ui-accent-active));
106
- border-color: var(--ui-action-primary-surface-active, var(--ui-accent-active));
107
- box-shadow: none;
108
- transform: translateY(0);
109
+ :is(:host-state([variant="solid"]), :host-state([variant="danger"])):hover:enabled {
110
+ background-color: var(--_tone-hover);
111
+ border-color: var(--_tone-hover);
109
112
  }
110
113
 
111
- :host-state([variant="danger"]) {
112
- border-color: var(--ui-danger-solid);
113
- background: var(--ui-danger-solid);
114
- color: var(--ui-text-on-accent);
115
- box-shadow: var(--ui-button-danger-shadow, inset 0 1px rgb(255 255 255 / 0.14), var(--ui-shadow-xs));
114
+ :is(:host-state([variant="solid"]), :host-state([variant="danger"])):active:enabled {
115
+ background-color: var(--_tone-active);
116
+ border-color: var(--_tone-active);
117
+ background-image: none;
118
+ box-shadow: var(--ui-pressed);
116
119
  }
117
120
 
118
- :host-state([variant="danger"]):hover:enabled {
119
- background: var(--ui-danger-hover);
120
- border-color: var(--ui-danger-hover);
121
- box-shadow: var(--ui-shadow-sm);
122
- transform: translateY(-1px);
121
+ /* Tone is the colour, variant is the volume. Every pairing works: a danger outline is a
122
+ destructive secondary action, not a special case. */
123
+ :host-state([tone="neutral"]) {
124
+ --_hue: var(--ui-text);
125
+ --_hue-hover: var(--ui-text);
126
+ --_hue-active: var(--ui-text);
127
+ --_hue-text: var(--ui-text);
128
+ --_on-hue: var(--ui-surface);
123
129
  }
124
130
 
125
- :host-state([variant="danger"]):active:enabled {
126
- box-shadow: none;
127
- transform: translateY(0);
131
+ :host-state([tone="danger"]), :host-state([variant="danger"]) {
132
+ --_hue: var(--ui-danger);
133
+ --_hue-hover: var(--ui-danger-hover);
134
+ --_hue-active: var(--ui-danger-hover);
135
+ --_hue-text: var(--ui-danger-hover);
136
+ --_on-hue: var(--ui-on-danger);
128
137
  }
129
138
 
130
- /* An accent-tinted secondary action: the outline, ghost, and link variants in the accent colour.
131
- Solid and danger already carry their own intent. */
132
- :host-state([tone="accent"]):host-state([variant="outline"]) {
133
- border-color: var(--ui-button-accent-border, var(--ui-accent));
134
- background: var(--ui-button-accent-surface, var(--ui-accent-subtle));
135
- color: var(--ui-button-accent-text, var(--ui-accent-active));
139
+ :host-state([tone="success"]) {
140
+ --_hue: var(--ui-success);
141
+ --_hue-hover: color-mix(in srgb, var(--ui-success) 85%, #000);
142
+ --_hue-active: color-mix(in srgb, var(--ui-success) 75%, #000);
143
+ --_hue-text: color-mix(in srgb, var(--ui-success) 85%, #000);
144
+ --_on-hue: var(--ui-surface);
136
145
  }
137
146
 
138
- :host-state([tone="accent"]):host-state([variant="outline"]):hover:enabled {
139
- border-color: var(--ui-button-accent-hover-border, var(--ui-accent-hover));
140
- background: var(--ui-button-accent-hover-surface, color-mix(in srgb, var(--ui-accent) 16%, var(--ui-surface)));
147
+ :host-state([tone="warning"]) {
148
+ --_hue: var(--ui-warning);
149
+ --_hue-hover: color-mix(in srgb, var(--ui-warning) 85%, #000);
150
+ --_hue-active: color-mix(in srgb, var(--ui-warning) 75%, #000);
151
+ --_hue-text: color-mix(in srgb, var(--ui-warning) 85%, #000);
152
+ --_on-hue: var(--ui-surface);
141
153
  }
142
154
 
143
- :host-state([tone="accent"]):is(:host-state([variant="ghost"]), :host-state([variant="link"])) {
144
- color: var(--ui-button-accent-text, var(--ui-accent-active));
155
+ :host-state([tone="info"]) {
156
+ --_hue: var(--ui-info);
157
+ --_hue-hover: color-mix(in srgb, var(--ui-info) 85%, #000);
158
+ --_hue-active: color-mix(in srgb, var(--ui-info) 75%, #000);
159
+ --_hue-text: color-mix(in srgb, var(--ui-info) 85%, #000);
160
+ --_on-hue: var(--ui-surface);
145
161
  }
146
162
 
147
163
  :host-state([variant="ghost"]) {
148
164
  border-color: transparent;
149
165
  background: transparent;
150
166
  box-shadow: none;
151
- color: var(--ui-button-ghost-text, var(--ui-action-secondary-text, var(--ui-text-primary)));
167
+ color: var(--ui-button-ghost-text, var(--_tone-text));
152
168
  }
153
169
 
154
170
  /* Tints derive from the text colour, not a surface token, so hover and press show on any background. */
155
171
  :host-state([variant="ghost"]):hover:enabled {
156
- background: var(--ui-button-ghost-hover-surface, color-mix(in srgb, currentColor 8%, transparent));
172
+ background-color: var(--ui-button-ghost-hover-surface, oklch(from var(--_tone) l c h / 10%));
157
173
  border-color: transparent;
158
174
  }
159
175
 
160
176
  :host-state([variant="ghost"]):active:enabled {
161
- background: var(--ui-button-ghost-active-surface, color-mix(in srgb, currentColor 14%, transparent));
177
+ background-color: var(--ui-button-ghost-active-surface, oklch(from var(--_tone) l c h / 16%));
162
178
  border-color: transparent;
179
+ box-shadow: var(--ui-pressed);
163
180
  }
164
181
 
165
182
  /* A soft halo that transitions in with the other box-shadow changes, layered on the raised shadow. */
166
183
  :host:focus-visible {
167
184
  outline: none;
168
185
  box-shadow:
169
- var(--ui-button-shadow, var(--ui-raised, var(--ui-shadow-xs))),
186
+ var(--ui-button-shadow, var(--ui-raised)),
170
187
  var(--ui-focus-halo);
171
188
  }
172
189
 
173
190
  :host-state([variant="solid"]):focus-visible {
174
191
  box-shadow:
175
- var(--ui-button-solid-shadow, inset 0 1px rgb(255 255 255 / 0.16), var(--ui-shadow-xs)),
192
+ var(--ui-button-solid-shadow, var(--ui-raised)),
176
193
  var(--ui-focus-halo);
177
194
  }
178
195
 
179
- /* Disabled colours fall back to each variant's own; ghost and link take only the text colour. */
196
+ /* Disabled drops the tone rather than the shape: every variant keeps its own structure, so a
197
+ disabled outline is still an outline and a disabled ghost is still text. Intent goes — an
198
+ unavailable action says "unavailable", not "unavailable, and destructive" — and so do the
199
+ highlight and shadow, because nothing that cannot be pressed should look raised. */
180
200
  :host:disabled {
201
+ /* Derived from the tone itself, not from a separate disabled palette: faded toward the
202
+ page, which desaturates it at the same time. Toward the page rather than toward white,
203
+ so a dark theme dims where a light one lightens — the same rule, both schemes. */
204
+ --_tone: color-mix(in oklab, var(--_hue) 58%, var(--ui-surface));
205
+ --_tone-hover: var(--_tone);
206
+ --_tone-active: var(--_tone);
207
+ --_tone-text: color-mix(in oklab, var(--_hue-text) 72%, var(--ui-surface));
208
+ --_on-tone: var(--_tone-text);
209
+
181
210
  opacity: var(--ui-button-disabled-opacity, 1);
182
211
  cursor: not-allowed;
183
- border-color: var(--ui-button-disabled-border, var(--ui-border));
184
- background-color: var(--ui-button-disabled-surface, var(--ui-disabled-surface));
185
- color: var(--ui-button-disabled-text, var(--ui-disabled-text));
186
- }
187
-
188
- :host-state([variant="solid"]):disabled {
189
- border-color: var(--ui-button-disabled-border, var(--ui-border));
190
- background-color: var(--ui-button-disabled-surface, var(--ui-disabled-surface));
191
- color: var(--ui-button-disabled-text, var(--ui-disabled-text));
212
+ background-image: none;
213
+ box-shadow: none;
192
214
  }
193
215
 
194
- :host-state([variant="danger"]):disabled {
195
- border-color: var(--ui-button-disabled-border, var(--ui-border));
196
- background-color: var(--ui-button-disabled-surface, var(--ui-disabled-surface));
197
- color: var(--ui-button-disabled-text, var(--ui-disabled-text));
216
+ /* A filled button keeps its fill, faded further so its text stays legible on it. */
217
+ :is(:host-state([variant="solid"]), :host-state([variant="danger"])):disabled {
218
+ --_tone: color-mix(in oklab, var(--_hue) 34%, var(--ui-surface));
198
219
  }
199
220
 
221
+ /* Ghost has no hover to fall back on, so disabled is the one state it has to state outright:
222
+ it takes a surface, light and tinted, rather than reading as plain text. */
200
223
  :host-state([variant="ghost"]):disabled {
201
- background: none;
224
+ background-color: color-mix(in oklab, var(--_hue) 28%, var(--ui-surface));
202
225
  border-color: transparent;
203
- color: var(--ui-button-disabled-text, var(--ui-disabled-text));
204
226
  }
205
227
 
206
228
  @media (pointer: coarse) {
@@ -65,7 +65,10 @@
65
65
  margin: auto;
66
66
  inline-size: 0.32rem;
67
67
  block-size: 0.58rem;
68
- border: solid var(--ui-text-on-accent);
68
+ /* Longhands, not `border: solid var(...)`: lowering drops a shorthand that omits a
69
+ component, which left the tick with no style and no colour. */
70
+ border-style: solid;
71
+ border-color: var(--ui-text-on-accent);
69
72
  border-width: 0 0.125rem 0.125rem 0;
70
73
  opacity: 1;
71
74
  transform: translateY(-0.11rem) rotate(45deg);
@@ -26,7 +26,11 @@
26
26
  <state name="display" :value="''"></state>
27
27
  <state name="selected" type="string | null" :value="null"></state>
28
28
  <state name="expanded" :value="false"></state>
29
- <state name="rows" type="list(object({ id: string, value: string, label: string, group?: string, disabled: boolean }))" :value="[]"></state>
29
+ <state
30
+ name="rows"
31
+ type="list(object({ id: string, value: string, label: string, group?: string, disabled: boolean, selected: boolean }))"
32
+ :value="[]"
33
+ ></state>
30
34
  <state name="active" :value="-1"></state>
31
35
  <state name="loading" :value="false"></state>
32
36
  <state name="helpOpen" :value="false"></state>
@@ -39,7 +43,7 @@
39
43
  <state name="uid" :value="''"></state>
40
44
  <state
41
45
  name="groups"
42
- type="list(object({ name: string, role: group | presentation, label: string | null, rows: list(object({ id: string, value: string, label: string, group?: string, disabled: boolean, index: integer })) }))"
46
+ type="list(object({ name: string, role: group | presentation, label: string | null, rows: list(object({ id: string, value: string, label: string, group?: string, disabled: boolean, selected: boolean, index: integer })) }))"
43
47
  :value="[]"
44
48
  ></state>
45
49
  <state name="creatable" :value="false"></state>
@@ -170,7 +174,7 @@
170
174
  class:active="row.index = active"
171
175
  role="option"
172
176
  :id="format('%s-option-%s', uid, row.index)"
173
- :aria-selected="row.value = selected"
177
+ :aria-selected="row.selected"
174
178
  :aria-disabled="row.disabled"
175
179
  :data-index="row.index"
176
180
  >
@@ -407,6 +411,39 @@
407
411
  }
408
412
  }
409
413
 
414
+ /* Multiple mode: every option carries a checkbox, so a list that takes several answers says so
415
+ before anything is picked. aria-selected is the state; this draws it.
416
+ Longhands, not shorthands: lowering drops a shorthand that omits a component. */
417
+ :host-state([multiple]) .option {
418
+ display: flex;
419
+ gap: var(--ui-space-2);
420
+ align-items: center;
421
+ }
422
+
423
+ :host-state([multiple]) .option::before {
424
+ content: "";
425
+ flex: none;
426
+ inline-size: 1.125rem;
427
+ block-size: 1.125rem;
428
+ border-style: solid;
429
+ border-width: 1px;
430
+ border-color: var(--ui-control-border-hover, var(--ui-border-strong));
431
+ border-radius: var(--ui-radius-sm);
432
+ background-color: var(--ui-control-surface, var(--ui-surface-default));
433
+ background-repeat: no-repeat;
434
+ background-position: center;
435
+ background-size: 0.7rem;
436
+ }
437
+
438
+ :host-state([multiple]) .option[aria-selected="true"]::before {
439
+ border-color: var(--ui-accent-solid);
440
+ background-color: var(--ui-accent-solid);
441
+ background-image: var(
442
+ --ui-combobox-option-check,
443
+ url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23ffffff' stroke-width='3' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='m5 12 5 5 9-9'/%3E%3C/svg%3E")
444
+ );
445
+ }
446
+
410
447
  /* Multiple mode: chips wrap before the cursor on one flexible field line. */
411
448
  :host-state([multiple]) .field {
412
449
  flex-wrap: wrap;
@@ -113,13 +113,17 @@ export default function controller(host) {
113
113
  }
114
114
  const ids = new Set();
115
115
  const selected = host.state.multiple ? selectedValues() : undefined;
116
+ // Multiple keeps its chosen options in the list, checked. Dropping them hid what was picked
117
+ // from the list and from assistive technology, which only ever heard aria-selected="false".
116
118
  const filtered = options.filter((option) => {
117
119
  if (ids.has(option.id)) return false;
118
120
  ids.add(option.id);
119
121
  if (fullSet) return true;
120
- const matches = option.label.toLocaleLowerCase().includes(query.toLocaleLowerCase());
121
- return selected ? !selected.has(option.value) && matches : matches;
122
- });
122
+ return option.label.toLocaleLowerCase().includes(query.toLocaleLowerCase());
123
+ }).map((option) => ({
124
+ ...option,
125
+ selected: selected ? selected.has(option.value) : option.value === host.state.selected
126
+ }));
123
127
  const groups = new Map();
124
128
  for (const row of filtered) {
125
129
  const group = row.group ?? "";
@@ -182,7 +186,16 @@ export default function controller(host) {
182
186
  input.value = query;
183
187
  host.dispatch("query-change", { query, display: query, trigger });
184
188
  };
185
- const addSelectedItem = (option, trigger) => {
189
+ // A row carries a view-only `selected` flag; an item is the option itself, in its declared shape.
190
+ const asItem = ({ id, value, label, group, disabled }) => ({
191
+ id,
192
+ value,
193
+ label,
194
+ ...(group === undefined ? {} : { group }),
195
+ ...(disabled === undefined ? {} : { disabled })
196
+ });
197
+ const addSelectedItem = (row, trigger) => {
198
+ const option = asItem(row);
186
199
  const current = items();
187
200
  host.dispatch("add-item", { item: option, index: current.length, trigger });
188
201
  emitItems([...current, option]);
@@ -192,10 +205,19 @@ export default function controller(host) {
192
205
  const option = (host.state.rows ?? [])[index];
193
206
  if (option?.disabled) return;
194
207
  if (host.state.multiple) {
195
- if (option) addSelectedItem(option, trigger);
208
+ if (option) {
209
+ // A checked row toggles off: the list is the selection, so it has to work both ways.
210
+ const position = items().findIndex((item) => item.value === option.value);
211
+ if (position >= 0) removeItemAt(position, trigger);
212
+ else addSelectedItem(option, trigger);
213
+ }
196
214
  else if (canCreate() && index === host.state.rows.length) createSelectedItem(String(host.state.raw).trim(), trigger);
197
215
  else return;
198
216
  setMultiQuery("", trigger);
217
+ // The list stays open so several can be chosen without reopening it.
218
+ search("selection");
219
+ input.focus();
220
+ return;
199
221
  } else if (option) commit(option.value, option.label, option, "selection", trigger);
200
222
  else if (canCreate() && index === host.state.rows.length) commit(null, host.state.raw, null, "create", trigger);
201
223
  else return;
@@ -4,9 +4,8 @@
4
4
  <prop name="disabled" type="boolean" default="false">Prevents selection and form submission.</prop>
5
5
  <prop name="invalid" type="boolean" default="false">Exposes invalid state to assistive technology and styling.</prop>
6
6
  <prop name="required" type="boolean" default="false">Requires a non-empty option for form validation.</prop>
7
- <prop name="multiple" type="boolean" default="false">Allows more than one option to be selected.</prop>
8
7
  </defs>
9
- <select :disabled="disabled" :required="required" :multiple="multiple" :aria-invalid="invalid"><slot></slot></select>
8
+ <select :disabled="disabled" :required="required" :aria-invalid="invalid"><slot></slot></select>
10
9
  <style>
11
10
  :host {
12
11
  box-sizing: border-box;
@@ -30,9 +29,9 @@
30
29
  background-color var(--ui-motion-fast) var(--ui-motion-ease);
31
30
  }
32
31
 
33
- /* Single selects draw their own chevron: the native caret cannot be inset or sized consistently.
32
+ /* The select draws its own chevron: the native caret cannot be inset or sized consistently.
34
33
  * The root stays a native <select>, so the icon is a background image rather than an element. */
35
- :host:not([multiple]) {
34
+ :host {
36
35
  appearance: none;
37
36
  padding-inline-end: calc(var(--ui-space-3) + 1.5rem);
38
37
  background-image: var(
@@ -73,7 +72,6 @@
73
72
  cursor: not-allowed;
74
73
  }
75
74
 
76
- :host[multiple],
77
75
  :host[size]:not([size="1"]) {
78
76
  min-block-size: 7rem;
79
77
  padding: var(--ui-space-2);