@zeyos/zx 4.3.4 → 5.0.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 (231) hide show
  1. package/CHANGELOG.md +438 -0
  2. package/README.md +49 -10
  3. package/dist/types/components/activity-item/activity-item.d.ts +351 -0
  4. package/dist/types/components/activity-list/activity-list.d.ts +217 -0
  5. package/dist/types/components/app-sidebar/app-sidebar.d.ts +33 -3
  6. package/dist/types/components/avatar/avatar.d.ts +46 -7
  7. package/dist/types/components/badge/badge.d.ts +21 -1
  8. package/dist/types/components/button/button.d.ts +23 -0
  9. package/dist/types/components/calendar/calendar.d.ts +19 -0
  10. package/dist/types/components/card/card.d.ts +7 -0
  11. package/dist/types/components/card-view/card-view.d.ts +11 -3
  12. package/dist/types/components/card-view/record-card.d.ts +7 -1
  13. package/dist/types/components/chart/chart.d.ts +10 -0
  14. package/dist/types/components/checklist/checklist.d.ts +9 -0
  15. package/dist/types/components/code-editor/code-editor.d.ts +149 -0
  16. package/dist/types/components/code-editor/field-adapter.d.ts +5 -0
  17. package/dist/types/components/context-menu/context-menu.d.ts +18 -2
  18. package/dist/types/components/date-picker/date-picker.d.ts +8 -0
  19. package/dist/types/components/date-picker/time-picker.d.ts +7 -0
  20. package/dist/types/components/datebox/date-range-box.d.ts +5 -1
  21. package/dist/types/components/datebox/datebox.d.ts +7 -2
  22. package/dist/types/components/dialog/dialog.d.ts +75 -6
  23. package/dist/types/components/dropdown/dropdown.d.ts +2 -2
  24. package/dist/types/components/entity-ref/entity-ref.d.ts +331 -0
  25. package/dist/types/components/field/field.d.ts +24 -2
  26. package/dist/types/components/fieldset/fieldset.d.ts +19 -1
  27. package/dist/types/components/file-item/file-item.d.ts +262 -0
  28. package/dist/types/components/file-list/file-list.d.ts +171 -0
  29. package/dist/types/components/filter/filter.d.ts +91 -36
  30. package/dist/types/components/filter-panel/filter-panel.d.ts +449 -0
  31. package/dist/types/components/form/form.d.ts +12 -1
  32. package/dist/types/components/kanban-view/kanban-card.d.ts +146 -0
  33. package/dist/types/components/kanban-view/kanban-view.d.ts +170 -4
  34. package/dist/types/components/launcher/launcher.d.ts +278 -26
  35. package/dist/types/components/list-toolbar/list-toolbar.d.ts +599 -0
  36. package/dist/types/components/menu-button/menu-button.d.ts +37 -42
  37. package/dist/types/components/message/message.d.ts +126 -17
  38. package/dist/types/components/modal/modal.d.ts +61 -2
  39. package/dist/types/components/navigation-bar/navigation-bar.d.ts +105 -21
  40. package/dist/types/components/panel/panel.d.ts +7 -0
  41. package/dist/types/components/search/search.d.ts +7 -0
  42. package/dist/types/components/select/color.d.ts +12 -0
  43. package/dist/types/components/select/glyphs.d.ts +124 -0
  44. package/dist/types/components/select/select.d.ts +15 -3
  45. package/dist/types/components/sort-control/sort-control.d.ts +105 -0
  46. package/dist/types/components/stat-tile/stat-tile.d.ts +288 -0
  47. package/dist/types/components/tabbox/tabbox.d.ts +16 -0
  48. package/dist/types/components/table/table.d.ts +190 -1
  49. package/dist/types/components/table-view/table-view.d.ts +11 -5
  50. package/dist/types/components/tag-picker/tag-picker.d.ts +21 -4
  51. package/dist/types/components/timebox/timebox.d.ts +7 -0
  52. package/dist/types/components/tree/tree.d.ts +13 -0
  53. package/dist/types/components/value-list/value-list.d.ts +8 -0
  54. package/dist/types/components/view/record-view.d.ts +96 -5
  55. package/dist/types/core/breakpoint.d.ts +1 -1
  56. package/dist/types/core/icons.d.ts +37 -22
  57. package/dist/types/core/motion.d.ts +59 -0
  58. package/dist/types/core/overlay-host.d.ts +15 -1
  59. package/dist/types/index.d.ts +16 -3
  60. package/dist/types/internal/app-rail.d.ts +10 -0
  61. package/dist/types/internal/menu-items.d.ts +134 -0
  62. package/dist/types/zeyos/connect.d.ts +6 -0
  63. package/dist/types/zeyos/form.d.ts +50 -9
  64. package/dist/types/zeyos/index.d.ts +1 -1
  65. package/dist/types/zeyos/query.d.ts +79 -0
  66. package/dist/types/zeyos/schema.d.ts +24 -1
  67. package/dist/types/zeyos/table.d.ts +100 -14
  68. package/dist/types/zeyos/view.d.ts +10 -0
  69. package/dist/zx-zeyos.esm.js +2206 -489
  70. package/dist/zx-zeyos.esm.min.js +56 -1
  71. package/dist/zx.css +3322 -724
  72. package/dist/zx.esm.js +12244 -5809
  73. package/dist/zx.esm.min.js +58 -3
  74. package/dist/zx.global.js +12246 -5811
  75. package/dist/zx.global.min.js +58 -3
  76. package/dist/zx.min.css +1 -1
  77. package/docs/api.json +1683 -146
  78. package/docs/llms.md +777 -110
  79. package/docs/llms.txt +14 -8
  80. package/package.json +1 -1
  81. package/src/components/account-menu/account-menu.css +35 -10
  82. package/src/components/activity-item/activity-item.css +137 -0
  83. package/src/components/activity-item/activity-item.js +602 -0
  84. package/src/components/activity-list/activity-list.css +99 -0
  85. package/src/components/activity-list/activity-list.js +409 -0
  86. package/src/components/app-icon/app-icon.css +1 -1
  87. package/src/components/app-rail/app-rail.css +191 -22
  88. package/src/components/app-sidebar/app-sidebar.css +349 -24
  89. package/src/components/app-sidebar/app-sidebar.js +86 -11
  90. package/src/components/aurora/aurora.css +5 -3
  91. package/src/components/avatar/avatar.css +32 -7
  92. package/src/components/avatar/avatar.js +72 -8
  93. package/src/components/badge/badge.css +54 -2
  94. package/src/components/badge/badge.js +29 -2
  95. package/src/components/button/button.css +74 -8
  96. package/src/components/button/button.js +44 -1
  97. package/src/components/calendar/calendar.css +46 -46
  98. package/src/components/calendar/calendar.js +33 -3
  99. package/src/components/card/card.css +2 -2
  100. package/src/components/card/card.js +12 -1
  101. package/src/components/card-view/card-view.css +10 -7
  102. package/src/components/card-view/card-view.js +25 -9
  103. package/src/components/card-view/record-card.js +7 -3
  104. package/src/components/chart/chart.css +4 -1
  105. package/src/components/chart/chart.js +12 -0
  106. package/src/components/check-button/check-button.css +10 -2
  107. package/src/components/checklist/checklist.css +9 -2
  108. package/src/components/checklist/checklist.js +11 -5
  109. package/src/components/checklist/field-adapter.js +3 -1
  110. package/src/components/code-editor/code-editor.css +68 -0
  111. package/src/components/code-editor/code-editor.js +306 -0
  112. package/src/components/code-editor/field-adapter.js +34 -0
  113. package/src/components/context-menu/context-menu.css +87 -5
  114. package/src/components/context-menu/context-menu.js +22 -30
  115. package/src/components/copy/copy-button.css +6 -0
  116. package/src/components/copy/copy-input.css +8 -3
  117. package/src/components/data-filter/data-filter.css +5 -4
  118. package/src/components/date-picker/date-picker.css +6 -6
  119. package/src/components/date-picker/date-picker.js +22 -8
  120. package/src/components/date-picker/date-range-picker.css +3 -3
  121. package/src/components/date-picker/month-picker.js +3 -3
  122. package/src/components/date-picker/time-picker.css +1 -1
  123. package/src/components/date-picker/time-picker.js +20 -8
  124. package/src/components/datebox/date-range-box.css +79 -2
  125. package/src/components/datebox/date-range-box.js +13 -2
  126. package/src/components/datebox/datebox.css +79 -2
  127. package/src/components/datebox/datebox.js +13 -2
  128. package/src/components/dialog/dialog.css +26 -0
  129. package/src/components/dialog/dialog.js +174 -70
  130. package/src/components/dropdown/dropdown.css +1 -1
  131. package/src/components/dropdown/dropdown.js +7 -5
  132. package/src/components/empty-state/empty-state.css +2 -2
  133. package/src/components/entity-ref/entity-ref.css +182 -0
  134. package/src/components/entity-ref/entity-ref.js +496 -0
  135. package/src/components/field/field.css +28 -9
  136. package/src/components/field/field.js +68 -26
  137. package/src/components/field-upload/field-upload.css +1 -1
  138. package/src/components/fieldset/fieldset.css +5 -3
  139. package/src/components/fieldset/fieldset.js +28 -5
  140. package/src/components/file-item/file-item.css +88 -0
  141. package/src/components/file-item/file-item.js +505 -0
  142. package/src/components/file-list/file-list.css +51 -0
  143. package/src/components/file-list/file-list.js +320 -0
  144. package/src/components/filter/filter.css +27 -1
  145. package/src/components/filter/filter.js +205 -47
  146. package/src/components/filter-panel/filter-panel.css +104 -0
  147. package/src/components/filter-panel/filter-panel.js +964 -0
  148. package/src/components/finder/finder.css +15 -13
  149. package/src/components/form/form.css +5 -4
  150. package/src/components/form/form.js +11 -2
  151. package/src/components/grid/grid.css +1 -1
  152. package/src/components/groupbox/groupbox.css +1 -1
  153. package/src/components/kanban-view/kanban-card.js +247 -0
  154. package/src/components/kanban-view/kanban-view.css +226 -8
  155. package/src/components/kanban-view/kanban-view.js +255 -12
  156. package/src/components/launcher/launcher.css +107 -25
  157. package/src/components/launcher/launcher.js +557 -63
  158. package/src/components/list-toolbar/list-toolbar.css +194 -0
  159. package/src/components/list-toolbar/list-toolbar.js +851 -0
  160. package/src/components/master-panel/master-panel.css +2 -0
  161. package/src/components/menu-button/menu-button.css +87 -5
  162. package/src/components/menu-button/menu-button.js +86 -46
  163. package/src/components/message/message.css +38 -5
  164. package/src/components/message/message.js +230 -17
  165. package/src/components/modal/modal.css +15 -0
  166. package/src/components/modal/modal.js +159 -3
  167. package/src/components/navigation-bar/navigation-bar.css +95 -11
  168. package/src/components/navigation-bar/navigation-bar.js +171 -4
  169. package/src/components/number-field/field-adapter.js +1 -0
  170. package/src/components/number-field/number-field.css +70 -0
  171. package/src/components/number-field/number-field.js +2 -0
  172. package/src/components/pagination/pagination.css +8 -6
  173. package/src/components/panel/panel.css +1 -1
  174. package/src/components/panel/panel.js +13 -2
  175. package/src/components/rating/rating.css +7 -0
  176. package/src/components/search/search.js +17 -5
  177. package/src/components/select/color.js +22 -0
  178. package/src/components/select/field-adapter.js +6 -1
  179. package/src/components/select/glyphs.js +165 -0
  180. package/src/components/select/select.css +138 -45
  181. package/src/components/select/select.js +86 -47
  182. package/src/components/sheet/sheet.css +11 -8
  183. package/src/components/slider/slider.css +16 -2
  184. package/src/components/slider/slider.js +1 -1
  185. package/src/components/sort-control/sort-control.css +20 -0
  186. package/src/components/sort-control/sort-control.js +140 -0
  187. package/src/components/stat-tile/stat-tile.css +208 -0
  188. package/src/components/stat-tile/stat-tile.js +601 -0
  189. package/src/components/tabbox/tabbox.css +32 -27
  190. package/src/components/tabbox/tabbox.js +17 -2
  191. package/src/components/table/table.css +155 -31
  192. package/src/components/table/table.js +386 -33
  193. package/src/components/table-view/table-view.css +8 -2
  194. package/src/components/table-view/table-view.js +21 -6
  195. package/src/components/tag-picker/tag-picker.css +86 -6
  196. package/src/components/tag-picker/tag-picker.js +25 -8
  197. package/src/components/timebox/timebox.css +22 -2
  198. package/src/components/timebox/timebox.js +18 -5
  199. package/src/components/toggle/toggle.css +12 -1
  200. package/src/components/toolbar/toolbar.js +2 -2
  201. package/src/components/tooltip/tooltip.css +4 -0
  202. package/src/components/tooltip/tooltip.js +10 -3
  203. package/src/components/tree/tree.css +25 -16
  204. package/src/components/tree/tree.js +31 -0
  205. package/src/components/value-list/value-list.css +3 -3
  206. package/src/components/value-list/value-list.js +22 -6
  207. package/src/components/view/record-view.css +5 -0
  208. package/src/components/view/record-view.js +100 -7
  209. package/src/core/breakpoint.js +1 -1
  210. package/src/core/icons.js +259 -57
  211. package/src/core/motion.js +147 -0
  212. package/src/core/overlay-host.js +21 -2
  213. package/src/index.js +30 -3
  214. package/src/internal/app-rail.js +18 -1
  215. package/src/internal/menu-items.js +142 -0
  216. package/src/zeyos/connect.js +6 -2
  217. package/src/zeyos/form.js +67 -27
  218. package/src/zeyos/index.js +1 -1
  219. package/src/zeyos/query.js +153 -0
  220. package/src/zeyos/schema.js +144 -27
  221. package/src/zeyos/table.js +68 -5
  222. package/src/zeyos/view.js +6 -0
  223. package/styles/base.css +56 -0
  224. package/styles/icons.css +11 -1
  225. package/styles/print.css +192 -0
  226. package/styles/tokens/dark.css +113 -37
  227. package/styles/tokens/global.css +121 -27
  228. package/styles/tokens/semantic.css +100 -31
  229. package/styles/tokens/themes.css +7 -6
  230. package/styles/zx.css +11 -1
  231. package/styles/tokens/density.css +0 -9
@@ -6,7 +6,11 @@ import { uid } from '../../core/util.js';
6
6
  import { badge } from '../badge/badge.js';
7
7
  import { createRecordCard, resolveRecordActions } from '../card-view/record-card.js';
8
8
  import { ContextMenu } from '../context-menu/context-menu.js';
9
- import { RecordView, readViewField, viewRecordId } from '../view/record-view.js';
9
+ import {
10
+ createKanbanAvatars, createKanbanCardHead, createKanbanProgress, resolveKanbanAssignees,
11
+ resolveKanbanIndicator, resolveKanbanProgress
12
+ } from './kanban-card.js';
13
+ import { RecordView, normalizeViewFieldControls, readViewField, viewRecordId } from '../view/record-view.js';
10
14
  import {
11
15
  absoluteKanbanIndex, allowsKanbanTransition, clampKanbanIndex, createKanbanHistory,
12
16
  evaluateKanbanMove, kanbanSearchTerms, locateKanbanRecord, matchesKanbanSearch, normalizeAxisId,
@@ -20,6 +24,12 @@ import {
20
24
  /** @typedef {import('../card-view/record-card.js').RecordCardAction} RecordCardAction */
21
25
  /** @typedef {import('../menu-button/menu-button.js').MenuItem} MenuItem */
22
26
  /** @typedef {import('./kanban-policy.js').KanbanRule} KanbanRule */
27
+ /** @typedef {import('./kanban-card.js').KanbanIndicator} KanbanIndicator */
28
+ /**
29
+ * A card indicator source: a field id, a reader, or a literal descriptor used for every card.
30
+ * @typedef {string|KanbanIndicator|((record:KanbanRecord,index:number)=>KanbanIndicator|Node|unknown)|null} KanbanIndicatorSource
31
+ */
32
+ /** @typedef {import('./kanban-card.js').KanbanAssignee} KanbanAssignee */
23
33
  /** @typedef {import('./kanban-policy.js').KanbanRejectReason} KanbanRejectReason */
24
34
  /** @typedef {import('./kanban-policy.js').KanbanMoveEvaluation} KanbanMoveEvaluation */
25
35
  /** @typedef {string|((record: KanbanRecord, index: number) => unknown)|null} KanbanAccessor */
@@ -116,10 +126,13 @@ import {
116
126
  * @property {{id:string,dir:'asc'|'desc'}|null} [sort=null] Shared initial sort.
117
127
  * @property {'local'|'server'} [sortMode='local'] Shared sort mode.
118
128
  * @property {false|'single'|'multi'} [selectable=false] Shared selection mode.
129
+ * @property {'checkbox'|'card'} [selectionTrigger='checkbox'] Select through a checkbox or the non-interactive card surface.
119
130
  * @property {unknown[]} [selection=[]] Initial record ids.
120
131
  * @property {string[]} [fieldOrder=[]] Shared field order.
121
132
  * @property {string[]} [hiddenFields=[]] Shared hidden fields.
122
- * @property {boolean} [fieldControls=true] Show shared field controls.
133
+ * @property {boolean|import('../view/record-view.js').ViewFieldControlsConfig} [fieldControls=true]
134
+ * Show shared field controls. Redeclared narrower than `RecordViewOptions` it fails declaration
135
+ * emit, because the two option types are intersected.
123
136
  * @property {string|Node|(() => string|Node)|null} [emptyText=null] Empty-board content.
124
137
  * @property {KanbanAccessor} [columnBy='status'] Column accessor.
125
138
  * @property {KanbanColumn[]|null} [columns=null] Explicit columns, or null to derive them.
@@ -134,6 +147,21 @@ import {
134
147
  * @property {'local'|'external'} [moveMode='local'] Whether accepted moves update local data.
135
148
  * @property {'card'|'handle'} [dragFrom='card'] Whether the whole card or only its handle starts a
136
149
  * pointer drag. Touch drags from the card body require a long press so the board still scrolls.
150
+ * @property {'keyboard'|'always'} [moveHandle='keyboard'] Move-handle presentation. `keyboard`
151
+ * keeps it announced and tabbable but out of sight until it takes focus, because the card itself
152
+ * is already the pointer drag source; `always` shows it alongside the card's other controls.
153
+ * `dragFrom:'handle'` overrides this — a drag source a pointer user cannot see is not a control.
154
+ * @property {string|((record:KanbanRecord,index:number)=>string|null)|null} [entityIcon=null] Icon
155
+ * naming what the record is, drawn before the identifier.
156
+ * @property {KanbanCardField} [identifier=null] Stable record key shown on the card head.
157
+ * @property {KanbanIndicatorSource} [status=null] Status indicator source.
158
+ * @property {Record<string,string>} [statusTones={}] Maps a status value to a semantic tone.
159
+ * @property {KanbanIndicatorSource} [priority=null] Priority indicator source.
160
+ * @property {Record<string,string>} [priorityTones={}] Maps a priority value to a semantic tone.
161
+ * @property {KanbanCardField} [progress=null] Completion source; a number or `{value,max,label}`.
162
+ * @property {number} [progressMax=100] Amount that counts as finished.
163
+ * @property {KanbanCardField} [assignees=null] Responsible people; a name, a list, or descriptors.
164
+ * @property {number} [maxAvatars=3] Faces drawn before the overflow chip.
137
165
  * @property {boolean} [multiMove=true] Move the whole selection when a selected card is picked up.
138
166
  * @property {boolean} [autoScroll=true] Scroll the board and columns while dragging near an edge.
139
167
  * @property {'warn'|'block'} [wipPolicy='warn'] Whether exceeding a limit warns or refuses.
@@ -151,7 +179,10 @@ import {
151
179
  * Render a per-column add control that emits `recordadd`.
152
180
  * @property {boolean|MenuItem[]|((context:KanbanCardContext)=>MenuItem[])} [contextMenu=false]
153
181
  * Card context menu. `true` offers move commands; an array or function appends application items.
154
- * @property {((context:KanbanCardContext)=>Node|null|undefined)|null} [renderCard=null] Replaces card content.
182
+ * @property {((context:KanbanCardContext)=>Node|null|undefined)|null} [renderCard=null] Replaces the
183
+ * shared record-card content — eyebrow, titles, and metadata. The preview, selection control,
184
+ * action group, move handle, rule badges, and any configured card head and footer still render,
185
+ * because each of those answers to its own option; clear those options to remove them.
155
186
  * @property {((context:KanbanHeaderContext)=>Node|null|undefined)|null} [renderColumnHeader=null] Replaces column header content.
156
187
  * @property {((context:KanbanHeaderContext)=>Node|null|undefined)|null} [renderSwimlaneHeader=null] Replaces lane header content.
157
188
  * @property {((context:KanbanHeaderContext)=>Node|null|undefined)|null} [renderColumnEmpty=null] Empty-column placeholder.
@@ -176,6 +207,18 @@ import {
176
207
  /** @typedef {{record:KanbanRecord,id:unknown,index:number,column:string,lane:string|null}} KanbanCardMeta */
177
208
  /** @typedef {{column:string,lane:string|null,list:HTMLElement}} KanbanSectionMeta */
178
209
  /** @typedef {{ids:unknown[],from:KanbanMovePoint,to:KanbanMovePoint}} KanbanKeyboardMove */
210
+ /** @typedef {((record:KanbanRecord,index:number)=>unknown)|null} KanbanCardReader */
211
+ /**
212
+ * @typedef {Object} KanbanCardSources
213
+ * @property {KanbanCardReader} identifier Record-key reader.
214
+ * @property {KanbanCardReader} status Status reader.
215
+ * @property {KanbanCardReader} priority Priority reader.
216
+ * @property {KanbanCardReader} progress Completion reader.
217
+ * @property {KanbanCardReader} assignees Responsible-people reader.
218
+ * @property {KanbanCardReader} entityIcon Entity-icon reader.
219
+ * @property {Set<string>} fieldIds Field ids the anatomy consumes.
220
+ * @property {boolean} any Whether any source is configured.
221
+ */
179
222
 
180
223
  const INTERACTIVE = 'a, button, input, select, textarea, summary, [contenteditable="true"]';
181
224
 
@@ -217,7 +260,14 @@ const DEFAULT_LABELS = Object.freeze({
217
260
  countLimit: '%count% records, work-in-progress limit %limit%',
218
261
  countLimitInLane: '%count% records in this lane, %total% of work-in-progress limit %limit% in the column',
219
262
  empty: 'No records',
220
- columnEmpty: '',
263
+ columnEmpty: 'Drop a card here',
264
+ identifier: 'Record',
265
+ priorityLabel: 'Priority',
266
+ statusLabel: 'Status',
267
+ progress: 'Progress',
268
+ progressValue: '%percent%%',
269
+ assignees: 'Assigned to %names%',
270
+ assigneeOverflow: '+%count%',
221
271
  selected: 'Selected',
222
272
  notSelected: 'Not selected',
223
273
  noMatches: 'No cards match the current search.',
@@ -247,6 +297,11 @@ const DEFAULT_LABELS = Object.freeze({
247
297
  });
248
298
 
249
299
  /**
300
+ * A field consumed by the card head or footer — the `identifier`, `status`, `priority`,
301
+ * `progress`, or `assignees` source — is dropped from the shared metadata list so the same value
302
+ * is never shown twice. `field.duplicate === true` opts it back in, extending the rule the shared
303
+ * record card already applies to title and subtitle fields.
304
+ *
250
305
  * Configurable record board with semantic columns, optional swim lanes, and equivalent pointer,
251
306
  * touch, keyboard, context-menu, and programmatic movement. Local moves clone records; callback
252
307
  * grouping remains fully usable for display and external moves but cannot be written safely by
@@ -265,6 +320,7 @@ export class KanbanView extends RecordView {
265
320
  /** @type {Readonly<KanbanViewOptions & RecordViewOptions>} */
266
321
  static defaults = {
267
322
  ...RecordView.defaults,
323
+ selectionTrigger: 'checkbox',
268
324
  columnBy: 'status',
269
325
  columns: null,
270
326
  swimlaneBy: null,
@@ -277,6 +333,17 @@ export class KanbanView extends RecordView {
277
333
  actions: [],
278
334
  moveMode: 'local',
279
335
  dragFrom: 'card',
336
+ moveHandle: 'keyboard',
337
+ entityIcon: null,
338
+ identifier: null,
339
+ status: null,
340
+ statusTones: {},
341
+ priority: null,
342
+ priorityTones: {},
343
+ progress: null,
344
+ progressMax: 100,
345
+ assignees: null,
346
+ maxAvatars: 3,
280
347
  multiMove: true,
281
348
  autoScroll: true,
282
349
  wipPolicy: 'warn',
@@ -354,13 +421,24 @@ export class KanbanView extends RecordView {
354
421
  this._sectionMeta = new WeakMap();
355
422
  this._cardsById = new Map();
356
423
  this._visibleIds = new Set();
424
+ this._cardSources = /** @type {KanbanCardSources} */ ({
425
+ identifier: null, status: null, priority: null, progress: null, assignees: null,
426
+ entityIcon: null, fieldIds: new Set(), any: false
427
+ });
428
+ this._fieldIndex = /** @type {Map<string, any>|null} */ (null);
429
+ this._cardFields = /** @type {{all:any[],metadata:any[]}|null} */ (null);
357
430
  this._kanbanMenu = /** @type {ContextMenu|null} */ (null);
358
431
  this._searchInput = /** @type {HTMLInputElement|null} */ (null);
359
432
  this._undoButton = /** @type {HTMLButtonElement|null} */ (null);
360
433
  this._redoButton = /** @type {HTMLButtonElement|null} */ (null);
361
434
 
362
435
  this._toolbar = h('div', { class: 'zx-record-view__toolbar zx-kanban-view__toolbar' });
363
- if (options.fieldControls) this._toolbar.append(this._createViewFieldControls(this._text('fields')));
436
+ // A label the host passed in the object form wins; otherwise Kanban's own
437
+ // message bag supplies it, which is where this label has always come from.
438
+ if (options.fieldControls) {
439
+ const configured = normalizeViewFieldControls(options.fieldControls).label;
440
+ this._toolbar.append(this._createViewFieldControls(configured ?? this._text('fields')));
441
+ }
364
442
  if (options.searchControl) this._toolbar.append(this._createSearchControl());
365
443
  if (options.historyControls) this._toolbar.append(this._createHistoryControls());
366
444
  this._board = h('div', {
@@ -881,8 +959,12 @@ export class KanbanView extends RecordView {
881
959
  id: String(/** @type {HTMLElement} */ (activeCollapse).dataset.kanbanId),
882
960
  lane: activeSection ? this._sectionMeta.get(activeSection)?.lane ?? null : null
883
961
  } : null;
962
+ // Any change that alters field descriptors also refreshes, so this is the invalidation point.
963
+ this._fieldIndex = null;
964
+ this._cardFields = null;
884
965
  const columns = this._resolvedColumns();
885
966
  const lanes = this._resolvedSwimlanes();
967
+ this._resolveCardSources();
886
968
  this._cardMeta = new WeakMap();
887
969
  this._sectionMeta = new WeakMap();
888
970
  this._cardsById = new Map();
@@ -1034,16 +1116,21 @@ export class KanbanView extends RecordView {
1034
1116
  const grabbed = Boolean(this._keyboardMove?.ids.some((candidate) => Object.is(candidate, id)));
1035
1117
  const rules = resolveKanbanRules(this._kanbanRules, record, { index, column, lane });
1036
1118
  const multiple = grabbed && this._keyboardMove ? this._keyboardMove.ids.length : 1;
1119
+ // A handle that is the only drag source must be visible, whatever `moveHandle` asks for.
1120
+ const reveal = options.moveHandle === 'always' || options.dragFrom === 'handle'
1121
+ ? 'always' : 'keyboard';
1037
1122
  const handle = h('button', {
1038
1123
  class: 'zx-kanban-view__move-handle', type: 'button',
1039
1124
  ariaPressed: String(grabbed),
1125
+ dataset: { reveal },
1040
1126
  ariaLabel: multiple > 1
1041
1127
  ? this._text('moveMultiple', { count: multiple })
1042
1128
  : this._text('move', { name: this._recordName(record, index) })
1043
1129
  }, icon('drag', { size: 13 }));
1130
+ const cardFields = this._resolvedCardFields();
1044
1131
  const card = createRecordCard(record, index, {
1045
- fields: this.getFields(),
1046
- visibleFields: this.getVisibleFields(),
1132
+ fields: cardFields.all,
1133
+ visibleFields: cardFields.metadata,
1047
1134
  titleField: options.titleField,
1048
1135
  subtitleField: options.subtitleField,
1049
1136
  preview: options.preview ?? undefined,
@@ -1051,6 +1138,7 @@ export class KanbanView extends RecordView {
1051
1138
  link: options.link ?? undefined,
1052
1139
  actions: options.actions,
1053
1140
  selectable: options.selectable,
1141
+ selectionTrigger: options.selectionTrigger,
1054
1142
  selected: this._isViewSelected(id),
1055
1143
  variant: options.variant,
1056
1144
  headingLevel: hasLanes ? 4 : 3,
@@ -1060,6 +1148,7 @@ export class KanbanView extends RecordView {
1060
1148
  if (grabbed) card.dataset.grabbed = 'true';
1061
1149
  this._applyCardContent(card, { record, id, index, column, lane, rules, selected: this._isViewSelected(id) });
1062
1150
  this._applyCardRules(card, rules);
1151
+ this._applyCardAnatomy(card, record, index);
1063
1152
  this._applyCardDescription(card, id);
1064
1153
  const meta = { record, id, index, column, lane };
1065
1154
  this._cardMeta.set(card, meta);
@@ -1067,6 +1156,137 @@ export class KanbanView extends RecordView {
1067
1156
  return card;
1068
1157
  }
1069
1158
 
1159
+ /**
1160
+ * Builds the two field arrays every card needs, once per refresh.
1161
+ * @returns {{all:import('../view/record-view.js').ViewField[],metadata:import('../view/record-view.js').ViewField[]}}
1162
+ */
1163
+ _resolvedCardFields() {
1164
+ if (!this._cardFields) {
1165
+ this._cardFields = { all: this.getFields(), metadata: this._cardMetadataFields() };
1166
+ }
1167
+ return this._cardFields;
1168
+ }
1169
+
1170
+ /**
1171
+ * Returns the fields the shared metadata list may show. A field the card head or footer already
1172
+ * projects is dropped, because repeating it as a labelled row is the same value twice; a field
1173
+ * marked `duplicate` opts back in, matching how title and subtitle already behave.
1174
+ * @returns {import('../view/record-view.js').ViewField[]} Metadata fields.
1175
+ */
1176
+ _cardMetadataFields() {
1177
+ const consumed = this._cardSources?.fieldIds;
1178
+ const visible = this.getVisibleFields();
1179
+ if (!consumed?.size) return visible;
1180
+ return visible.filter((field) => !consumed.has(field.id) || field.duplicate === true);
1181
+ }
1182
+
1183
+ /**
1184
+ * Resolves the card-anatomy readers once per refresh. A field id would otherwise be looked up
1185
+ * through `getFields()` — which clones every descriptor — once per source per card, so a board
1186
+ * with six anatomy sources rebuilt the whole field array six times for every card it drew.
1187
+ * @returns {void}
1188
+ */
1189
+ _resolveCardSources() {
1190
+ const options = this._kanbanOptions();
1191
+ const fields = this.getFields();
1192
+ /** @type {Set<string>} */
1193
+ const fieldIds = new Set();
1194
+ /** @param {unknown} source @returns {((record:KanbanRecord,index:number)=>unknown)|null} */
1195
+ const reader = (source) => {
1196
+ if (source == null) return null;
1197
+ if (typeof source === 'function') return /** @type {any} */ (source);
1198
+ // A literal descriptor is content, not an accessor: reading it as a property name would
1199
+ // silently resolve undefined and render nothing.
1200
+ if (typeof source === 'object') return () => source;
1201
+ const id = String(source);
1202
+ const field = fields.find((candidate) => candidate.id === id);
1203
+ fieldIds.add(id);
1204
+ return field
1205
+ ? (record, index) => readViewField(field, record, index) : (record) => record?.[id];
1206
+ };
1207
+ const sources = {
1208
+ identifier: reader(options.identifier),
1209
+ status: reader(options.status),
1210
+ priority: reader(options.priority),
1211
+ progress: reader(options.progress),
1212
+ assignees: reader(options.assignees),
1213
+ entityIcon: typeof options.entityIcon === 'function' ? options.entityIcon
1214
+ : options.entityIcon == null ? null : () => options.entityIcon
1215
+ };
1216
+ this._cardSources = /** @type {KanbanCardSources} */ ({
1217
+ ...sources,
1218
+ fieldIds,
1219
+ any: Object.values(sources).some(Boolean)
1220
+ });
1221
+ }
1222
+
1223
+ /**
1224
+ * Adds the two Kanban-owned regions that bracket the shared record-card anatomy: a head naming
1225
+ * what the record is and how urgent it is, and a footer carrying completion and the people
1226
+ * responsible. Each part is omitted when its option is unset, so a board that configures none of
1227
+ * them renders exactly the card it rendered before.
1228
+ * @param {HTMLElement} card Card element.
1229
+ * @param {KanbanRecord} record Record.
1230
+ * @param {number} index Display index.
1231
+ * @returns {void}
1232
+ */
1233
+ _applyCardAnatomy(card, record, index) {
1234
+ const options = this._kanbanOptions();
1235
+ const sources = this._cardSources;
1236
+ if (!sources?.any) return;
1237
+ const body = card.querySelector('.zx-record-card__body');
1238
+ if (!body) return;
1239
+ const entityIcon = sources.entityIcon?.(record, index);
1240
+ const identifier = sources.identifier?.(record, index);
1241
+ const head = createKanbanCardHead({
1242
+ entityIcon: entityIcon == null || entityIcon === '' ? null : String(entityIcon),
1243
+ identifier: identifier == null ? '' : String(identifier),
1244
+ identifierPrefix: this._text('identifier'),
1245
+ flags: [
1246
+ {
1247
+ indicator: this._cardIndicator(sources.status, options.statusTones, record, index),
1248
+ prefix: this._text('statusLabel')
1249
+ },
1250
+ {
1251
+ indicator: this._cardIndicator(sources.priority, options.priorityTones, record, index),
1252
+ prefix: this._text('priorityLabel')
1253
+ }
1254
+ ]
1255
+ });
1256
+ if (head) body.prepend(head);
1257
+
1258
+ const footer = [];
1259
+ const progress = resolveKanbanProgress(
1260
+ sources.progress?.(record, index), Number(options.progressMax));
1261
+ if (progress) {
1262
+ footer.push(createKanbanProgress(progress,
1263
+ progress.label ?? this._text('progress'),
1264
+ this._text('progressValue', { percent: progress.percent })));
1265
+ }
1266
+ const people = resolveKanbanAssignees(sources.assignees?.(record, index));
1267
+ const avatars = createKanbanAvatars(people, Number(options.maxAvatars),
1268
+ this._text('assignees', { names: people.map((person) => person.name).filter(Boolean).join(', ') }),
1269
+ (count) => this._text('assigneeOverflow', { count }));
1270
+ if (avatars) footer.push(avatars);
1271
+ if (footer.length) body.append(h('div', { class: 'zx-kanban-view__card-footer' }, footer));
1272
+ }
1273
+
1274
+ /**
1275
+ * Resolves one indicator from an already-resolved reader. A reader may return a ready-made
1276
+ * `Node`, which is adopted rather than described, so an application keeps full control.
1277
+ * @param {((record:KanbanRecord,index:number)=>unknown)|null} source Resolved reader.
1278
+ * @param {unknown} tones Value-to-tone map.
1279
+ * @param {KanbanRecord} record Record.
1280
+ * @param {number} index Display index.
1281
+ * @returns {KanbanIndicator|Node|null} Indicator, node, or null.
1282
+ */
1283
+ _cardIndicator(source, tones, record, index) {
1284
+ if (!source) return null;
1285
+ const value = source(record, index);
1286
+ if (value && typeof value === 'object' && 'nodeType' in value) return /** @type {Node} */ (value);
1287
+ return resolveKanbanIndicator(value, /** @type {Record<string,string>} */ (tones ?? {}));
1288
+ }
1289
+
1070
1290
  /**
1071
1291
  * Hands card content to `renderCard` while keeping the managed shell: the list item, its
1072
1292
  * selection control, the action group, and the move handle are never given away, because they
@@ -1220,10 +1440,24 @@ export class KanbanView extends RecordView {
1220
1440
  _readAxis(accessor, record, index) {
1221
1441
  if (typeof accessor === 'function') return accessor(record, index);
1222
1442
  if (accessor == null) return null;
1223
- const field = this.getFields().find((candidate) => candidate.id === accessor);
1443
+ const field = this._axisField(String(accessor));
1224
1444
  return field ? readViewField(field, record, index) : record?.[accessor];
1225
1445
  }
1226
1446
 
1447
+ /**
1448
+ * Resolves one field descriptor by id through a per-refresh index. `getFields()` rebuilds a Map
1449
+ * and clones every descriptor on each call, and a string column or lane accessor is read once
1450
+ * per record per column per lane — which made this the most expensive line in a board render.
1451
+ * @param {string} id Field id.
1452
+ * @returns {import('../view/record-view.js').ViewField|undefined} Descriptor, if any.
1453
+ */
1454
+ _axisField(id) {
1455
+ if (!this._fieldIndex) {
1456
+ this._fieldIndex = new Map(this.getFields().map((field) => [field.id, field]));
1457
+ }
1458
+ return this._fieldIndex.get(id);
1459
+ }
1460
+
1227
1461
  /** @param {string} column @param {string|null} lane @returns {{record:KanbanRecord,index:number}[]} */
1228
1462
  _recordsIn(column, lane) {
1229
1463
  const options = this._kanbanOptions();
@@ -1508,12 +1742,12 @@ export class KanbanView extends RecordView {
1508
1742
  /** @returns {HTMLElement} Built-in history controls. */
1509
1743
  _createHistoryControls() {
1510
1744
  this._undoButton = /** @type {HTMLButtonElement} */ (h('button', {
1511
- class: 'zx-button zx-button--sm', type: 'button', disabled: true,
1512
- dataset: { kanbanHistory: 'undo' }
1745
+ class: 'zx-btn', type: 'button', disabled: true,
1746
+ dataset: { kanbanHistory: 'undo', size: 'sm', kind: 'ghost' }
1513
1747
  }, this._text('undo')));
1514
1748
  this._redoButton = /** @type {HTMLButtonElement} */ (h('button', {
1515
- class: 'zx-button zx-button--sm', type: 'button', disabled: true,
1516
- dataset: { kanbanHistory: 'redo' }
1749
+ class: 'zx-btn', type: 'button', disabled: true,
1750
+ dataset: { kanbanHistory: 'redo', size: 'sm', kind: 'ghost' }
1517
1751
  }, this._text('redo')));
1518
1752
  const group = h('div', { class: 'zx-kanban-view__history', role: 'group' },
1519
1753
  this._undoButton, this._redoButton);
@@ -1689,6 +1923,11 @@ export class KanbanView extends RecordView {
1689
1923
  }
1690
1924
  if (target?.closest('.zx-kanban-view__move-handle, .zx-record-card__selection, [data-record-select]')) return;
1691
1925
  if (target?.closest(INTERACTIVE) || hasTextSelection()) return;
1926
+ if (this.options.selectable && this._kanbanOptions().selectionTrigger === 'card') {
1927
+ this.toggleSelection(meta.id, { range: event.shiftKey });
1928
+ /** @type {HTMLElement|null} */ (target?.closest('.zx-record-card'))?.focus({ preventScroll: true });
1929
+ return;
1930
+ }
1692
1931
  this.emit('recordclick', { ...meta, event });
1693
1932
  if (this.options.selectable === 'single' && !this._isViewSelected(meta.id)) {
1694
1933
  this.setSelection([meta.id]);
@@ -1724,6 +1963,10 @@ export class KanbanView extends RecordView {
1724
1963
  if (!cardMeta || target !== card) return;
1725
1964
  if (event.key === 'Enter') {
1726
1965
  event.preventDefault();
1966
+ if (this.options.selectable && this._kanbanOptions().selectionTrigger === 'card') {
1967
+ this.toggleSelection(cardMeta.id);
1968
+ return;
1969
+ }
1727
1970
  this.emit('recordclick', { ...cardMeta, event });
1728
1971
  if (this.options.selectable === 'single') this.setSelection([cardMeta.id]);
1729
1972
  } else if (event.key === ' ' && this.options.selectable !== false) {
@@ -2,7 +2,7 @@
2
2
  inline-size: min(760px, calc(100vw - (2 * var(--zx-space-4))));
3
3
  max-block-size: min(720px, calc(100vh - (2 * var(--zx-space-6))));
4
4
  border: 1px solid var(--zx-color-overlay-border);
5
- border-radius: var(--zx-radius-xl);
5
+ border-radius: var(--zx-radius-lg);
6
6
  background-color: var(--zx-color-overlay-panel);
7
7
  background-image: var(--zx-overlay-panel-image);
8
8
  background-clip: padding-box;
@@ -29,24 +29,60 @@
29
29
 
30
30
  .zx-launcher__surface {
31
31
  display: grid;
32
+ /* One column no wider than the dialog: a long page chip or a wide row shrinks inside it rather
33
+ than pushing the close button off a phone screen. */
34
+ grid-template-columns: minmax(0, 1fr);
32
35
  grid-template-rows: auto auto minmax(0, 1fr) auto;
33
36
  max-block-size: inherit;
34
37
  }
35
38
 
36
39
  .zx-launcher__search {
37
- display: grid;
38
- grid-template-columns: auto minmax(0, 1fr) auto auto;
40
+ display: flex;
39
41
  align-items: center;
40
42
  gap: var(--zx-space-3);
41
43
  border-block-end: 1px solid var(--zx-color-overlay-divider);
42
44
  padding: var(--zx-space-3) var(--zx-space-4);
43
45
  }
44
46
 
45
- .zx-launcher__search[data-shortcut="false"] {
46
- grid-template-columns: auto minmax(0, 1fr) auto;
47
+ .zx-launcher__search > .zx-icon {
48
+ flex: none;
49
+ }
50
+
51
+ /* The page you are on, before the query: where you are, so neutral rather than accent. Its × is
52
+ one step back. */
53
+ .zx-launcher__scope {
54
+ display: inline-flex;
55
+ flex: 0 1 auto;
56
+ align-items: center;
57
+ gap: var(--zx-space-1);
58
+ min-inline-size: 0;
59
+ max-inline-size: 50%;
60
+ border-radius: var(--zx-radius-sm);
61
+ background: var(--zx-color-bg-current);
62
+ padding-inline: var(--zx-space-2) 2px;
63
+ color: var(--zx-color-text);
64
+ font-size: var(--zx-text-sm);
65
+ font-weight: var(--zx-weight-medium);
66
+ line-height: 1.5;
67
+ white-space: nowrap;
68
+ }
69
+
70
+ .zx-launcher__scope[hidden] { display: none; }
71
+
72
+ .zx-launcher__scope-label {
73
+ overflow: hidden;
74
+ text-overflow: ellipsis;
75
+ }
76
+
77
+ /* A 24px target (WCAG 2.5.8); the chip's own padding stays slim beside it. */
78
+ .zx-launcher__scope .zx-launcher__scope-back {
79
+ inline-size: 24px;
80
+ block-size: 24px;
47
81
  }
48
82
 
49
83
  .zx-launcher__input {
84
+ /* The query yields its room first; the chip keeps its label until it reaches its cap. */
85
+ flex: 1 1 0;
50
86
  min-inline-size: 0;
51
87
  border: 0;
52
88
  outline: 0;
@@ -62,7 +98,8 @@
62
98
  .zx-launcher__input::-webkit-search-cancel-button { display: none; }
63
99
 
64
100
  .zx-launcher__shortcut,
65
- .zx-launcher__hints kbd {
101
+ .zx-launcher__hints kbd,
102
+ .zx-launcher__key {
66
103
  border: 1px solid var(--zx-color-border-control);
67
104
  border-radius: var(--zx-radius-sm);
68
105
  background: var(--zx-color-bg-muted);
@@ -74,6 +111,7 @@
74
111
  }
75
112
 
76
113
  .zx-launcher__shortcut {
114
+ flex: none;
77
115
  padding-inline: var(--zx-space-2);
78
116
  white-space: nowrap;
79
117
  }
@@ -90,16 +128,16 @@
90
128
  .zx-launcher__results {
91
129
  min-block-size: 0;
92
130
  overflow: auto;
93
- padding: var(--zx-space-2);
131
+ padding: var(--zx-space-1);
94
132
  }
95
133
 
134
+ /* A caption above its rows, never taller than one of them; inset like the rows so the label and
135
+ the labels under it start on one line. */
96
136
  .zx-launcher__group-label {
97
- padding: var(--zx-space-3) var(--zx-space-3) var(--zx-space-1);
137
+ padding: var(--zx-space-2) var(--zx-space-3) var(--zx-space-1);
98
138
  color: var(--zx-color-text-muted);
99
139
  font-size: var(--zx-text-xs);
100
- font-weight: 700;
101
- letter-spacing: .04em;
102
- text-transform: uppercase;
140
+ font-weight: var(--zx-weight-semibold);
103
141
  }
104
142
 
105
143
  .zx-launcher__group[data-layout="grid"] {
@@ -119,11 +157,11 @@
119
157
  align-items: center;
120
158
  gap: var(--zx-space-3);
121
159
  inline-size: 100%;
122
- min-block-size: calc(var(--zx-control-height) + var(--zx-space-2));
160
+ min-block-size: var(--zx-control-height);
123
161
  border: 0;
124
- border-radius: var(--zx-radius-md);
162
+ border-radius: var(--zx-radius-sm);
125
163
  background: transparent;
126
- padding: var(--zx-space-2) var(--zx-space-3);
164
+ padding: var(--zx-space-1) var(--zx-space-3);
127
165
  color: inherit;
128
166
  text-align: start;
129
167
  text-decoration: none;
@@ -134,9 +172,15 @@
134
172
  background: var(--zx-color-overlay-hover);
135
173
  }
136
174
 
175
+ /* The application you are in is where you are, not a pick: the neutral current fill and a
176
+ heavier label. The highlighted row stays the lighter hover step. */
137
177
  .zx-launcher__option[aria-current="page"] {
138
- background: var(--zx-color-overlay-selected);
139
- color: var(--zx-color-accent);
178
+ background: var(--zx-color-bg-current);
179
+ color: var(--zx-color-text);
180
+ }
181
+
182
+ .zx-launcher__option[aria-current="page"] .zx-launcher__label {
183
+ font-weight: var(--zx-weight-semibold);
140
184
  }
141
185
 
142
186
  .zx-launcher__option[data-kind="application"] {
@@ -145,12 +189,13 @@
145
189
  justify-items: center;
146
190
  align-content: center;
147
191
  min-block-size: 92px;
192
+ border-radius: var(--zx-radius-md);
148
193
  padding: var(--zx-space-3) var(--zx-space-2);
149
194
  text-align: center;
150
195
  }
151
196
 
152
197
  .zx-launcher__option[data-kind="application"][aria-current="page"] {
153
- background: var(--zx-color-overlay-selected);
198
+ background: var(--zx-color-bg-current);
154
199
  }
155
200
 
156
201
  .zx-launcher__option[data-kind="application"] .zx-launcher__icon {
@@ -162,8 +207,9 @@
162
207
  inline-size: 100%;
163
208
  }
164
209
 
165
- .zx-launcher__option[data-kind="application"] .zx-launcher__description,
166
- .zx-launcher__option[data-kind="application"] .zx-launcher__badge {
210
+ .zx-launcher__option[data-kind="application"] :is(
211
+ .zx-launcher__description, .zx-launcher__badge, .zx-launcher__hint, .zx-launcher__keys, .zx-launcher__chevron
212
+ ) {
167
213
  display: none;
168
214
  }
169
215
 
@@ -179,10 +225,47 @@
179
225
 
180
226
  .zx-launcher__icon { display: inline-flex; flex: none; }
181
227
  .zx-launcher__copy { display: grid; min-inline-size: 0; gap: 2px; }
182
- .zx-launcher__label { overflow: hidden; font-weight: 600; text-overflow: ellipsis; white-space: nowrap; }
228
+ .zx-launcher__label { overflow: hidden; font-weight: var(--zx-weight-medium); text-overflow: ellipsis; white-space: nowrap; }
183
229
  .zx-launcher__description { overflow: hidden; color: var(--zx-color-text-muted); font-size: var(--zx-text-sm); text-overflow: ellipsis; white-space: nowrap; }
184
230
  .zx-launcher__badge { margin-inline-start: auto; color: var(--zx-color-text-muted); font-size: var(--zx-text-xs); }
185
231
 
232
+ /* A row's copy takes the room; what trails it (badge, hint, key caps, the page chevron) sits at
233
+ the end in that order. */
234
+ .zx-launcher__option:not([data-kind="application"]) .zx-launcher__copy { flex: 1 1 auto; }
235
+
236
+ .zx-launcher__hint {
237
+ flex: 0 1 auto;
238
+ min-inline-size: 0;
239
+ max-inline-size: 40%;
240
+ overflow: hidden;
241
+ color: var(--zx-color-text-muted);
242
+ font-size: var(--zx-text-sm);
243
+ text-overflow: ellipsis;
244
+ white-space: nowrap;
245
+ }
246
+
247
+ .zx-launcher__keys {
248
+ display: inline-flex;
249
+ flex: none;
250
+ gap: var(--zx-space-1);
251
+ }
252
+
253
+ .zx-launcher__chevron {
254
+ display: inline-flex;
255
+ flex: none;
256
+ color: var(--zx-color-text-subtle);
257
+ }
258
+
259
+ /* Spoken, not drawn: where a page change landed. */
260
+ .zx-launcher__announce {
261
+ position: absolute;
262
+ inline-size: 1px;
263
+ block-size: 1px;
264
+ overflow: hidden;
265
+ clip-path: inset(50%);
266
+ white-space: nowrap;
267
+ }
268
+
186
269
  .zx-launcher__hints {
187
270
  display: flex;
188
271
  flex-wrap: wrap;
@@ -200,7 +283,10 @@
200
283
  gap: var(--zx-space-1);
201
284
  }
202
285
 
203
- .zx-launcher__hints kbd {
286
+ .zx-launcher__hints span[hidden] { display: none; }
287
+
288
+ .zx-launcher__hints kbd,
289
+ .zx-launcher__key {
204
290
  min-inline-size: 1.5rem;
205
291
  padding-inline: 4px;
206
292
  text-align: center;
@@ -215,10 +301,6 @@
215
301
  display: none;
216
302
  }
217
303
 
218
- .zx-launcher__search {
219
- grid-template-columns: auto minmax(0, 1fr) auto;
220
- }
221
-
222
304
  .zx-launcher__hints {
223
305
  justify-content: center;
224
306
  }