@zeyos/zx 4.4.0 → 5.2.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 (252) hide show
  1. package/CHANGELOG.md +447 -0
  2. package/README.md +44 -11
  3. package/dist/types/components/account-menu/account-menu.d.ts +8 -0
  4. package/dist/types/components/activity-list/activity-list.d.ts +31 -1
  5. package/dist/types/components/app-icon/app-icon.d.ts +14 -2
  6. package/dist/types/components/app-sidebar/app-sidebar.d.ts +293 -17
  7. package/dist/types/components/avatar/avatar.d.ts +46 -7
  8. package/dist/types/components/badge/badge.d.ts +21 -1
  9. package/dist/types/components/button/button.d.ts +15 -0
  10. package/dist/types/components/calendar/calendar.d.ts +67 -5
  11. package/dist/types/components/card-view/card-view.d.ts +7 -4
  12. package/dist/types/components/card-view/record-card.d.ts +11 -0
  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 +6 -17
  16. package/dist/types/components/context-menu/context-menu.d.ts +6 -2
  17. package/dist/types/components/data-filter/data-filter.d.ts +4 -4
  18. package/dist/types/components/datebox/date-range-box.d.ts +5 -1
  19. package/dist/types/components/datebox/datebox.d.ts +7 -2
  20. package/dist/types/components/dialog/dialog.d.ts +46 -6
  21. package/dist/types/components/dropdown/dropdown.d.ts +24 -9
  22. package/dist/types/components/entity-ref/entity-ref.d.ts +2 -2
  23. package/dist/types/components/field/field.d.ts +54 -2
  24. package/dist/types/components/fieldset/external-adapter.d.ts +83 -0
  25. package/dist/types/components/fieldset/fieldset.d.ts +26 -1
  26. package/dist/types/components/file-item/file-item.d.ts +34 -6
  27. package/dist/types/components/file-list/file-list.d.ts +35 -3
  28. package/dist/types/components/filter/filter.d.ts +127 -3
  29. package/dist/types/components/filter-panel/filter-panel.d.ts +36 -7
  30. package/dist/types/components/form/form.d.ts +12 -1
  31. package/dist/types/components/kanban-view/kanban-card.d.ts +8 -0
  32. package/dist/types/components/launcher/launcher.d.ts +268 -15
  33. package/dist/types/components/list-toolbar/list-toolbar.d.ts +16 -7
  34. package/dist/types/components/menu-button/menu-button.d.ts +39 -5
  35. package/dist/types/components/message/message.d.ts +132 -17
  36. package/dist/types/components/modal/modal.d.ts +43 -2
  37. package/dist/types/components/module-bar/module-bar.d.ts +395 -0
  38. package/dist/types/components/pagination/pagination.d.ts +17 -3
  39. package/dist/types/components/select/color.d.ts +12 -0
  40. package/dist/types/components/select/glyphs.d.ts +124 -0
  41. package/dist/types/components/select/select.d.ts +17 -3
  42. package/dist/types/components/sheet/sheet.d.ts +15 -0
  43. package/dist/types/components/stat-tile/stat-tile.d.ts +16 -5
  44. package/dist/types/components/tabbox/tabbox.d.ts +9 -0
  45. package/dist/types/components/table/table.d.ts +311 -18
  46. package/dist/types/components/tag-picker/tag-picker.d.ts +21 -4
  47. package/dist/types/components/tree/tree.d.ts +13 -0
  48. package/dist/types/components/view/record-view.d.ts +59 -2
  49. package/dist/types/components/view-settings/view-settings.d.ts +879 -0
  50. package/dist/types/components/workspace-switcher/workspace-switcher.d.ts +327 -0
  51. package/dist/types/core/breakpoint.d.ts +1 -1
  52. package/dist/types/core/component.d.ts +12 -2
  53. package/dist/types/core/i18n.d.ts +27 -3
  54. package/dist/types/core/icons.d.ts +23 -25
  55. package/dist/types/core/keyboard.d.ts +98 -1
  56. package/dist/types/core/messages.d.ts +382 -0
  57. package/dist/types/core/motion.d.ts +59 -0
  58. package/dist/types/core/overlay-host.d.ts +42 -1
  59. package/dist/types/core/overlay-stack.d.ts +260 -0
  60. package/dist/types/index.d.ts +10 -5
  61. package/dist/types/internal/app-rail.d.ts +38 -0
  62. package/dist/types/internal/menu-items.d.ts +71 -3
  63. package/dist/types/zeyos/connect.d.ts +6 -0
  64. package/dist/types/zeyos/form.d.ts +50 -9
  65. package/dist/types/zeyos/index.d.ts +2 -1
  66. package/dist/types/zeyos/launcher.d.ts +9 -0
  67. package/dist/types/zeyos/module-bar.d.ts +176 -0
  68. package/dist/types/zeyos/query.d.ts +110 -5
  69. package/dist/types/zeyos/schema.d.ts +24 -1
  70. package/dist/types/zeyos/table.d.ts +345 -22
  71. package/dist/types/zeyos/view.d.ts +26 -2
  72. package/dist/zx-zeyos.esm.js +6178 -1008
  73. package/dist/zx-zeyos.esm.min.js +53 -5
  74. package/dist/zx.css +2861 -827
  75. package/dist/zx.esm.js +8909 -1480
  76. package/dist/zx.esm.min.js +55 -7
  77. package/dist/zx.global.js +8911 -1482
  78. package/dist/zx.global.min.js +55 -7
  79. package/dist/zx.min.css +1 -1
  80. package/docs/api.json +1079 -117
  81. package/docs/llms.md +956 -154
  82. package/docs/llms.txt +9 -5
  83. package/package.json +1 -1
  84. package/src/components/account-menu/account-menu.css +35 -10
  85. package/src/components/account-menu/account-menu.js +12 -0
  86. package/src/components/activity-item/activity-item.css +1 -1
  87. package/src/components/activity-item/activity-item.js +3 -0
  88. package/src/components/activity-list/activity-list.css +38 -3
  89. package/src/components/activity-list/activity-list.js +61 -1
  90. package/src/components/app-icon/app-icon.css +14 -3
  91. package/src/components/app-icon/app-icon.js +11 -1
  92. package/src/components/app-rail/app-rail.css +59 -79
  93. package/src/components/app-sidebar/app-sidebar.css +304 -168
  94. package/src/components/app-sidebar/app-sidebar.js +667 -46
  95. package/src/components/aurora/aurora.css +5 -3
  96. package/src/components/avatar/avatar.css +32 -7
  97. package/src/components/avatar/avatar.js +72 -8
  98. package/src/components/badge/badge.css +54 -2
  99. package/src/components/badge/badge.js +29 -2
  100. package/src/components/breadcrumb/breadcrumb.css +1 -1
  101. package/src/components/button/button.css +67 -9
  102. package/src/components/button/button.js +38 -0
  103. package/src/components/calendar/calendar.css +47 -47
  104. package/src/components/calendar/calendar.js +26 -52
  105. package/src/components/card/card.css +4 -4
  106. package/src/components/card-view/card-view.css +14 -12
  107. package/src/components/card-view/card-view.js +13 -9
  108. package/src/components/card-view/record-card.js +14 -3
  109. package/src/components/chart/chart.css +4 -1
  110. package/src/components/chart/chart.js +12 -0
  111. package/src/components/check-button/check-button.css +10 -2
  112. package/src/components/checklist/checklist.css +9 -2
  113. package/src/components/checklist/checklist.js +11 -5
  114. package/src/components/checklist/field-adapter.js +3 -1
  115. package/src/components/code-editor/code-editor.css +7 -6
  116. package/src/components/code-editor/code-editor.js +19 -1
  117. package/src/components/code-editor/field-adapter.js +9 -2
  118. package/src/components/context-menu/context-menu.css +9 -4
  119. package/src/components/context-menu/context-menu.js +8 -2
  120. package/src/components/copy/copy-button.css +6 -0
  121. package/src/components/copy/copy-input.css +9 -4
  122. package/src/components/data-filter/data-filter.css +5 -4
  123. package/src/components/data-filter/data-filter.js +2 -2
  124. package/src/components/date-picker/date-picker.css +6 -6
  125. package/src/components/date-picker/date-range-picker.css +3 -3
  126. package/src/components/date-picker/time-picker.css +3 -3
  127. package/src/components/datebox/date-range-box.css +79 -2
  128. package/src/components/datebox/date-range-box.js +13 -2
  129. package/src/components/datebox/datebox.css +79 -2
  130. package/src/components/datebox/datebox.js +13 -2
  131. package/src/components/dialog/dialog.css +26 -0
  132. package/src/components/dialog/dialog.js +99 -17
  133. package/src/components/dock/dock.css +5 -4
  134. package/src/components/dropdown/dropdown.css +1 -1
  135. package/src/components/dropdown/dropdown.js +159 -72
  136. package/src/components/empty-state/empty-state.css +2 -2
  137. package/src/components/entity-ref/entity-ref.css +1 -1
  138. package/src/components/entity-ref/entity-ref.js +1 -1
  139. package/src/components/field/field.css +28 -9
  140. package/src/components/field/field.js +101 -29
  141. package/src/components/field-upload/field-upload.css +1 -1
  142. package/src/components/fieldset/external-adapter.js +363 -0
  143. package/src/components/fieldset/fieldset.css +5 -3
  144. package/src/components/fieldset/fieldset.js +38 -64
  145. package/src/components/file-item/file-item.js +144 -10
  146. package/src/components/file-list/file-list.js +144 -21
  147. package/src/components/filter/filter.css +86 -2
  148. package/src/components/filter/filter.js +307 -11
  149. package/src/components/filter-panel/filter-panel.css +1 -1
  150. package/src/components/filter-panel/filter-panel.js +149 -32
  151. package/src/components/finder/finder.css +16 -14
  152. package/src/components/form/form.css +5 -4
  153. package/src/components/form/form.js +11 -2
  154. package/src/components/grid/grid.css +1 -1
  155. package/src/components/groupbox/groupbox.css +1 -1
  156. package/src/components/kanban-view/kanban-card.js +11 -3
  157. package/src/components/kanban-view/kanban-view.css +42 -18
  158. package/src/components/kanban-view/kanban-view.js +9 -6
  159. package/src/components/launcher/launcher.css +108 -26
  160. package/src/components/launcher/launcher.js +530 -42
  161. package/src/components/list-toolbar/list-toolbar.css +13 -8
  162. package/src/components/list-toolbar/list-toolbar.js +40 -14
  163. package/src/components/master-panel/master-panel.css +2 -0
  164. package/src/components/master-panel/master-panel.js +0 -1
  165. package/src/components/menu-button/menu-button.css +72 -4
  166. package/src/components/menu-button/menu-button.js +160 -18
  167. package/src/components/message/message.css +33 -3
  168. package/src/components/message/message.js +264 -22
  169. package/src/components/modal/modal.css +1 -1
  170. package/src/components/modal/modal.js +83 -15
  171. package/src/components/module-bar/module-bar.css +486 -0
  172. package/src/components/module-bar/module-bar.js +1074 -0
  173. package/src/components/multi-value-editor/multi-value-editor.css +1 -1
  174. package/src/components/navigation-bar/navigation-bar.css +18 -9
  175. package/src/components/number-field/field-adapter.js +3 -1
  176. package/src/components/number-field/number-field.css +70 -0
  177. package/src/components/number-field/number-field.js +2 -0
  178. package/src/components/pagination/pagination.css +13 -11
  179. package/src/components/pagination/pagination.js +20 -4
  180. package/src/components/panel/panel.css +4 -3
  181. package/src/components/questionnaire/questionnaire.css +4 -4
  182. package/src/components/rating/field-adapter.js +2 -1
  183. package/src/components/rating/rating.css +8 -1
  184. package/src/components/search/search.css +4 -1
  185. package/src/components/select/color.js +22 -0
  186. package/src/components/select/field-adapter.js +2 -0
  187. package/src/components/select/glyphs.js +165 -0
  188. package/src/components/select/select.css +138 -45
  189. package/src/components/select/select.js +92 -47
  190. package/src/components/sheet/sheet.css +12 -9
  191. package/src/components/sheet/sheet.js +74 -32
  192. package/src/components/slider/field-adapter.js +6 -1
  193. package/src/components/slider/slider.css +18 -4
  194. package/src/components/slider/slider.js +1 -1
  195. package/src/components/sort-control/sort-control.css +4 -1
  196. package/src/components/split-view/split-view.css +1 -1
  197. package/src/components/stat-tile/stat-tile.css +42 -47
  198. package/src/components/stat-tile/stat-tile.js +17 -6
  199. package/src/components/stepper/stepper.css +1 -1
  200. package/src/components/tabbox/tabbox.css +41 -32
  201. package/src/components/tabbox/tabbox.js +5 -1
  202. package/src/components/table/table.css +187 -37
  203. package/src/components/table/table.js +718 -73
  204. package/src/components/tag-picker/field-adapter.js +2 -1
  205. package/src/components/tag-picker/tag-picker.css +83 -5
  206. package/src/components/tag-picker/tag-picker.js +25 -8
  207. package/src/components/timebox/timebox.css +22 -2
  208. package/src/components/toggle/toggle.css +12 -1
  209. package/src/components/toolbar/toolbar.js +2 -2
  210. package/src/components/tooltip/tooltip.css +4 -0
  211. package/src/components/tooltip/tooltip.js +44 -19
  212. package/src/components/tree/tree.css +26 -17
  213. package/src/components/tree/tree.js +31 -0
  214. package/src/components/value-list/value-list.css +3 -3
  215. package/src/components/view/record-view.css +1 -1
  216. package/src/components/view/record-view.js +178 -18
  217. package/src/components/view-settings/view-settings.css +285 -0
  218. package/src/components/view-settings/view-settings.js +1830 -0
  219. package/src/components/workspace-switcher/workspace-switcher.css +188 -0
  220. package/src/components/workspace-switcher/workspace-switcher.js +463 -0
  221. package/src/core/breakpoint.js +1 -1
  222. package/src/core/component.js +88 -17
  223. package/src/core/fontawesome.js +5 -1
  224. package/src/core/i18n.js +47 -4
  225. package/src/core/icons.js +204 -61
  226. package/src/core/keyboard.js +206 -11
  227. package/src/core/messages.js +427 -0
  228. package/src/core/motion.js +147 -0
  229. package/src/core/overlay-host.js +77 -3
  230. package/src/core/overlay-stack.js +487 -0
  231. package/src/core/position.js +22 -3
  232. package/src/index.js +10 -5
  233. package/src/internal/app-rail.js +135 -55
  234. package/src/internal/menu-items.js +61 -3
  235. package/src/zeyos/connect.js +6 -2
  236. package/src/zeyos/form.js +69 -27
  237. package/src/zeyos/index.js +4 -1
  238. package/src/zeyos/launcher.js +8 -3
  239. package/src/zeyos/module-bar.js +181 -0
  240. package/src/zeyos/query.js +197 -13
  241. package/src/zeyos/schema.js +144 -27
  242. package/src/zeyos/table.js +287 -84
  243. package/src/zeyos/view.js +49 -93
  244. package/styles/base.css +72 -1
  245. package/styles/icons.css +11 -1
  246. package/styles/print.css +1 -1
  247. package/styles/tokens/dark.css +117 -37
  248. package/styles/tokens/global.css +121 -27
  249. package/styles/tokens/semantic.css +140 -43
  250. package/styles/tokens/themes.css +7 -6
  251. package/styles/zx.css +3 -1
  252. package/styles/tokens/density.css +0 -9
@@ -74,6 +74,11 @@ const FLOAT_STEP = 0.01;
74
74
  * applies every committed edit.
75
75
  * @property {FilterPanelSerialize} [serialize='seconds'] How a date leaves the component. A range's
76
76
  * lower bound takes the start of its day and its upper bound the end, so a one-day range matches.
77
+ * @property {string|null} [timeZone=null] IANA zone the calendar days are counted in, such as the
78
+ * ZeyOS instance's `Europe/Vienna`. A day serializes as the first (or last) instant of that day
79
+ * in this zone, and an incoming instant shows as the day it falls on there — the same rule
80
+ * `zeyosForm()` and `zeyosTable()` apply to date-only fields. Null uses the runtime's own zone.
81
+ * An unknown zone throws a `RangeError`.
77
82
  * @property {string|null} [emptyText=null] Text shown when `fields` is empty; null resolves
78
83
  * `filterPanel.empty`.
79
84
  * @property {Record<string, string>|Record<string, Record<string, string>>} [msg] Localized messages.
@@ -111,6 +116,7 @@ export class FilterPanel extends Component {
111
116
  value: {},
112
117
  mode: 'draft',
113
118
  serialize: 'seconds',
119
+ timeZone: null,
114
120
  emptyText: null
115
121
  };
116
122
 
@@ -130,9 +136,10 @@ export class FilterPanel extends Component {
130
136
  const root = /** @type {HTMLElement} */ (this.el ?? h('div'));
131
137
  this.el = root;
132
138
 
139
+ this._timeZone = normalizePanelTimeZone(this.options.timeZone);
133
140
  this._fields = orderFields(this.options.fields, this.options.order);
134
- this._draft = toDraft(this.options.value, this.options.fields);
135
- this._applied = serializeFilterValue(this._draft, this.options.fields, this.options.serialize);
141
+ this._draft = toDraft(this.options.value, this.options.fields, this._timeZone);
142
+ this._applied = serializeFilterValue(this._draft, this.options.fields, this.options.serialize, this._timeZone);
136
143
  this._appliedDraft = draftKey(this._draft);
137
144
 
138
145
  const children = [];
@@ -187,7 +194,7 @@ export class FilterPanel extends Component {
187
194
  * @fires FilterPanel#change
188
195
  */
189
196
  setValue(value) {
190
- this._draft = toDraft(value, this.options.fields);
197
+ this._draft = toDraft(value, this.options.fields, this._timeZone);
191
198
  this._commit(this._draftValue());
192
199
  this._writeControls();
193
200
  this._syncActions();
@@ -219,7 +226,7 @@ export class FilterPanel extends Component {
219
226
  * @fires FilterPanel#apply
220
227
  */
221
228
  reset() {
222
- return this._replaceDraft(toDraft(this.options.value, this.options.fields));
229
+ return this._replaceDraft(toDraft(this.options.value, this.options.fields, this._timeZone));
223
230
  }
224
231
 
225
232
  /**
@@ -233,7 +240,7 @@ export class FilterPanel extends Component {
233
240
  * @fires FilterPanel#apply
234
241
  */
235
242
  clear() {
236
- return this._replaceDraft(toDraft({}, this.options.fields), { commit: true });
243
+ return this._replaceDraft(toDraft({}, this.options.fields, this._timeZone), { commit: true });
237
244
  }
238
245
 
239
246
  /** Destroys the composed controls and puts an enhanced target back as found. @returns {void} */
@@ -584,7 +591,7 @@ export class FilterPanel extends Component {
584
591
 
585
592
  /** @returns {Record<string, unknown>} The draft, serialized and pruned. */
586
593
  _draftValue() {
587
- return serializeFilterValue(this._draft, this.options.fields, this.options.serialize);
594
+ return serializeFilterValue(this._draft, this.options.fields, this.options.serialize, this._timeZone);
588
595
  }
589
596
 
590
597
  /** Pushes the draft back into the controls without re-entering the edit path. @returns {void} */
@@ -703,42 +710,48 @@ export function pruneValue(value) {
703
710
 
704
711
  /**
705
712
  * Converts anything a calendar day can arrive as — a `YYYY-MM-DD` string, an ISO timestamp, a
706
- * `Date`, Unix seconds or milliseconds — into the local calendar day a native date input holds.
713
+ * `Date`, Unix seconds or milliseconds — into the calendar day a native date input holds.
707
714
  * Numbers below 1e11 are read as seconds, which is the boundary between the two epochs in use.
715
+ * An instant — Unix seconds or milliseconds, an ISO timestamp — names the day it falls on in
716
+ * `timeZone`. A `Date` is read as a picked day and keeps its local calendar day, matching
717
+ * `calendarDayToUnixSeconds()` in the ZeyOS binding.
708
718
  * @param {unknown} value Candidate day.
719
+ * @param {string|null} [timeZone=null] IANA zone instants are read in; null for the runtime's own.
709
720
  * @returns {string} `YYYY-MM-DD`, or an empty string when there is no day in it.
710
721
  */
711
- export function toDateInput(value) {
722
+ export function toDateInput(value, timeZone = null) {
712
723
  if (value === null || value === undefined || value === '') return '';
713
- if (value instanceof Date) return Number.isNaN(value.getTime()) ? '' : localDay(value);
724
+ // A Date is a picked day — what Datebox and DatePicker hold — so its local day counts, as in
725
+ // `calendarDayToUnixSeconds()`; numbers and timestamps are instants, read in the zone.
726
+ if (value instanceof Date) return Number.isNaN(value.getTime()) ? '' : zoneDay(value.getTime(), null);
714
727
  if (typeof value === 'number') {
715
728
  if (!Number.isFinite(value)) return '';
716
- return localDay(new Date(Math.abs(value) < SECONDS_CUTOFF ? value * 1000 : value));
729
+ return zoneDay(Math.abs(value) < SECONDS_CUTOFF ? value * 1000 : value, timeZone);
717
730
  }
718
731
  const text = String(value).trim();
719
732
  if (/^\d{4}-\d{2}-\d{2}$/.test(text)) return text;
720
- if (/^-?\d+(\.\d+)?$/.test(text)) return toDateInput(Number(text));
733
+ if (/^-?\d+(\.\d+)?$/.test(text)) return toDateInput(Number(text), timeZone);
721
734
  const parsed = new Date(text);
722
- return Number.isNaN(parsed.getTime()) ? '' : localDay(parsed);
735
+ return Number.isNaN(parsed.getTime()) ? '' : zoneDay(parsed.getTime(), timeZone);
723
736
  }
724
737
 
725
738
  /**
726
739
  * Serializes one calendar day at one edge of a range. The `from` edge takes the first instant of
727
- * its local day and the `to` edge the last, so a range whose ends name the same day still matches
728
- * everything recorded on it.
740
+ * its day and the `to` edge the last, so a range whose ends name the same day still matches
741
+ * everything recorded on it. The day is counted in `timeZone`: with `Europe/Vienna`, 2026-03-10
742
+ * starts at 23:00 UTC on the 9th whatever zone the browser is in.
729
743
  * @param {unknown} day Calendar day in any form `toDateInput()` accepts.
730
744
  * @param {FilterPanelEdge} edge Which end of the range the day is.
731
745
  * @param {FilterPanelSerialize} [serialize='seconds'] Output format, or a function receiving the
732
746
  * edge instant as a `Date`.
747
+ * @param {string|null} [timeZone=null] IANA zone the day is counted in; null for the runtime's own.
733
748
  * @returns {unknown} The serialized day, or null when there is no day.
734
749
  */
735
- export function serializeDate(day, edge, serialize = 'seconds') {
736
- const text = toDateInput(day);
750
+ export function serializeDate(day, edge, serialize = 'seconds', timeZone = null) {
751
+ const text = toDateInput(day, timeZone);
737
752
  if (!text) return null;
738
753
  const [year, month, date] = text.split('-').map(Number);
739
- const instant = edge === 'to'
740
- ? new Date(year, month - 1, date, 23, 59, 59, 999)
741
- : new Date(year, month - 1, date, 0, 0, 0, 0);
754
+ const instant = new Date(dayEdge(year, month - 1, date, edge, timeZone));
742
755
  if (typeof serialize === 'function') return serialize(instant, edge);
743
756
  if (serialize === 'ms') return instant.getTime();
744
757
  if (serialize === 'iso') return instant.toISOString();
@@ -775,9 +788,10 @@ export function normalizeOptions(options) {
775
788
  * Running it on a draft returns the same draft, so it is safe on either shape.
776
789
  * @param {Record<string, unknown>|null|undefined} value Incoming values keyed by field.
777
790
  * @param {Record<string, FilterPanelField>} fields Field metadata.
791
+ * @param {string|null} [timeZone=null] IANA zone incoming instants are read in.
778
792
  * @returns {Record<string, unknown>}
779
793
  */
780
- export function toDraft(value, fields) {
794
+ export function toDraft(value, fields, timeZone = null) {
781
795
  const source = isPlainObject(value) ? value : {};
782
796
  /** @type {Record<string, unknown>} */
783
797
  const draft = {};
@@ -792,8 +806,8 @@ export function toDraft(value, fields) {
792
806
  draft[key] = selectionOf(raw, normalizeOptions(field.options));
793
807
  } else if (spec.kind === 'date') {
794
808
  draft[key] = spec.range
795
- ? { from: toDateInput(readEdge(raw, 'from')), to: toDateInput(readEdge(raw, 'to')) }
796
- : toDateInput(raw);
809
+ ? { from: toDateInput(readEdge(raw, 'from'), timeZone), to: toDateInput(readEdge(raw, 'to'), timeZone) }
810
+ : toDateInput(raw, timeZone);
797
811
  } else {
798
812
  draft[key] = spec.range
799
813
  ? { from: toNumberValue(readEdge(raw, 'from'), spec), to: toNumberValue(readEdge(raw, 'to'), spec) }
@@ -809,10 +823,11 @@ export function toDraft(value, fields) {
809
823
  * @param {Record<string, unknown>|null|undefined} value Values keyed by field, raw or drafted.
810
824
  * @param {Record<string, FilterPanelField>} fields Field metadata.
811
825
  * @param {FilterPanelSerialize} [serialize='seconds'] Date output format.
826
+ * @param {string|null} [timeZone=null] IANA zone calendar days are counted in.
812
827
  * @returns {Record<string, unknown>}
813
828
  */
814
- export function serializeFilterValue(value, fields, serialize = 'seconds') {
815
- const draft = toDraft(value, fields);
829
+ export function serializeFilterValue(value, fields, serialize = 'seconds', timeZone = null) {
830
+ const draft = toDraft(value, fields, timeZone);
816
831
  /** @type {Record<string, unknown>} */
817
832
  const result = {};
818
833
  for (const [key, declared] of Object.entries(fields ?? {})) {
@@ -832,14 +847,14 @@ export function serializeFilterValue(value, fields, serialize = 'seconds') {
832
847
  if (spec.kind === 'date') {
833
848
  if (!spec.range) {
834
849
  // A single date names a day, so it serializes as that day's first instant.
835
- if (raw) result[key] = serializeDate(raw, 'from', serialize);
850
+ if (raw) result[key] = serializeDate(raw, 'from', serialize, timeZone);
836
851
  continue;
837
852
  }
838
853
  const days = /** @type {{from: string, to: string}} */ (raw);
839
854
  /** @type {Record<string, unknown>} */
840
855
  const range = {};
841
- if (days.from) range.from = serializeDate(days.from, 'from', serialize);
842
- if (days.to) range.to = serializeDate(days.to, 'to', serialize);
856
+ if (days.from) range.from = serializeDate(days.from, 'from', serialize, timeZone);
857
+ if (days.to) range.to = serializeDate(days.to, 'to', serialize, timeZone);
843
858
  if (Object.keys(range).length > 0) result[key] = range;
844
859
  continue;
845
860
  }
@@ -917,11 +932,113 @@ function toNumberValue(raw, spec) {
917
932
  return spec.integer ? Math.trunc(parsed) : parsed;
918
933
  }
919
934
 
920
- /** @param {Date} date Instant. @returns {string} Local `YYYY-MM-DD`. */
921
- function localDay(date) {
922
- const month = String(date.getMonth() + 1).padStart(2, '0');
923
- const day = String(date.getDate()).padStart(2, '0');
924
- return `${date.getFullYear()}-${month}-${day}`;
935
+ /**
936
+ * Validates the `timeZone` option.
937
+ * @param {unknown} timeZone IANA zone name; nullish or empty means the runtime's own zone.
938
+ * @returns {string|null} Canonical zone name, or null for the runtime's own zone.
939
+ * @throws {RangeError} When the runtime does not know the zone.
940
+ */
941
+ export function normalizePanelTimeZone(timeZone) {
942
+ if (timeZone === null || timeZone === undefined || timeZone === '') return null;
943
+ try {
944
+ return new Intl.DateTimeFormat('en-US', { timeZone: String(timeZone) }).resolvedOptions().timeZone;
945
+ } catch {
946
+ throw new RangeError(`Unknown time zone "${String(timeZone)}"`);
947
+ }
948
+ }
949
+
950
+ /**
951
+ * The calendar day an instant falls on, as `YYYY-MM-DD`, in a zone or the runtime's own.
952
+ * @param {number} milliseconds Epoch milliseconds.
953
+ * @param {string|null} timeZone IANA zone, or null.
954
+ * @returns {string}
955
+ */
956
+ function zoneDay(milliseconds, timeZone) {
957
+ if (!timeZone) {
958
+ const date = new Date(milliseconds);
959
+ return isoDay(date.getFullYear(), date.getMonth(), date.getDate());
960
+ }
961
+ const wall = wallClock(milliseconds, timeZone);
962
+ return isoDay(wall.year, wall.month, wall.day);
963
+ }
964
+
965
+ /**
966
+ * Epoch milliseconds of the first (`from`) or last (`to`) instant of a calendar day in a zone.
967
+ * The last instant is one millisecond before the next day starts, which keeps a day that a
968
+ * daylight-saving change shortens or lengthens whole.
969
+ * @param {number} year Full year.
970
+ * @param {number} month Zero-based month.
971
+ * @param {number} day Day of the month.
972
+ * @param {FilterPanelEdge} edge Edge of the day.
973
+ * @param {string|null} timeZone IANA zone, or null for the runtime's own.
974
+ * @returns {number}
975
+ */
976
+ function dayEdge(year, month, day, edge, timeZone) {
977
+ if (!timeZone) {
978
+ return (edge === 'to'
979
+ ? new Date(year, month, day, 23, 59, 59, 999)
980
+ : new Date(year, month, day, 0, 0, 0, 0)).getTime();
981
+ }
982
+ if (edge !== 'to') return zoneMidnight(year, month, day, timeZone);
983
+ const next = new Date(Date.UTC(year, month, day + 1));
984
+ return zoneMidnight(next.getUTCFullYear(), next.getUTCMonth(), next.getUTCDate(), timeZone) - 1;
985
+ }
986
+
987
+ /**
988
+ * The first instant of a calendar day in a zone: its midnight, or the first minute after a
989
+ * daylight-saving gap that swallows midnight. Two offset corrections settle it on either side of
990
+ * a change. This is the rule `zeyos/query.js` `calendarZone().midnightOf()` follows, so a day the
991
+ * panel serializes is the day a ZeyOS form stores.
992
+ * @param {number} year Full year.
993
+ * @param {number} month Zero-based month.
994
+ * @param {number} day Day of the month.
995
+ * @param {string} timeZone IANA zone.
996
+ * @returns {number} Epoch milliseconds.
997
+ */
998
+ function zoneMidnight(year, month, day, timeZone) {
999
+ const target = Date.UTC(year, month, day);
1000
+ const offsetAt = (milliseconds) => {
1001
+ const wall = wallClock(milliseconds, timeZone);
1002
+ return Date.UTC(wall.year, wall.month, wall.day) + wall.seconds * 1000
1003
+ - Math.floor(milliseconds / 1000) * 1000;
1004
+ };
1005
+ const first = target - offsetAt(target);
1006
+ const second = target - offsetAt(first);
1007
+ let best = null;
1008
+ for (const candidate of [second, first]) {
1009
+ const wall = wallClock(candidate, timeZone);
1010
+ if (wall.year !== year || wall.month !== month || wall.day !== day) continue;
1011
+ if (!best || wall.seconds < best.seconds) best = { candidate, seconds: wall.seconds };
1012
+ }
1013
+ return best ? best.candidate : second;
1014
+ }
1015
+
1016
+ /**
1017
+ * The wall clock of an instant in a zone.
1018
+ * @param {number} milliseconds Epoch milliseconds.
1019
+ * @param {string} timeZone IANA zone.
1020
+ * @returns {{year: number, month: number, day: number, seconds: number}} Zero-based month and
1021
+ * seconds since the zone's midnight.
1022
+ */
1023
+ function wallClock(milliseconds, timeZone) {
1024
+ const formatter = new Intl.DateTimeFormat('en-US', {
1025
+ timeZone, hourCycle: 'h23', year: 'numeric', month: 'numeric', day: 'numeric',
1026
+ hour: 'numeric', minute: 'numeric', second: 'numeric'
1027
+ });
1028
+ /** @type {Record<string, number>} */
1029
+ const parts = {};
1030
+ for (const part of formatter.formatToParts(new Date(milliseconds))) parts[part.type] = Number(part.value);
1031
+ return {
1032
+ year: parts.year,
1033
+ month: parts.month - 1,
1034
+ day: parts.day,
1035
+ seconds: (parts.hour % 24) * 3600 + parts.minute * 60 + parts.second
1036
+ };
1037
+ }
1038
+
1039
+ /** @param {number} year @param {number} month Zero-based. @param {number} day @returns {string} `YYYY-MM-DD`. */
1040
+ function isoDay(year, month, day) {
1041
+ return `${String(year).padStart(4, '0')}-${String(month + 1).padStart(2, '0')}-${String(day).padStart(2, '0')}`;
925
1042
  }
926
1043
 
927
1044
  /**
@@ -17,7 +17,7 @@
17
17
  flex: none;
18
18
  min-inline-size: 0;
19
19
  flex-direction: column;
20
- border-inline-end: 1px solid var(--zx-color-border);
20
+ border-inline-end: 1px solid var(--zx-color-border-subtle);
21
21
  }
22
22
 
23
23
  .zx-finder__list {
@@ -32,9 +32,12 @@
32
32
  display: flex;
33
33
  align-items: center;
34
34
  gap: var(--zx-space-2);
35
- min-block-size: calc(var(--zx-control-height) - 6px);
36
- padding-inline: var(--zx-space-2);
37
- border-radius: var(--zx-radius-md);
35
+ /* A browsing row: a space-1 margin round one line of text, and the smallest radius, so a
36
+ column of them reads as a list rather than a stack of buttons. */
37
+ min-block-size: calc(var(--zx-control-height) - var(--zx-space-1));
38
+ padding: var(--zx-space-1) var(--zx-space-2);
39
+ border-radius: var(--zx-radius-sm);
40
+ line-height: 1.25;
38
41
  cursor: pointer;
39
42
  }
40
43
 
@@ -42,30 +45,29 @@
42
45
  background: var(--zx-color-bg-hover);
43
46
  }
44
47
 
48
+ /* The selected row of each column is the path you are on, not a pick: the neutral current fill,
49
+ the full text colour and a heavier label. */
45
50
  .zx-finder__row[aria-selected="true"] {
46
- background: var(--zx-color-bg-selected);
47
- color: var(--zx-color-accent);
48
- font-weight: 550;
51
+ background: var(--zx-color-bg-current);
52
+ color: var(--zx-color-text);
53
+ font-weight: var(--zx-weight-semibold);
49
54
  }
50
55
 
51
56
  .zx-finder__row:focus-visible {
52
- outline: 0;
57
+ outline-color: transparent;
53
58
  box-shadow: var(--zx-focus-ring);
54
59
  }
55
60
 
61
+ /* Icons are quieter than labels, folders included; the current row's icon takes the text colour. */
56
62
  .zx-finder__icon {
57
63
  display: inline-grid;
58
64
  place-items: center;
59
65
  flex: none;
60
- color: var(--zx-color-text-muted);
61
- }
62
-
63
- .zx-finder__row[data-branch] .zx-finder__icon {
64
- color: var(--zx-color-warning);
66
+ color: var(--zx-color-text-subtle);
65
67
  }
66
68
 
67
69
  .zx-finder__row[aria-selected="true"] .zx-finder__icon {
68
- color: inherit;
70
+ color: var(--zx-color-text);
69
71
  }
70
72
 
71
73
  .zx-finder__label {
@@ -29,11 +29,12 @@
29
29
  }
30
30
 
31
31
  .zx-form__actions .zx-btn[type="submit"] {
32
- border-color: var(--zx-color-accent);
33
- background: var(--zx-color-accent);
34
- color: var(--zx-color-on-accent);
32
+ border-color: var(--zx-color-accent-solid);
33
+ background: var(--zx-color-accent-solid);
34
+ color: var(--zx-color-on-accent-solid);
35
35
  }
36
36
 
37
37
  .zx-form__actions .zx-btn[type="submit"]:hover {
38
- background: var(--zx-color-accent-hover);
38
+ border-color: var(--zx-color-accent-solid-hover);
39
+ background: var(--zx-color-accent-solid-hover);
39
40
  }
@@ -1,6 +1,7 @@
1
1
  import { Component } from '../../core/component.js';
2
2
  import { h } from '../../core/dom.js';
3
3
  import { icon as createIcon } from '../../core/icons.js';
4
+ import { uid } from '../../core/util.js';
4
5
  import { Fieldset } from '../fieldset/fieldset.js';
5
6
  import { coerceFloat, coerceInteger } from '../field/field.js';
6
7
 
@@ -22,6 +23,9 @@ import { coerceFloat, coerceInteger } from '../field/field.js';
22
23
  * @property {Array<Fieldset|import('../fieldset/fieldset.js').FieldsetOptions>} [fieldsets=[]] Initial fieldsets.
23
24
  * @property {FormAction[]} [actions=[]] Form action descriptors.
24
25
  * @property {boolean} [novalidate=true] Native `novalidate` state.
26
+ * @property {string|null} [idPrefix=null] Prefix of the DOM ids generated for the fields of the
27
+ * fieldsets this form builds, as `<idPrefix>-<key>`. Null generates one per form instance, so a
28
+ * form mounted twice never repeats an id. A fieldset's own `idPrefix` wins.
25
29
  * @property {(event: CustomEvent<{values: Record<string, unknown>}>) => void} [onsubmit] Submit listener.
26
30
  * @property {(event: CustomEvent<{errors: Record<string, string>}>) => void} [oninvalid] Invalid listener.
27
31
  * @property {(event: CustomEvent<{id: string, value: unknown}>) => void} [onchange] Field change listener.
@@ -42,6 +46,7 @@ export class Form extends Component {
42
46
  fieldsets: [],
43
47
  actions: [],
44
48
  novalidate: true,
49
+ idPrefix: null,
45
50
  msg: {
46
51
  'form.required': 'This field is required.',
47
52
  'form.int': 'Enter a whole number.',
@@ -56,6 +61,7 @@ export class Form extends Component {
56
61
  this._ownedFieldsets = new Set();
57
62
  this._origins = new Map();
58
63
  this._ownedNodes = [];
64
+ this._idPrefix = this.options.idPrefix ? String(this.options.idPrefix) : uid('zx-form');
59
65
  this._novalidateAttribute = root.getAttribute('novalidate');
60
66
  root.toggleAttribute('novalidate', Boolean(this.options.novalidate));
61
67
 
@@ -81,13 +87,16 @@ export class Form extends Component {
81
87
  }
82
88
 
83
89
  /**
84
- * Adds an existing fieldset or constructs one from options.
90
+ * Adds an existing fieldset or constructs one from options. A constructed fieldset generates its
91
+ * field ids from the form's `idPrefix` unless its options carry their own.
85
92
  * @param {Fieldset|import('../fieldset/fieldset.js').FieldsetOptions} fieldset Fieldset source.
86
93
  * @returns {Fieldset}
87
94
  */
88
95
  addFieldset(fieldset) {
89
96
  const owned = !(fieldset instanceof Fieldset);
90
- const instance = owned ? new Fieldset(null, fieldset ?? {}) : fieldset;
97
+ const instance = owned
98
+ ? new Fieldset(null, { ...(fieldset ?? {}), idPrefix: fieldset?.idPrefix ?? this._idPrefix })
99
+ : fieldset;
91
100
  if (this._fieldsets.includes(instance)) return instance;
92
101
  if (owned) this._ownedFieldsets.add(instance);
93
102
  else this._origins.set(instance, { parent: instance.el.parentNode, next: instance.el.nextSibling });
@@ -2,5 +2,5 @@
2
2
  the lowercase `.zx-grid` layout helper remains a separate, unchanged primitive. */
3
3
  .zx-table[data-preset="billing-items"] tr.zx-grid-billing__summary > td {
4
4
  background: var(--zx-color-bg-muted);
5
- font-weight: 700;
5
+ font-weight: 650;
6
6
  }
@@ -11,7 +11,7 @@
11
11
  gap: var(--zx-space-2);
12
12
  min-block-size: var(--zx-control-height);
13
13
  padding: var(--zx-space-2) var(--zx-space-4);
14
- font-weight: 650;
14
+ font-weight: var(--zx-weight-semibold);
15
15
  cursor: pointer;
16
16
  user-select: none;
17
17
  }
@@ -1,7 +1,7 @@
1
1
  // @ts-check
2
2
  import { h, safeHref } from '../../core/dom.js';
3
3
  import { icon } from '../../core/icons.js';
4
- import { avatarInitials } from '../avatar/avatar.js';
4
+ import { AVATAR_TONES, avatarInitials, avatarTone } from '../avatar/avatar.js';
5
5
  import { badge } from '../badge/badge.js';
6
6
 
7
7
  /** @typedef {Record<string, any>} KanbanRecord */
@@ -28,6 +28,9 @@ import { badge } from '../badge/badge.js';
28
28
  * @property {string} name Person's name, used as the accessible label.
29
29
  * @property {string|null} src Safe image URL, or null for the initials fallback.
30
30
  * @property {string} initials Fallback initials.
31
+ * @property {import('../avatar/avatar.js').AvatarTone} tone Fallback tint: a descriptor's own
32
+ * `tone` when it names one of `AVATAR_TONES`, otherwise `avatarTone(name)` — the colour the same
33
+ * person has in every Avatar.
31
34
  */
32
35
 
33
36
  /** Semantic intents an indicator may carry, in the order they are documented. */
@@ -132,6 +135,8 @@ export function createKanbanProgress(progress, label, valueText) {
132
135
  /**
133
136
  * Normalizes one or more responsible people. Names, name lists, and descriptor objects all
134
137
  * resolve to the same shape, and an unsafe image URL degrades to initials rather than failing.
138
+ * A descriptor may name its `tone`; otherwise the tone is derived from the name exactly as Avatar
139
+ * derives it.
135
140
  * @param {unknown} value Raw field value.
136
141
  * @returns {KanbanAssignee[]} Normalized people, in the order supplied.
137
142
  */
@@ -148,7 +153,8 @@ export function resolveKanbanAssignees(value) {
148
153
  if (!name) return null;
149
154
  const src = safeHref(source.src ?? source.image ?? null);
150
155
  const initials = source.initials == null ? avatarInitials(name) : String(source.initials);
151
- return { name, src, initials };
156
+ const tone = AVATAR_TONES.includes(source.tone) ? source.tone : avatarTone(name);
157
+ return { name, src, initials, tone };
152
158
  }).filter(Boolean));
153
159
  }
154
160
 
@@ -185,8 +191,10 @@ export function createKanbanAvatars(people, max, groupLabel, overflowLabel) {
185
191
  const split = kanbanAvatarLimit(max, people.length);
186
192
  const shown = people.slice(0, split.shown);
187
193
  const hidden = split.hidden;
194
+ // Each face carries its tone, so the initials sit on the tint the person has in every Avatar.
188
195
  const faces = shown.map((person) => h('span', {
189
- class: 'zx-kanban-view__avatar', ariaHidden: 'true', title: person.name || undefined
196
+ class: 'zx-kanban-view__avatar', ariaHidden: 'true', title: person.name || undefined,
197
+ dataset: { tone: person.tone ?? avatarTone(person.name) }
190
198
  }, person.src
191
199
  ? h('img', { class: 'zx-kanban-view__avatar-image', src: person.src, alt: '', draggable: false })
192
200
  : h('span', { class: 'zx-kanban-view__avatar-initials' }, person.initials)));
@@ -120,8 +120,8 @@
120
120
  }
121
121
 
122
122
  .zx-kanban-view__move-handle[aria-pressed="true"] {
123
- background: var(--zx-color-accent-subtle);
124
- color: var(--zx-color-accent);
123
+ background: var(--zx-color-bg-current);
124
+ color: var(--zx-color-text);
125
125
  cursor: grabbing;
126
126
  }
127
127
 
@@ -134,7 +134,7 @@
134
134
  .zx-kanban-view__collapse:focus-visible,
135
135
  .zx-kanban-view__move-handle:focus-visible,
136
136
  .zx-kanban-view .zx-record-card:focus-visible {
137
- outline: none;
137
+ outline-color: transparent;
138
138
  box-shadow: var(--zx-focus-ring);
139
139
  }
140
140
 
@@ -188,7 +188,7 @@
188
188
  }
189
189
 
190
190
  .zx-kanban-view :where(.zx-record-card[data-variant="outlined"]) {
191
- box-shadow: var(--zx-shadow-1);
191
+ box-shadow: var(--zx-surface-shadow);
192
192
  }
193
193
 
194
194
  /* The shared card anatomy stays intact; Kanban constrains media to a compact thumbnail. */
@@ -348,7 +348,7 @@
348
348
  }
349
349
 
350
350
  .zx-kanban-view .zx-record-card__eyebrow-group[data-card-slot="eyebrow-start"] .zx-record-card__field {
351
- border-radius: var(--zx-radius-full);
351
+ border-radius: var(--zx-radius-sm);
352
352
  background: var(--zx-color-bg-muted);
353
353
  padding: calc(var(--zx-space-1) / 2) var(--zx-space-2);
354
354
  }
@@ -419,6 +419,12 @@
419
419
  .zx-kanban-view .zx-record-card[data-grabbed="true"] {
420
420
  outline: 2px solid var(--zx-color-accent);
421
421
  outline-offset: 2px;
422
+ /* A card picked up from the keyboard is the item being dragged, so it lifts like one. */
423
+ box-shadow: var(--zx-drag-shadow);
424
+ }
425
+
426
+ .zx-kanban-view .zx-record-card[data-grabbed="true"]:focus-visible {
427
+ box-shadow: var(--zx-focus-ring), var(--zx-drag-shadow);
422
428
  }
423
429
 
424
430
  .zx-kanban-view__empty {
@@ -470,10 +476,6 @@
470
476
  }
471
477
  }
472
478
 
473
- [data-zx-density="compact"] .zx-kanban-view {
474
- --zx-kanban-card-padding-inline: var(--zx-space-2);
475
- }
476
-
477
479
  @media (prefers-reduced-motion: no-preference) {
478
480
  .zx-kanban-view__collapse,
479
481
  .zx-kanban-view__move-handle,
@@ -561,7 +563,7 @@
561
563
  }
562
564
 
563
565
  .zx-kanban-view__add:focus-visible {
564
- outline: none;
566
+ outline-color: transparent;
565
567
  box-shadow: var(--zx-focus-ring);
566
568
  }
567
569
 
@@ -651,7 +653,8 @@
651
653
  border: 1px solid var(--zx-color-border);
652
654
  border-radius: var(--zx-radius-md);
653
655
  background: var(--zx-color-bg-raised);
654
- box-shadow: var(--zx-shadow-2);
656
+ /* The item in flight: lifted above every resting card by the shared drag shadow. */
657
+ box-shadow: var(--zx-drag-shadow);
655
658
  padding: var(--zx-space-1) var(--zx-space-2);
656
659
  color: var(--zx-color-text);
657
660
  font-size: var(--zx-text-xs);
@@ -704,7 +707,6 @@
704
707
  color: var(--zx-color-text-muted);
705
708
  font-size: var(--zx-text-xs);
706
709
  font-variant-numeric: tabular-nums;
707
- letter-spacing: .02em;
708
710
  text-overflow: ellipsis;
709
711
  white-space: nowrap;
710
712
  }
@@ -767,6 +769,11 @@
767
769
  min-inline-size: 0;
768
770
  }
769
771
 
772
+ /*
773
+ * A face is a small Avatar: the initials on the tint of the tone the component derives from the
774
+ * name with Avatar's own hash (`data-tone`), so a person has the same colour on the board as
775
+ * everywhere else. The surface-coloured edge is what separates overlapping faces.
776
+ */
770
777
  .zx-kanban-view__avatar {
771
778
  display: inline-grid;
772
779
  flex: none;
@@ -776,17 +783,27 @@
776
783
  overflow: hidden;
777
784
  border: 1px solid var(--zx-color-bg-surface);
778
785
  border-radius: var(--zx-radius-full);
779
- background: var(--zx-color-bg-muted);
780
- color: var(--zx-color-text-muted);
786
+ background: var(--zx-kanban-avatar-bg, var(--zx-color-bg-muted));
787
+ color: var(--zx-kanban-avatar-fg, var(--zx-color-text-muted));
781
788
  }
782
789
 
790
+ .zx-kanban-view__avatar[data-tone="blue"] { --zx-kanban-avatar-bg: var(--zx-color-tone-blue-bg); --zx-kanban-avatar-fg: var(--zx-color-tone-blue-fg); }
791
+ .zx-kanban-view__avatar[data-tone="teal"] { --zx-kanban-avatar-bg: var(--zx-color-tone-teal-bg); --zx-kanban-avatar-fg: var(--zx-color-tone-teal-fg); }
792
+ .zx-kanban-view__avatar[data-tone="green"] { --zx-kanban-avatar-bg: var(--zx-color-tone-green-bg); --zx-kanban-avatar-fg: var(--zx-color-tone-green-fg); }
793
+ .zx-kanban-view__avatar[data-tone="amber"] { --zx-kanban-avatar-bg: var(--zx-color-tone-amber-bg); --zx-kanban-avatar-fg: var(--zx-color-tone-amber-fg); }
794
+ .zx-kanban-view__avatar[data-tone="orange"] { --zx-kanban-avatar-bg: var(--zx-color-tone-orange-bg); --zx-kanban-avatar-fg: var(--zx-color-tone-orange-fg); }
795
+ .zx-kanban-view__avatar[data-tone="red"] { --zx-kanban-avatar-bg: var(--zx-color-tone-red-bg); --zx-kanban-avatar-fg: var(--zx-color-tone-red-fg); }
796
+ .zx-kanban-view__avatar[data-tone="pink"] { --zx-kanban-avatar-bg: var(--zx-color-tone-pink-bg); --zx-kanban-avatar-fg: var(--zx-color-tone-pink-fg); }
797
+ .zx-kanban-view__avatar[data-tone="violet"] { --zx-kanban-avatar-bg: var(--zx-color-tone-violet-bg); --zx-kanban-avatar-fg: var(--zx-color-tone-violet-fg); }
798
+ .zx-kanban-view__avatar[data-tone="slate"] { --zx-kanban-avatar-bg: var(--zx-color-tone-slate-bg); --zx-kanban-avatar-fg: var(--zx-color-tone-slate-fg); }
799
+
783
800
  .zx-kanban-view__avatar + .zx-kanban-view__avatar {
784
801
  margin-inline-start: calc(var(--zx-space-2) * -1);
785
802
  }
786
803
 
787
804
  .zx-kanban-view__avatar[data-overflow="true"] {
788
- background: var(--zx-color-accent-subtle);
789
- color: var(--zx-color-accent);
805
+ background: var(--zx-color-bg-current);
806
+ color: var(--zx-color-text-muted);
790
807
  }
791
808
 
792
809
  .zx-kanban-view__avatar-image {
@@ -795,13 +812,20 @@
795
812
  object-fit: cover;
796
813
  }
797
814
 
815
+ /* Avatar's initials at the small size: 0.4 of the face, never under 10px, semibold. */
798
816
  .zx-kanban-view__avatar-initials {
799
- font-size: var(--zx-text-xs);
800
- font-weight: 600;
817
+ font-size: max(10px, calc(1.5rem * .4));
818
+ font-weight: var(--zx-weight-semibold);
819
+ letter-spacing: 0;
801
820
  line-height: 1;
802
821
  text-transform: uppercase;
803
822
  }
804
823
 
824
+ /* The overflow chip is a count, not a person: it keeps the text size it can be read at. */
825
+ .zx-kanban-view__avatar[data-overflow="true"] .zx-kanban-view__avatar-initials {
826
+ font-size: var(--zx-text-xs);
827
+ }
828
+
805
829
  /* ── Move handle ──────────────────────────────────────────────────────────────────────────── */
806
830
 
807
831
  /* The card is the pointer drag source, so the handle is keyboard furniture: announced and in tab