@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
@@ -1,4 +1,50 @@
1
+ /*
2
+ * AppSidebar — the surface an application is judged by before it is read.
3
+ *
4
+ * Everything here hangs off one spacing scale declared on the root. Item height, icon gutter,
5
+ * row gap, group gap and indentation are derived from the tier-2 control and spacing tokens
6
+ * rather than picked per rule, so a theme that changes `--zx-space-unit` or
7
+ * `--zx-control-height` changes the rhythm of the whole nav instead of one row of it.
8
+ *
9
+ * Three deliberate choices worth stating:
10
+ *
11
+ * - **Labels wrap, they never truncate.** German runs 20-35% longer than English and a nav is
12
+ * where that shows first: "Belege & Rechnungen" is two lines in a 16.5rem sidebar. An ellipsis
13
+ * there hides the word that distinguishes one destination from another, so the row grows
14
+ * instead and the fixed icon column keeps the second line aligned under the first.
15
+ * - **The active state never rests on colour alone.** It is a neutral fill and a heavier label —
16
+ * where you are, not something you picked, so it takes the `--zx-color-bg-current` role rather
17
+ * than the accent. The weight survives greyscale and a reduced-transparency page, and forced
18
+ * colours swap the fill for the system's own selected pair. Nothing is docked to the edge.
19
+ * - **A child never starts further in than its parent's label.** A text-only child lines up with
20
+ * the words above it; a child with its own icon steps in only far enough for that icon to clear
21
+ * the guide line. A parent list without icons indents its children by a short text step.
22
+ */
23
+
1
24
  .zx-app-sidebar {
25
+ /* The scale. A row is exactly one control tall, the icon column is as wide as the 24px module
26
+ chips at the default unit, and every gap and inset is a step of the spacing scale. */
27
+ --zx-app-sidebar-gutter: var(--zx-space-2);
28
+ --zx-app-sidebar-icon: var(--zx-space-6);
29
+ --zx-app-sidebar-item-gap: var(--zx-space-2);
30
+ --zx-app-sidebar-item-height: var(--zx-control-height);
31
+ --zx-app-sidebar-pad-x: var(--zx-space-2);
32
+ --zx-app-sidebar-row-gap: 2px;
33
+ --zx-app-sidebar-group-gap: var(--zx-space-3);
34
+ /* The indent under a parent list that has no icon column to hand down. */
35
+ --zx-app-sidebar-indent: var(--zx-space-3);
36
+ /* The indent of a child list that brings icons of its own: enough for them to clear the guide,
37
+ not a whole icon column, so a deep branch never walks across the sidebar. */
38
+ --zx-app-sidebar-icon-indent: var(--zx-space-4);
39
+ --zx-app-sidebar-radius: var(--zx-radius-md);
40
+ /* Three steps of one overlay: hover, the current row, and press. The hover and current roles
41
+ mix the *text* colour into whatever sits behind, which moves the fill away from the page in
42
+ whichever direction the theme runs, so a sidebar on the grey chrome tone and one on a white
43
+ surface both read correctly. Hover stays below the current fill, so pointing at a row never
44
+ looks like being on it; press goes one step past it. */
45
+ --zx-app-sidebar-hover: var(--zx-color-bg-hover);
46
+ --zx-app-sidebar-current: var(--zx-color-bg-current);
47
+ --zx-app-sidebar-press: color-mix(in srgb, var(--zx-color-text) 12%, transparent);
2
48
  display: flex;
3
49
  min-block-size: 0;
4
50
  border: 1px solid var(--zx-color-glass-border);
@@ -29,57 +75,336 @@
29
75
  .zx-app-sidebar__expanded[hidden],
30
76
  .zx-app-sidebar__rail[hidden] { display: none; }
31
77
 
78
+ /* The header carries no rule. It is a brand slot above a nav, not a boundary between two kinds
79
+ of content, and a hairline there reads as leftover scaffolding. The footer keeps its rule:
80
+ an identity control below navigation is a different region and the line is doing work. */
32
81
  .zx-app-sidebar__header {
33
82
  display: grid;
34
83
  grid-template-columns: minmax(0, 1fr) auto;
35
84
  align-items: center;
36
85
  gap: var(--zx-space-2);
37
- min-block-size: calc(var(--zx-control-height) + var(--zx-space-3));
38
- border-block-end: 1px solid var(--zx-color-border);
39
- padding: var(--zx-space-2);
86
+ min-block-size: calc(var(--zx-control-height) + var(--zx-space-4));
87
+ padding: var(--zx-space-2) var(--zx-space-3);
40
88
  }
41
89
 
42
90
  .zx-app-sidebar__header-content { min-inline-size: 0; }
43
- .zx-app-sidebar__nav { min-block-size: 0; overflow: auto; padding: var(--zx-space-2); }
91
+
92
+ /* `collapsible: false` hides the minimise button; the icon-button display rule must not show it. */
93
+ .zx-app-sidebar .zx-app-sidebar__collapse[hidden] { display: none; }
94
+
95
+ /* The block padding is not decoration: the focus ring is a 3px outer glow, and a nav that padded
96
+ its scroll box by less would clip the ring off the first and last row. */
97
+ .zx-app-sidebar__nav {
98
+ min-block-size: 0;
99
+ overflow: auto;
100
+ padding: var(--zx-space-2) var(--zx-app-sidebar-gutter) var(--zx-space-3);
101
+ }
102
+
44
103
  .zx-app-sidebar__list,
45
- .zx-app-sidebar__sublist { display: grid; gap: var(--zx-space-1); margin: 0; padding: 0; list-style: none; }
46
- .zx-app-sidebar__sublist { margin-block-start: var(--zx-space-1); }
104
+ .zx-app-sidebar__sublist {
105
+ display: grid;
106
+ gap: var(--zx-app-sidebar-row-gap);
107
+ margin: 0;
108
+ padding: 0;
109
+ list-style: none;
110
+ }
111
+
112
+ /* A list whose items carry no icons should not pay for an empty gutter; one that has even a
113
+ single icon keeps the column on every row so the labels stay in one line down the list. Each
114
+ list answers for its own rows only — a branch of plain labels under iconned parents drops the
115
+ column without taking it from the parents. */
116
+ .zx-app-sidebar__list:not(:has(> .zx-app-sidebar__row > .zx-app-sidebar__item .zx-app-sidebar__item-icon)),
117
+ .zx-app-sidebar__sublist:not(:has(> .zx-app-sidebar__row > .zx-app-sidebar__item .zx-app-sidebar__item-icon)) {
118
+ --zx-app-sidebar-icon: 0px;
119
+ --zx-app-sidebar-item-gap: 0px;
120
+ }
121
+
122
+ /* Where a branch's children start, measured from the row padding, and where the guide that scopes
123
+ them runs. Both are resolved on the parent row — where the parent's own icon column is still
124
+ the one in force — and inherited from there, so a child list that drops its column cannot pull
125
+ the indent or the guide back with it. A row's step is its icon column, or the text indent when
126
+ it has none; level 3 adds its own step to the one level 2 already sits at. */
127
+ .zx-app-sidebar__row[data-level="1"] {
128
+ --zx-app-sidebar-step-1: max(var(--zx-app-sidebar-indent), var(--zx-app-sidebar-icon) + var(--zx-app-sidebar-item-gap));
129
+ --zx-app-sidebar-guide: calc(var(--zx-app-sidebar-pad-x) + max(var(--zx-app-sidebar-indent), var(--zx-app-sidebar-icon)) / 2);
130
+ }
131
+
132
+ .zx-app-sidebar__row[data-level="2"] {
133
+ --zx-app-sidebar-step-2: calc(var(--zx-app-sidebar-step-1) + max(var(--zx-app-sidebar-indent), var(--zx-app-sidebar-icon) + var(--zx-app-sidebar-item-gap)));
134
+ --zx-app-sidebar-guide: calc(var(--zx-app-sidebar-pad-x) + var(--zx-app-sidebar-step-1) + max(var(--zx-app-sidebar-indent), var(--zx-app-sidebar-icon)) / 2);
135
+ }
136
+
137
+ /* Children with icons of their own step in by the shorter icon indent instead: their icons sit
138
+ just past the guide rather than under the parent's label. Text-only children keep lining up
139
+ with the words above them. */
140
+ .zx-app-sidebar__row[data-level="1"]:has(> .zx-app-sidebar__sublist > .zx-app-sidebar__row > .zx-app-sidebar__item .zx-app-sidebar__item-icon) {
141
+ --zx-app-sidebar-step-1: max(var(--zx-app-sidebar-indent), var(--zx-app-sidebar-icon-indent));
142
+ }
143
+
144
+ .zx-app-sidebar__row[data-level="2"]:has(> .zx-app-sidebar__sublist > .zx-app-sidebar__row > .zx-app-sidebar__item .zx-app-sidebar__item-icon) {
145
+ --zx-app-sidebar-step-2: calc(var(--zx-app-sidebar-step-1) + max(var(--zx-app-sidebar-indent), var(--zx-app-sidebar-icon-indent)));
146
+ }
147
+
148
+ .zx-app-sidebar__sublist { position: relative; margin-block-start: var(--zx-app-sidebar-row-gap); }
47
149
  .zx-app-sidebar__sublist[hidden] { display: none; }
48
150
 
151
+ /* The one rule in the component that carries meaning: a guide dropped from the centre of the
152
+ parent's icon that scopes the open branch to the row that opened it. */
153
+ .zx-app-sidebar__sublist::before {
154
+ content: "";
155
+ position: absolute;
156
+ inset-block: 2px;
157
+ inset-inline-start: var(--zx-app-sidebar-guide);
158
+ inline-size: 1px;
159
+ border-radius: var(--zx-radius-full);
160
+ background: var(--zx-color-border);
161
+ }
162
+
163
+ /* Groups are separated by space, not by rules: a top-level branch opens a group, and the row
164
+ after that group closes it. The first row never gets the gap. */
165
+ .zx-app-sidebar__list > .zx-app-sidebar__row:has(> .zx-app-sidebar__sublist),
166
+ .zx-app-sidebar__list > .zx-app-sidebar__row:has(> .zx-app-sidebar__sublist) + .zx-app-sidebar__row {
167
+ margin-block-start: var(--zx-app-sidebar-group-gap);
168
+ }
169
+
170
+ .zx-app-sidebar__list > .zx-app-sidebar__row:first-child { margin-block-start: 0; }
171
+
49
172
  .zx-app-sidebar__item {
173
+ position: relative;
50
174
  display: grid;
51
- grid-template-columns: auto minmax(0, 1fr) auto auto;
175
+ /* The icon track is composed here and not in a variable on the root: a custom property
176
+ substitutes its `var()`s where it is declared, so a track precomputed on the component would
177
+ ignore the list-level override below and keep an empty 36px gutter on an icon-less sidebar. */
178
+ grid-template-columns: calc(var(--zx-app-sidebar-icon) + var(--zx-app-sidebar-item-gap)) minmax(0, 1fr) auto auto;
52
179
  align-items: center;
53
- gap: var(--zx-space-2);
180
+ column-gap: 0;
54
181
  inline-size: 100%;
55
- min-block-size: var(--zx-control-height);
56
- border-color: transparent;
57
- border-radius: var(--zx-radius-md);
182
+ min-block-size: var(--zx-app-sidebar-item-height);
183
+ border: 0;
184
+ border-radius: var(--zx-app-sidebar-radius);
58
185
  background: transparent;
59
- padding-inline: var(--zx-space-3);
60
- color: inherit;
186
+ /* The icon column fills the row exactly; a row without one keeps a space-1 margin round its
187
+ label. Either way a single-line row is one control tall, and a wrapped one grows. */
188
+ padding-block: min(var(--zx-space-1), (var(--zx-app-sidebar-item-height) - var(--zx-app-sidebar-icon)) / 2);
189
+ padding-inline: var(--zx-app-sidebar-pad-x);
190
+ color: var(--zx-color-text);
191
+ font-size: var(--zx-text-md);
192
+ font-weight: 500;
193
+ line-height: 1.35;
61
194
  text-align: start;
62
195
  text-decoration: none;
63
196
  }
64
197
 
65
- .zx-app-sidebar__row[data-level="2"] > .zx-app-sidebar__item { padding-inline-start: calc(var(--zx-space-3) + var(--zx-space-5)); }
66
- .zx-app-sidebar__row[data-level="3"] > .zx-app-sidebar__item { padding-inline-start: calc(var(--zx-space-3) + var(--zx-space-7)); }
67
- .zx-app-sidebar__item:hover,
68
- .zx-app-sidebar__item:focus-visible,
69
- .zx-app-sidebar__item[data-active-descendant="true"],
70
- .zx-app-sidebar__item[aria-current="page"] { background: var(--zx-color-bg-hover); color: var(--zx-color-accent); }
71
- .zx-app-sidebar__item[aria-disabled="true"] { color: var(--zx-color-text-muted); cursor: not-allowed; opacity: .65; }
72
- .zx-app-sidebar__item-icon,
73
- .zx-app-sidebar__item-disclosure { display: inline-flex; }
74
- .zx-app-sidebar__item-badge { color: var(--zx-color-text-muted); font-size: var(--zx-text-xs); }
198
+ /* A nav is a touch target before it is a dense list. The drawn row is one control tall, and a
199
+ coarse pointer puts the hit area back to 40px whatever the theme's control height says. */
200
+ @media (pointer: coarse) {
201
+ .zx-app-sidebar__item { min-block-size: 40px; }
202
+ }
203
+
204
+ .zx-app-sidebar__row[data-level="2"] > .zx-app-sidebar__item { padding-inline-start: calc(var(--zx-app-sidebar-pad-x) + var(--zx-app-sidebar-step-1)); }
205
+ .zx-app-sidebar__row[data-level="3"] > .zx-app-sidebar__item { padding-inline-start: calc(var(--zx-app-sidebar-pad-x) + var(--zx-app-sidebar-step-2)); }
206
+
207
+ .zx-app-sidebar__item-icon {
208
+ grid-column: 1;
209
+ display: flex;
210
+ align-items: center;
211
+ justify-content: center;
212
+ inline-size: var(--zx-app-sidebar-icon);
213
+ color: var(--zx-color-text-muted);
214
+ }
215
+
216
+ /* No nowrap and no ellipsis: the row grows instead. `hyphens` is what makes that bearable for a
217
+ German compound — where the document declares a language the browser breaks "Rechnungsausgang"
218
+ at a syllable, and `overflow-wrap` is only the last resort behind it for a word that fits
219
+ nowhere at all. */
220
+ .zx-app-sidebar__item-label {
221
+ grid-column: 2;
222
+ min-inline-size: 0;
223
+ hyphens: auto;
224
+ overflow-wrap: break-word;
225
+ }
226
+
227
+ .zx-app-sidebar__item-badge {
228
+ grid-column: 3;
229
+ margin-inline-start: var(--zx-space-2);
230
+ color: var(--zx-color-text-muted);
231
+ font-size: var(--zx-text-xs);
232
+ font-variant-numeric: tabular-nums;
233
+ line-height: 1;
234
+ }
235
+
236
+ .zx-app-sidebar__item-disclosure {
237
+ grid-column: 4;
238
+ display: inline-flex;
239
+ margin-inline-start: var(--zx-space-1);
240
+ color: var(--zx-color-text-muted);
241
+ }
242
+
243
+ .zx-app-sidebar__item:hover { background: var(--zx-app-sidebar-hover); }
244
+ .zx-app-sidebar__item:hover .zx-app-sidebar__item-icon,
245
+ .zx-app-sidebar__item:hover .zx-app-sidebar__item-disclosure { color: var(--zx-color-text); }
246
+ .zx-app-sidebar__item:active { background: var(--zx-app-sidebar-press); }
247
+
248
+ /* A branch that is closed over the active route carries the heavier label, so the way back to
249
+ the current page is visible without opening it; a branch that is open says so with its chevron
250
+ and shows the active row itself. */
251
+ .zx-app-sidebar__item[data-active-descendant="true"][aria-expanded="false"] {
252
+ font-weight: var(--zx-weight-semibold);
253
+ }
254
+
255
+ .zx-app-sidebar__item[data-active-descendant="true"][aria-expanded="false"] .zx-app-sidebar__item-icon {
256
+ color: var(--zx-color-text);
257
+ }
258
+
259
+ /* The current page: a neutral fill, the full text colour, a heavier label. Focus is a ring on top
260
+ of it, never a replacement for it, so a keyboard user arrowing past the active route can still
261
+ tell which row is current — the shared `--zx-focus-ring` reads on a neutral fill as it does on
262
+ none. */
263
+ .zx-app-sidebar__item[aria-current="page"] {
264
+ background: var(--zx-app-sidebar-current);
265
+ color: var(--zx-color-text);
266
+ font-weight: var(--zx-weight-semibold);
267
+ }
268
+
269
+ .zx-app-sidebar__item[aria-current="page"]:active {
270
+ background: var(--zx-app-sidebar-press);
271
+ }
272
+
273
+ .zx-app-sidebar__item[aria-current="page"] .zx-app-sidebar__item-icon,
274
+ .zx-app-sidebar__item[aria-current="page"] .zx-app-sidebar__item-disclosure {
275
+ color: var(--zx-color-text);
276
+ }
277
+
278
+ /* Unavailable, and still readable. A blanket `opacity` on the row took the label under 3:1, which
279
+ is the one thing a destination a user cannot reach still has to do: say where it would go. The
280
+ icon carries the dimming instead, and the label keeps the muted text role. */
281
+ .zx-app-sidebar__item[aria-disabled="true"] {
282
+ color: var(--zx-color-text-muted);
283
+ cursor: not-allowed;
284
+ }
285
+
286
+ .zx-app-sidebar__item[aria-disabled="true"] .zx-app-sidebar__item-icon { opacity: .55; }
287
+
288
+ .zx-app-sidebar__item[aria-disabled="true"]:hover,
289
+ .zx-app-sidebar__item[aria-disabled="true"]:active { background: transparent; }
75
290
 
76
291
  .zx-app-sidebar__footer {
77
292
  border-block-start: 1px solid var(--zx-color-border);
78
- padding: var(--zx-space-2);
293
+ padding: var(--zx-space-2) var(--zx-space-3);
79
294
  }
80
295
  .zx-app-sidebar__footer[hidden] { display: none; }
296
+
81
297
  .zx-app-sidebar__rail-header { display: grid; gap: var(--zx-space-1); }
82
298
 
299
+ /*
300
+ * The minimized rail. Only the presentation changes; the same vocabulary applies — the neutral
301
+ * current fill, the full text colour for a branch over the active route, a badge that survives
302
+ * the loss of the label.
303
+ */
304
+ .zx-app-sidebar__rail .zx-app-rail__item {
305
+ color: var(--zx-color-text-muted);
306
+ }
307
+
308
+ .zx-app-sidebar__rail .zx-app-rail__item:not([aria-current="page"]):hover,
309
+ .zx-app-sidebar__rail .zx-app-rail__item:not([aria-current="page"])[aria-expanded="true"] {
310
+ background: var(--zx-app-sidebar-hover);
311
+ color: var(--zx-color-text);
312
+ }
313
+
314
+ .zx-app-sidebar__rail .zx-app-rail__item:active {
315
+ background: var(--zx-app-sidebar-press);
316
+ }
317
+
318
+ .zx-app-sidebar__rail .zx-app-rail__item[aria-current="page"] {
319
+ background: var(--zx-app-sidebar-current);
320
+ color: var(--zx-color-text);
321
+ }
322
+
323
+ /* A rail item whose flyout holds the active route says so the way a closed branch does in the
324
+ tree: in the full text colour, without a fill of its own. */
325
+ .zx-app-sidebar__rail .zx-app-rail__item[data-active-descendant="true"]:not([aria-current="page"]) {
326
+ color: var(--zx-color-text);
327
+ }
328
+
329
+ /*
330
+ * The count a minimized rail would otherwise lose. `data-zx-badge` is set by the component from
331
+ * the item's own badge: a number that fits stays a number, and one that does not becomes a dot,
332
+ * because a three-digit pill on a 30px target stops being a badge and starts being a label. The
333
+ * figure itself moves into the control's accessible name either way, so nothing is dropped —
334
+ * only redrawn.
335
+ */
336
+ .zx-app-sidebar__rail .zx-app-rail__item-badge {
337
+ inset-block-start: 0;
338
+ inset-inline-end: 0;
339
+ min-inline-size: var(--zx-space-4);
340
+ block-size: var(--zx-space-4);
341
+ border-radius: var(--zx-radius-full);
342
+ background: var(--zx-color-accent);
343
+ box-shadow: 0 0 0 2px var(--zx-color-bg-surface);
344
+ padding-inline: var(--zx-space-1);
345
+ color: var(--zx-color-on-accent);
346
+ font-size: var(--zx-text-xs);
347
+ font-variant-numeric: tabular-nums;
348
+ font-weight: var(--zx-weight-semibold);
349
+ line-height: var(--zx-space-4);
350
+ text-align: center;
351
+ }
352
+
353
+ .zx-app-sidebar__rail .zx-app-rail__item-badge[data-zx-badge="dot"] {
354
+ min-inline-size: 0;
355
+ inline-size: var(--zx-space-2);
356
+ block-size: var(--zx-space-2);
357
+ padding: 0;
358
+ font-size: 0;
359
+ line-height: 0;
360
+ }
361
+
362
+ /*
363
+ * Motion. Everything here is decoration on a state change that is already expressed statically,
364
+ * so all of it is opt-in.
365
+ *
366
+ * The expanded tree is re-rendered wholesale on every branch toggle, which is why the disclosure
367
+ * animation is driven by `data-branch-motion` — an attribute the component sets on exactly the
368
+ * sublist it just opened, so the branches that were already open do not replay it.
369
+ */
370
+ @media (prefers-reduced-motion: no-preference) {
371
+ .zx-app-sidebar__item,
372
+ .zx-app-sidebar__rail .zx-app-rail__item {
373
+ transition: background-color var(--zx-dur-fast) var(--zx-ease),
374
+ color var(--zx-dur-fast) var(--zx-ease),
375
+ box-shadow var(--zx-dur-fast) var(--zx-ease);
376
+ }
377
+
378
+ .zx-app-sidebar__item:active,
379
+ .zx-app-sidebar__rail .zx-app-rail__item:active { transform: scale(.985); }
380
+
381
+ .zx-app-sidebar__expanded,
382
+ .zx-app-sidebar__rail { animation: zx-app-sidebar-mode var(--zx-dur-base) var(--zx-ease); }
383
+
384
+ .zx-app-sidebar__sublist[data-branch-motion="expand"] { animation: zx-app-sidebar-branch var(--zx-dur-base) var(--zx-ease); }
385
+ }
386
+
387
+ @keyframes zx-app-sidebar-mode {
388
+ from { opacity: 0; }
389
+ to { opacity: 1; }
390
+ }
391
+
392
+ @keyframes zx-app-sidebar-branch {
393
+ from { opacity: 0; transform: translateY(-4px); }
394
+ to { opacity: 1; transform: translateY(0); }
395
+ }
396
+
397
+ /* Forced colours drop author backgrounds, so the current fill would disappear. The system's own
398
+ selected-item pair carries the state there instead. */
399
+ @media (forced-colors: active) {
400
+ .zx-app-sidebar__item[aria-current="page"],
401
+ .zx-app-sidebar__rail .zx-app-rail__item[aria-current="page"],
402
+ .zx-app-sidebar__rail .zx-app-rail__item-badge {
403
+ background: Highlight;
404
+ color: HighlightText;
405
+ }
406
+ }
407
+
83
408
  @media (prefers-reduced-transparency: reduce) {
84
409
  .zx-app-sidebar {
85
410
  border-color: var(--zx-color-border);
@@ -1,5 +1,6 @@
1
1
  import { Component } from '../../core/component.js';
2
2
  import { h } from '../../core/dom.js';
3
+ import { printf } from '../../core/i18n.js';
3
4
  import { icon } from '../../core/icons.js';
4
5
  import { uid } from '../../core/util.js';
5
6
  import {
@@ -77,7 +78,9 @@ export class AppSidebar extends Component {
77
78
  /** @returns {HTMLElement} */
78
79
  render() {
79
80
  this._createdRoot = this.el === null;
80
- const root = /** @type {HTMLElement} */ (this.el ?? h('aside'));
81
+ // A plain box: the <nav> inside is the landmark. An <aside> here would add an unnamed
82
+ // complementary landmark around every navigation column.
83
+ const root = /** @type {HTMLElement} */ (this.el ?? h('div'));
81
84
  this.el = root;
82
85
  this._original = this._createdRoot ? null : {
83
86
  attributes: Array.from(root.attributes, (attribute) => [attribute.name, attribute.value]),
@@ -99,17 +102,20 @@ export class AppSidebar extends Component {
99
102
  setItems(items) {
100
103
  this._items = normalizeAppItems(items);
101
104
  this._reconcileExpanded();
102
- if (this._rail) this._rail.setItems(this._items);
103
- else this._renderTree();
105
+ if (this._rail) {
106
+ this._rail.setItems(this._items);
107
+ this._decorateRail();
108
+ } else this._renderTree();
104
109
  return this;
105
110
  }
106
111
 
107
- /** Returns a shallow copy of normalized items. @returns {AppNavItem[]} */
112
+ /**
113
+ * Returns a copy of the normalized items. A leaf has no `children` property, as it was written,
114
+ * rather than the empty list normalization gives it.
115
+ * @returns {AppNavItem[]}
116
+ */
108
117
  getItems() {
109
- return this._items.map((item) => ({
110
- ...item,
111
- children: item.children?.map((child) => ({ ...child }))
112
- }));
118
+ return copyItems(this._items);
113
119
  }
114
120
 
115
121
  /** Sets the active id without routing. @param {string|number|null} id @returns {this} */
@@ -151,7 +157,10 @@ export class AppSidebar extends Component {
151
157
  const expanded = !this._expanded.has(key);
152
158
  if (expanded) this._expanded.add(key);
153
159
  else this._expanded.delete(key);
154
- if (!this._rail) this._renderTree();
160
+ if (!this._rail) {
161
+ this._renderTree();
162
+ if (expanded) this._markBranchMotion(key);
163
+ }
155
164
  if (!silent) this.emit('branchchange', {
156
165
  item,
157
166
  id: item.id,
@@ -298,7 +307,7 @@ export class AppSidebar extends Component {
298
307
  const collapseButton = h('button', {
299
308
  class: 'zx-app-sidebar__collapse zx-icon-btn',
300
309
  type: 'button',
301
- ariaLabel: 'Minimize application sidebar'
310
+ ariaLabel: this._message('appSidebar.minimize', 'Minimize application sidebar')
302
311
  }, icon(this._side === 'right' ? 'chevron-right' : 'chevron-left'));
303
312
  collapseButton.hidden = !this.options.collapsible;
304
313
  const footer = h('footer', { class: 'zx-app-sidebar__footer' });
@@ -330,7 +339,7 @@ export class AppSidebar extends Component {
330
339
  const expandButton = h('button', {
331
340
  class: 'zx-app-sidebar__rail-toggle zx-icon-btn',
332
341
  type: 'button',
333
- ariaLabel: 'Expand application sidebar'
342
+ ariaLabel: this._message('appSidebar.expand', 'Expand application sidebar')
334
343
  }, icon(this._side === 'right' ? 'chevron-left' : 'chevron-right'));
335
344
  railHeader.append(expandButton);
336
345
  this._railExpandButton = expandButton;
@@ -356,6 +365,31 @@ export class AppSidebar extends Component {
356
365
  });
357
366
  this._listenMode(railHost, 'zx-select', (event) => event.stopPropagation());
358
367
  this._listenMode(railHost, 'zx-flyoutchange', (event) => event.stopPropagation());
368
+ this._decorateRail();
369
+ }
370
+
371
+ /**
372
+ * Keeps a minimized rail's counts readable and audible.
373
+ *
374
+ * A rail destination is icon-only, so its accessible name is the label alone and the badge the
375
+ * expanded tree showed as text is announced by nothing. Here the figure is folded back into the
376
+ * name, and `data-zx-badge` tells the stylesheet whether the count still fits the target or has
377
+ * to be drawn as a dot. Presentation only: no item, option, event, or listener is touched.
378
+ * @returns {void}
379
+ */
380
+ _decorateRail() {
381
+ const rail = this.refs.rail;
382
+ if (!rail) return;
383
+ for (const control of rail.querySelectorAll('.zx-app-rail__item[data-app-nav-value-id]')) {
384
+ const badge = control.querySelector(':scope > .zx-app-rail__item-badge');
385
+ if (!badge) continue;
386
+ const item = findItem(this._items, control.getAttribute('data-app-nav-value-id'));
387
+ if (!item || item.badge == null) continue;
388
+ const count = String(item.badge);
389
+ badge.dataset.zxBadge = count.length > 2 ? 'dot' : 'count';
390
+ control.setAttribute('aria-label',
391
+ this._message('appSidebar.itemBadge', '%1 (%2)', item.label, count));
392
+ }
359
393
  }
360
394
 
361
395
  /** @param {EventTarget} target @param {string} type @param {EventListener} listener @returns {void} */
@@ -494,6 +528,35 @@ export class AppSidebar extends Component {
494
528
  }
495
529
  }
496
530
 
531
+ /**
532
+ * Flags the sublist a branch toggle just opened so the stylesheet can animate that one branch.
533
+ *
534
+ * `_renderTree()` rebuilds every row on every toggle, so a transition declared on the sublist
535
+ * alone would replay on branches that never moved. The attribute is written after the rebuild
536
+ * and disappears with the element on the next one, which is why it needs no teardown.
537
+ * @param {string} key Branch id as a string.
538
+ * @returns {void}
539
+ */
540
+ _markBranchMotion(key) {
541
+ if (!this.refs.list) return;
542
+ const control = [...this.refs.list.querySelectorAll('[data-app-nav-id]')]
543
+ .find((candidate) => candidate.getAttribute('data-app-nav-id') === key) ?? null;
544
+ const sublist = control?.parentElement?.querySelector(':scope > .zx-app-sidebar__sublist');
545
+ if (sublist instanceof HTMLElement) sublist.dataset.branchMotion = 'expand';
546
+ }
547
+
548
+ /**
549
+ * Resolves a message through the host translator, falling back to the built-in English text.
550
+ * @param {string} key Message key.
551
+ * @param {string} fallback Built-in text, with `%1`-style placeholders.
552
+ * @param {...unknown} args Interpolation values.
553
+ * @returns {string}
554
+ */
555
+ _message(key, fallback, ...args) {
556
+ const message = this.msg(key, ...args);
557
+ return message === key ? printf(fallback, args) : message;
558
+ }
559
+
497
560
  /** @returns {void} */
498
561
  _reconcileExpanded() {
499
562
  const branchIds = new Set();
@@ -584,5 +647,17 @@ function normalizeSide(orientation, side) {
584
647
 
585
648
  /** Fired when a destination/action is activated. @event AppSidebar#select @type {CustomEvent<AppNavSelectDetail>} */
586
649
  /** Fired when an inline branch changes. @event AppSidebar#branchchange @type {CustomEvent<{item: AppNavItem|null, id: string|number|null, expanded: boolean|null, ids: Array<string|number>}>} */
650
+ /**
651
+ * Copies a navigation tree, leaving out the empty `children` lists normalization adds to leaves.
652
+ * @param {AppNavItem[]} items Normalized items.
653
+ * @returns {AppNavItem[]}
654
+ */
655
+ function copyItems(items) {
656
+ return items.map((item) => {
657
+ const { children, ...rest } = item;
658
+ return children?.length ? { ...rest, children: copyItems(children) } : rest;
659
+ });
660
+ }
661
+
587
662
  /** Fired when expanded/minimized presentation changes. @event AppSidebar#collapsechange @type {CustomEvent<{collapsed: boolean}>} */
588
663
  /** Fired when a minimized flyout changes. @event AppSidebar#flyoutchange @type {CustomEvent<{item: AppNavItem, id: string|number, open: boolean}>} */
@@ -1,8 +1,10 @@
1
1
  .zx-aurora {
2
+ /* The brand accent, then three chart series: decoration takes categorical colour, never the
3
+ status set, so the light behind a page never reads as a success or a warning. */
2
4
  --zx-aurora-color-1: var(--zx-color-accent);
3
- --zx-aurora-color-2: var(--zx-color-info);
4
- --zx-aurora-color-3: var(--zx-color-success);
5
- --zx-aurora-color-4: var(--zx-color-warning);
5
+ --zx-aurora-color-2: var(--zx-color-chart-1);
6
+ --zx-aurora-color-3: var(--zx-color-chart-3);
7
+ --zx-aurora-color-4: var(--zx-color-chart-4);
6
8
  --zx-aurora-core: 7%;
7
9
  --zx-aurora-wash: 4%;
8
10
  --zx-aurora-faint: 2%;
@@ -1,5 +1,11 @@
1
+ /*
2
+ * The fallback is the avatar most people see, so it carries an identity of its own: initials on a
3
+ * tint from the categorical tone palette, picked by a stable hash of the name and written as
4
+ * `data-tone` by the component. No ring — the tint is the edge. The three presets are steps of the
5
+ * spacing scale: 24 / 32 / 40px at the default unit.
6
+ */
1
7
  .zx-avatar {
2
- --zx-avatar-size: var(--zx-control-height);
8
+ --zx-avatar-size: var(--zx-space-7);
3
9
  position: relative;
4
10
  display: inline-grid;
5
11
  flex: none;
@@ -11,12 +17,22 @@
11
17
  vertical-align: middle;
12
18
  }
13
19
 
14
- .zx-avatar[data-size="sm"] { --zx-avatar-size: calc(var(--zx-control-height) * .75); }
15
- .zx-avatar[data-size="lg"] { --zx-avatar-size: calc(var(--zx-control-height) * 1.25); }
20
+ .zx-avatar[data-size="sm"] { --zx-avatar-size: var(--zx-space-6); }
21
+ .zx-avatar[data-size="lg"] { --zx-avatar-size: var(--zx-space-8); }
16
22
  .zx-avatar[data-shape="circle"] { border-radius: 50%; }
17
23
  .zx-avatar[data-shape="rounded"] { border-radius: var(--zx-radius-md); }
18
24
  .zx-avatar[data-shape="square"] { border-radius: 0; }
19
25
 
26
+ .zx-avatar[data-tone="blue"] { --zx-avatar-bg: var(--zx-color-tone-blue-bg); --zx-avatar-fg: var(--zx-color-tone-blue-fg); }
27
+ .zx-avatar[data-tone="teal"] { --zx-avatar-bg: var(--zx-color-tone-teal-bg); --zx-avatar-fg: var(--zx-color-tone-teal-fg); }
28
+ .zx-avatar[data-tone="green"] { --zx-avatar-bg: var(--zx-color-tone-green-bg); --zx-avatar-fg: var(--zx-color-tone-green-fg); }
29
+ .zx-avatar[data-tone="amber"] { --zx-avatar-bg: var(--zx-color-tone-amber-bg); --zx-avatar-fg: var(--zx-color-tone-amber-fg); }
30
+ .zx-avatar[data-tone="orange"] { --zx-avatar-bg: var(--zx-color-tone-orange-bg); --zx-avatar-fg: var(--zx-color-tone-orange-fg); }
31
+ .zx-avatar[data-tone="red"] { --zx-avatar-bg: var(--zx-color-tone-red-bg); --zx-avatar-fg: var(--zx-color-tone-red-fg); }
32
+ .zx-avatar[data-tone="pink"] { --zx-avatar-bg: var(--zx-color-tone-pink-bg); --zx-avatar-fg: var(--zx-color-tone-pink-fg); }
33
+ .zx-avatar[data-tone="violet"] { --zx-avatar-bg: var(--zx-color-tone-violet-bg); --zx-avatar-fg: var(--zx-color-tone-violet-fg); }
34
+ .zx-avatar[data-tone="slate"] { --zx-avatar-bg: var(--zx-color-tone-slate-bg); --zx-avatar-fg: var(--zx-color-tone-slate-fg); }
35
+
20
36
  .zx-avatar__image,
21
37
  .zx-avatar__fallback {
22
38
  inline-size: 100%;
@@ -35,14 +51,18 @@
35
51
  display: none;
36
52
  }
37
53
 
54
+ /* Initials at 0.4 of the size, never under 10px; one of them below 24px (the component decides). */
38
55
  .zx-avatar__fallback {
39
56
  display: grid;
40
57
  place-items: center;
41
- border: 1px solid var(--zx-color-border);
42
- background: var(--zx-color-bg-muted);
43
- font-size: calc(var(--zx-avatar-size) * .34);
44
- font-weight: 700;
58
+ overflow: hidden;
59
+ background: var(--zx-avatar-bg, var(--zx-color-bg-muted));
60
+ color: var(--zx-avatar-fg, var(--zx-color-text-muted));
61
+ font-size: max(10px, calc(var(--zx-avatar-size) * .4));
62
+ font-weight: var(--zx-weight-semibold);
63
+ letter-spacing: 0;
45
64
  line-height: 1;
65
+ white-space: nowrap;
46
66
  }
47
67
 
48
68
  .zx-avatar__status {
@@ -59,3 +79,8 @@
59
79
  .zx-avatar__status[data-status="online"] { background: var(--zx-color-success); }
60
80
  .zx-avatar__status[data-status="away"] { background: var(--zx-color-warning); }
61
81
  .zx-avatar__status[data-status="busy"] { background: var(--zx-color-danger); }
82
+
83
+ /* Forced colours drop the tint; a system-coloured edge keeps the fallback a shape. */
84
+ @media (forced-colors: active) {
85
+ .zx-avatar__fallback { border: 1px solid CanvasText; }
86
+ }