@zeyos/zx 4.4.0 → 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 (182) hide show
  1. package/CHANGELOG.md +283 -0
  2. package/README.md +42 -10
  3. package/dist/types/components/activity-list/activity-list.d.ts +22 -0
  4. package/dist/types/components/app-sidebar/app-sidebar.d.ts +5 -3
  5. package/dist/types/components/avatar/avatar.d.ts +46 -7
  6. package/dist/types/components/badge/badge.d.ts +21 -1
  7. package/dist/types/components/button/button.d.ts +15 -0
  8. package/dist/types/components/chart/chart.d.ts +10 -0
  9. package/dist/types/components/checklist/checklist.d.ts +9 -0
  10. package/dist/types/components/code-editor/code-editor.d.ts +6 -17
  11. package/dist/types/components/datebox/date-range-box.d.ts +5 -1
  12. package/dist/types/components/datebox/datebox.d.ts +7 -2
  13. package/dist/types/components/dialog/dialog.d.ts +46 -6
  14. package/dist/types/components/dropdown/dropdown.d.ts +2 -2
  15. package/dist/types/components/entity-ref/entity-ref.d.ts +2 -2
  16. package/dist/types/components/field/field.d.ts +24 -2
  17. package/dist/types/components/fieldset/fieldset.d.ts +19 -1
  18. package/dist/types/components/file-item/file-item.d.ts +34 -6
  19. package/dist/types/components/form/form.d.ts +12 -1
  20. package/dist/types/components/kanban-view/kanban-card.d.ts +8 -0
  21. package/dist/types/components/launcher/launcher.d.ts +260 -15
  22. package/dist/types/components/list-toolbar/list-toolbar.d.ts +1 -1
  23. package/dist/types/components/menu-button/menu-button.d.ts +33 -5
  24. package/dist/types/components/message/message.d.ts +119 -17
  25. package/dist/types/components/modal/modal.d.ts +13 -2
  26. package/dist/types/components/select/color.d.ts +12 -0
  27. package/dist/types/components/select/glyphs.d.ts +124 -0
  28. package/dist/types/components/select/select.d.ts +11 -3
  29. package/dist/types/components/stat-tile/stat-tile.d.ts +16 -5
  30. package/dist/types/components/tabbox/tabbox.d.ts +9 -0
  31. package/dist/types/components/table/table.d.ts +46 -1
  32. package/dist/types/components/tag-picker/tag-picker.d.ts +21 -4
  33. package/dist/types/components/tree/tree.d.ts +13 -0
  34. package/dist/types/core/breakpoint.d.ts +1 -1
  35. package/dist/types/core/icons.d.ts +23 -25
  36. package/dist/types/core/motion.d.ts +59 -0
  37. package/dist/types/core/overlay-host.d.ts +15 -1
  38. package/dist/types/index.d.ts +5 -3
  39. package/dist/types/zeyos/connect.d.ts +6 -0
  40. package/dist/types/zeyos/form.d.ts +50 -9
  41. package/dist/types/zeyos/index.d.ts +1 -1
  42. package/dist/types/zeyos/query.d.ts +79 -0
  43. package/dist/types/zeyos/schema.d.ts +24 -1
  44. package/dist/types/zeyos/table.d.ts +100 -14
  45. package/dist/types/zeyos/view.d.ts +10 -0
  46. package/dist/zx-zeyos.esm.js +1621 -443
  47. package/dist/zx-zeyos.esm.min.js +53 -5
  48. package/dist/zx.css +1411 -660
  49. package/dist/zx.esm.js +1976 -703
  50. package/dist/zx.esm.min.js +56 -8
  51. package/dist/zx.global.js +1978 -705
  52. package/dist/zx.global.min.js +56 -8
  53. package/dist/zx.min.css +1 -1
  54. package/docs/api.json +219 -34
  55. package/docs/llms.md +472 -110
  56. package/docs/llms.txt +3 -3
  57. package/package.json +1 -1
  58. package/src/components/account-menu/account-menu.css +35 -10
  59. package/src/components/activity-item/activity-item.css +1 -1
  60. package/src/components/activity-item/activity-item.js +3 -0
  61. package/src/components/activity-list/activity-list.css +38 -3
  62. package/src/components/activity-list/activity-list.js +34 -0
  63. package/src/components/app-icon/app-icon.css +1 -1
  64. package/src/components/app-rail/app-rail.css +40 -79
  65. package/src/components/app-sidebar/app-sidebar.css +109 -174
  66. package/src/components/app-sidebar/app-sidebar.js +21 -6
  67. package/src/components/aurora/aurora.css +5 -3
  68. package/src/components/avatar/avatar.css +32 -7
  69. package/src/components/avatar/avatar.js +72 -8
  70. package/src/components/badge/badge.css +54 -2
  71. package/src/components/badge/badge.js +29 -2
  72. package/src/components/button/button.css +67 -9
  73. package/src/components/button/button.js +38 -0
  74. package/src/components/calendar/calendar.css +46 -46
  75. package/src/components/card/card.css +2 -2
  76. package/src/components/card-view/card-view.css +9 -7
  77. package/src/components/chart/chart.css +4 -1
  78. package/src/components/chart/chart.js +12 -0
  79. package/src/components/check-button/check-button.css +10 -2
  80. package/src/components/checklist/checklist.css +9 -2
  81. package/src/components/checklist/checklist.js +11 -5
  82. package/src/components/checklist/field-adapter.js +3 -1
  83. package/src/components/code-editor/code-editor.css +6 -5
  84. package/src/components/code-editor/code-editor.js +19 -1
  85. package/src/components/context-menu/context-menu.css +9 -4
  86. package/src/components/copy/copy-button.css +6 -0
  87. package/src/components/copy/copy-input.css +8 -3
  88. package/src/components/data-filter/data-filter.css +5 -4
  89. package/src/components/date-picker/date-picker.css +6 -6
  90. package/src/components/date-picker/date-range-picker.css +3 -3
  91. package/src/components/date-picker/time-picker.css +1 -1
  92. package/src/components/datebox/date-range-box.css +79 -2
  93. package/src/components/datebox/date-range-box.js +13 -2
  94. package/src/components/datebox/datebox.css +79 -2
  95. package/src/components/datebox/datebox.js +13 -2
  96. package/src/components/dialog/dialog.css +26 -0
  97. package/src/components/dialog/dialog.js +99 -17
  98. package/src/components/dropdown/dropdown.css +1 -1
  99. package/src/components/dropdown/dropdown.js +5 -3
  100. package/src/components/empty-state/empty-state.css +2 -2
  101. package/src/components/entity-ref/entity-ref.css +1 -1
  102. package/src/components/entity-ref/entity-ref.js +1 -1
  103. package/src/components/field/field.css +28 -9
  104. package/src/components/field/field.js +68 -26
  105. package/src/components/field-upload/field-upload.css +1 -1
  106. package/src/components/fieldset/fieldset.css +5 -3
  107. package/src/components/fieldset/fieldset.js +28 -5
  108. package/src/components/file-item/file-item.js +144 -10
  109. package/src/components/filter/filter.js +7 -3
  110. package/src/components/filter-panel/filter-panel.css +1 -1
  111. package/src/components/finder/finder.css +15 -13
  112. package/src/components/form/form.css +5 -4
  113. package/src/components/form/form.js +11 -2
  114. package/src/components/grid/grid.css +1 -1
  115. package/src/components/groupbox/groupbox.css +1 -1
  116. package/src/components/kanban-view/kanban-card.js +11 -3
  117. package/src/components/kanban-view/kanban-view.css +39 -15
  118. package/src/components/kanban-view/kanban-view.js +4 -4
  119. package/src/components/launcher/launcher.css +107 -25
  120. package/src/components/launcher/launcher.js +503 -41
  121. package/src/components/list-toolbar/list-toolbar.css +13 -8
  122. package/src/components/list-toolbar/list-toolbar.js +1 -1
  123. package/src/components/master-panel/master-panel.css +2 -0
  124. package/src/components/menu-button/menu-button.css +9 -4
  125. package/src/components/menu-button/menu-button.js +65 -9
  126. package/src/components/message/message.css +33 -3
  127. package/src/components/message/message.js +217 -15
  128. package/src/components/modal/modal.js +18 -3
  129. package/src/components/navigation-bar/navigation-bar.css +18 -9
  130. package/src/components/number-field/field-adapter.js +1 -0
  131. package/src/components/number-field/number-field.css +70 -0
  132. package/src/components/number-field/number-field.js +2 -0
  133. package/src/components/pagination/pagination.css +8 -6
  134. package/src/components/panel/panel.css +1 -1
  135. package/src/components/rating/rating.css +7 -0
  136. package/src/components/select/color.js +22 -0
  137. package/src/components/select/field-adapter.js +2 -0
  138. package/src/components/select/glyphs.js +165 -0
  139. package/src/components/select/select.css +138 -45
  140. package/src/components/select/select.js +83 -47
  141. package/src/components/sheet/sheet.css +11 -8
  142. package/src/components/slider/slider.css +16 -2
  143. package/src/components/slider/slider.js +1 -1
  144. package/src/components/sort-control/sort-control.css +4 -1
  145. package/src/components/stat-tile/stat-tile.css +41 -46
  146. package/src/components/stat-tile/stat-tile.js +17 -6
  147. package/src/components/tabbox/tabbox.css +32 -27
  148. package/src/components/tabbox/tabbox.js +5 -1
  149. package/src/components/table/table.css +104 -29
  150. package/src/components/table/table.js +90 -16
  151. package/src/components/tag-picker/tag-picker.css +83 -5
  152. package/src/components/tag-picker/tag-picker.js +25 -8
  153. package/src/components/timebox/timebox.css +22 -2
  154. package/src/components/toggle/toggle.css +12 -1
  155. package/src/components/toolbar/toolbar.js +2 -2
  156. package/src/components/tooltip/tooltip.css +4 -0
  157. package/src/components/tooltip/tooltip.js +10 -3
  158. package/src/components/tree/tree.css +25 -16
  159. package/src/components/tree/tree.js +31 -0
  160. package/src/components/value-list/value-list.css +3 -3
  161. package/src/components/view/record-view.js +1 -1
  162. package/src/core/breakpoint.js +1 -1
  163. package/src/core/icons.js +193 -60
  164. package/src/core/motion.js +147 -0
  165. package/src/core/overlay-host.js +21 -2
  166. package/src/index.js +5 -3
  167. package/src/internal/app-rail.js +1 -2
  168. package/src/zeyos/connect.js +6 -2
  169. package/src/zeyos/form.js +67 -27
  170. package/src/zeyos/index.js +1 -1
  171. package/src/zeyos/query.js +153 -0
  172. package/src/zeyos/schema.js +144 -27
  173. package/src/zeyos/table.js +68 -5
  174. package/src/zeyos/view.js +6 -0
  175. package/styles/base.css +56 -0
  176. package/styles/icons.css +11 -1
  177. package/styles/tokens/dark.css +113 -37
  178. package/styles/tokens/global.css +121 -27
  179. package/styles/tokens/semantic.css +100 -31
  180. package/styles/tokens/themes.css +7 -6
  181. package/styles/zx.css +0 -1
  182. package/styles/tokens/density.css +0 -9
@@ -2,39 +2,49 @@
2
2
  * AppSidebar — the surface an application is judged by before it is read.
3
3
  *
4
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 it rather than picked per rule, which is
6
- * what makes a density switch a real change of rhythm instead of a smaller font.
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.
7
8
  *
8
- * Two deliberate choices worth stating:
9
+ * Three deliberate choices worth stating:
9
10
  *
10
11
  * - **Labels wrap, they never truncate.** German runs 20-35% longer than English and a nav is
11
12
  * where that shows first: "Belege & Rechnungen" is two lines in a 16.5rem sidebar. An ellipsis
12
13
  * there hides the word that distinguishes one destination from another, so the row grows
13
14
  * instead and the fixed icon column keeps the second line aligned under the first.
14
- * - **The active state never rests on colour alone.** It is a filled pill, a marker docked to the
15
- * sidebar's outer edge, and a heavier label — three signals, one of which survives greyscale,
16
- * forced colours and a reduced-transparency page.
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.
17
22
  */
18
23
 
19
24
  .zx-app-sidebar {
20
- /* The scale. `--zx-control-height` and `--zx-space-1..4` are density-aware, so compact
21
- tightens the item, the gaps and the padding together while `--zx-space-5/6` hold the icon
22
- column and the indent steady — a narrower rhythm, not a smaller one. */
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. */
23
27
  --zx-app-sidebar-gutter: var(--zx-space-2);
24
28
  --zx-app-sidebar-icon: var(--zx-space-6);
25
- --zx-app-sidebar-item-gap: var(--zx-space-3);
26
- --zx-app-sidebar-item-height: calc(var(--zx-control-height) + var(--zx-space-2));
27
- --zx-app-sidebar-pad-x: var(--zx-space-3);
28
- --zx-app-sidebar-row-gap: var(--zx-space-1);
29
- --zx-app-sidebar-group-gap: var(--zx-space-4);
30
- --zx-app-sidebar-indent: var(--zx-space-5);
31
- --zx-app-sidebar-marker: 3px;
32
- --zx-app-sidebar-radius: var(--zx-radius-xl);
33
- /* One overlay expression for hover and press. Mixing the *text* colour into the surface moves
34
- the fill away from the page in whichever direction the theme runs, so a single declaration
35
- is correct in light and in dark instead of needing a per-theme override. */
36
- --zx-app-sidebar-hover: color-mix(in srgb, var(--zx-color-text) 7%, transparent);
37
- --zx-app-sidebar-press: color-mix(in srgb, var(--zx-color-text) 14%, transparent);
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);
38
48
  display: flex;
39
49
  min-block-size: 0;
40
50
  border: 1px solid var(--zx-color-glass-border);
@@ -43,11 +53,6 @@
43
53
  color: var(--zx-color-text);
44
54
  }
45
55
 
46
- [data-zx-density="compact"] .zx-app-sidebar,
47
- .zx-app-sidebar[data-zx-density="compact"] {
48
- --zx-app-sidebar-group-gap: var(--zx-space-3);
49
- }
50
-
51
56
  .zx-app-sidebar[data-orientation="horizontal"] {
52
57
  inline-size: 100%;
53
58
  }
@@ -84,6 +89,9 @@
84
89
 
85
90
  .zx-app-sidebar__header-content { min-inline-size: 0; }
86
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
+
87
95
  /* The block padding is not decoration: the focus ring is a 3px outer glow, and a nav that padded
88
96
  its scroll box by less would clip the ring off the first and last row. */
89
97
  .zx-app-sidebar__nav {
@@ -101,13 +109,42 @@
101
109
  list-style: none;
102
110
  }
103
111
 
104
- /* A sidebar whose items carry no icons should not pay for an empty gutter; one that has even a
105
- single icon keeps the column on every row so the labels stay in one line down the list. */
106
- .zx-app-sidebar__list:not(:has(.zx-app-sidebar__item-icon)) {
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)) {
107
118
  --zx-app-sidebar-icon: 0px;
108
119
  --zx-app-sidebar-item-gap: 0px;
109
120
  }
110
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
+
111
148
  .zx-app-sidebar__sublist { position: relative; margin-block-start: var(--zx-app-sidebar-row-gap); }
112
149
  .zx-app-sidebar__sublist[hidden] { display: none; }
113
150
 
@@ -116,8 +153,8 @@
116
153
  .zx-app-sidebar__sublist::before {
117
154
  content: "";
118
155
  position: absolute;
119
- inset-block: var(--zx-space-1);
120
- inset-inline-start: calc(var(--zx-app-sidebar-pad-x) + var(--zx-app-sidebar-icon) / 2);
156
+ inset-block: 2px;
157
+ inset-inline-start: var(--zx-app-sidebar-guide);
121
158
  inline-size: 1px;
122
159
  border-radius: var(--zx-radius-full);
123
160
  background: var(--zx-color-border);
@@ -146,7 +183,9 @@
146
183
  border: 0;
147
184
  border-radius: var(--zx-app-sidebar-radius);
148
185
  background: transparent;
149
- padding-block: var(--zx-space-1);
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);
150
189
  padding-inline: var(--zx-app-sidebar-pad-x);
151
190
  color: var(--zx-color-text);
152
191
  font-size: var(--zx-text-md);
@@ -156,49 +195,14 @@
156
195
  text-decoration: none;
157
196
  }
158
197
 
159
- /* A nav is a touch target before it is a dense list. Compact tightens the drawn row, and a
160
- coarse pointer puts the hit area back to 40px whatever the density says. */
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. */
161
200
  @media (pointer: coarse) {
162
201
  .zx-app-sidebar__item { min-block-size: 40px; }
163
202
  }
164
203
 
165
- .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-indent)); }
166
- .zx-app-sidebar__row[data-level="3"] > .zx-app-sidebar__item { padding-inline-start: calc(var(--zx-app-sidebar-pad-x) + 2 * var(--zx-app-sidebar-indent)); }
167
-
168
- /* The active marker. It is anchored to the control and not to the row, because a row is the
169
- branch *and everything open under it* — a marker measured against that would grow into a
170
- bracket down the whole group. It sits in the nav's own gutter, docked to the sidebar's outer
171
- edge, so every marker lines up whatever the nesting depth, and it is the signal that survives
172
- when colour does not. A collapsed branch that merely *contains* the active route gets a short
173
- stub of the same marker: the same vocabulary, a quieter sentence. */
174
- .zx-app-sidebar__item::before {
175
- content: "";
176
- position: absolute;
177
- inset-block: 50%;
178
- inset-inline-start: calc(-1 * var(--zx-app-sidebar-gutter));
179
- inline-size: var(--zx-app-sidebar-marker);
180
- border-start-end-radius: var(--zx-radius-full);
181
- border-end-end-radius: var(--zx-radius-full);
182
- background: var(--zx-color-accent);
183
- opacity: 0;
184
- }
185
-
186
- .zx-app-sidebar[data-side="right"] .zx-app-sidebar__item::before {
187
- inset-inline: auto calc(-1 * var(--zx-app-sidebar-gutter));
188
- border-radius: 0;
189
- border-start-start-radius: var(--zx-radius-full);
190
- border-end-start-radius: var(--zx-radius-full);
191
- }
192
-
193
- .zx-app-sidebar__item[aria-current="page"]::before {
194
- inset-block: 25%;
195
- opacity: 1;
196
- }
197
-
198
- .zx-app-sidebar__item[data-active-descendant="true"][aria-expanded="false"]::before {
199
- inset-block: 38%;
200
- opacity: .55;
201
- }
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)); }
202
206
 
203
207
  .zx-app-sidebar__item-icon {
204
208
  grid-column: 1;
@@ -241,42 +245,34 @@
241
245
  .zx-app-sidebar__item:hover .zx-app-sidebar__item-disclosure { color: var(--zx-color-text); }
242
246
  .zx-app-sidebar__item:active { background: var(--zx-app-sidebar-press); }
243
247
 
244
- /* A branch that is closed over the active route keeps the accent in the label and the marker
245
- stub outside it; a branch that is open says so with its chevron and needs neither. */
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. */
246
251
  .zx-app-sidebar__item[data-active-descendant="true"][aria-expanded="false"] {
247
- color: var(--zx-color-accent);
248
- font-weight: 600;
252
+ font-weight: var(--zx-weight-semibold);
249
253
  }
250
254
 
251
- .zx-app-sidebar__item[aria-current="page"] {
252
- background: var(--zx-color-accent);
253
- color: var(--zx-color-on-accent);
254
- font-weight: 600;
255
+ .zx-app-sidebar__item[data-active-descendant="true"][aria-expanded="false"] .zx-app-sidebar__item-icon {
256
+ color: var(--zx-color-text);
255
257
  }
256
258
 
257
- /* Hover and press on the pill move the fill toward the text colour, which raises the contrast
258
- with `--zx-color-on-accent` in both polarities instead of dropping it in one of them. */
259
- .zx-app-sidebar__item[aria-current="page"]:hover {
260
- background: color-mix(in srgb, var(--zx-color-text) 8%, var(--zx-color-accent));
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);
261
267
  }
262
268
 
263
269
  .zx-app-sidebar__item[aria-current="page"]:active {
264
- background: color-mix(in srgb, var(--zx-color-text) 16%, var(--zx-color-accent));
265
- }
266
-
267
- /* Focus is a ring, never a fill: a keyboard user arrowing past the active route must still be
268
- able to tell which row is current. Ordinary rows take the shared `--zx-focus-ring` from
269
- `base.css` untouched; only the filled pill needs help, because an accent glow around an accent
270
- fill is a ring nobody can see. The inner line is drawn in the pill's own text colour, which the
271
- preset contract already guarantees against that fill. */
272
- .zx-app-sidebar__item[aria-current="page"]:focus-visible {
273
- box-shadow: inset 0 0 0 2px var(--zx-color-on-accent), var(--zx-focus-ring);
270
+ background: var(--zx-app-sidebar-press);
274
271
  }
275
272
 
276
273
  .zx-app-sidebar__item[aria-current="page"] .zx-app-sidebar__item-icon,
277
- .zx-app-sidebar__item[aria-current="page"] .zx-app-sidebar__item-badge,
278
274
  .zx-app-sidebar__item[aria-current="page"] .zx-app-sidebar__item-disclosure {
279
- color: inherit;
275
+ color: var(--zx-color-text);
280
276
  }
281
277
 
282
278
  /* Unavailable, and still readable. A blanket `opacity` on the row took the label under 3:1, which
@@ -301,16 +297,12 @@
301
297
  .zx-app-sidebar__rail-header { display: grid; gap: var(--zx-space-1); }
302
298
 
303
299
  /*
304
- * The minimized rail. Only the presentation changes; the same vocabulary applies — filled pill,
305
- * docked marker, badge that survives the loss of the label.
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.
306
303
  */
307
- .zx-app-sidebar__rail .zx-app-rail__item[aria-current="page"] {
308
- background: var(--zx-color-accent);
309
- color: var(--zx-color-on-accent);
310
- }
311
-
312
- .zx-app-sidebar__rail .zx-app-rail__item[aria-current="page"]:hover {
313
- background: color-mix(in srgb, var(--zx-color-text) 8%, var(--zx-color-accent));
304
+ .zx-app-sidebar__rail .zx-app-rail__item {
305
+ color: var(--zx-color-text-muted);
314
306
  }
315
307
 
316
308
  .zx-app-sidebar__rail .zx-app-rail__item:not([aria-current="page"]):hover,
@@ -319,65 +311,25 @@
319
311
  color: var(--zx-color-text);
320
312
  }
321
313
 
322
- .zx-app-sidebar__rail .zx-app-rail__item:not([aria-current="page"]):active {
314
+ .zx-app-sidebar__rail .zx-app-rail__item:active {
323
315
  background: var(--zx-app-sidebar-press);
324
316
  }
325
317
 
326
- .zx-app-sidebar__rail .zx-app-rail__item[aria-current="page"]:focus-visible {
327
- box-shadow: inset 0 0 0 2px var(--zx-color-on-accent), var(--zx-focus-ring);
328
- }
329
-
330
- .zx-app-sidebar__rail .zx-app-rail__item::after {
331
- content: "";
332
- position: absolute;
333
- inset-block: 22%;
334
- inset-inline-start: calc(-1 * var(--zx-space-1));
335
- inline-size: var(--zx-app-sidebar-marker);
336
- border-start-end-radius: var(--zx-radius-full);
337
- border-end-end-radius: var(--zx-radius-full);
338
- background: var(--zx-color-accent);
339
- opacity: 0;
340
- }
341
-
342
- .zx-app-sidebar[data-side="right"] .zx-app-rail__item::after {
343
- inset-inline: auto calc(-1 * var(--zx-space-1));
344
- border-radius: 0;
345
- border-start-start-radius: var(--zx-radius-full);
346
- border-end-start-radius: var(--zx-radius-full);
347
- }
348
-
349
- .zx-app-sidebar[data-orientation="horizontal"] .zx-app-rail__item::after {
350
- inset-block: auto calc(-1 * var(--zx-space-1));
351
- inset-inline: 22%;
352
- inline-size: auto;
353
- block-size: var(--zx-app-sidebar-marker);
354
- border-radius: 0;
355
- border-start-start-radius: var(--zx-radius-full);
356
- border-start-end-radius: var(--zx-radius-full);
357
- }
358
-
359
- .zx-app-sidebar[data-side="bottom"] .zx-app-rail__item::after {
360
- inset-block: calc(-1 * var(--zx-space-1)) auto;
361
- border-radius: 0;
362
- border-end-start-radius: var(--zx-radius-full);
363
- border-end-end-radius: var(--zx-radius-full);
364
- }
365
-
366
- .zx-app-sidebar__rail .zx-app-rail__item[aria-current="page"]::after { opacity: 1; }
367
- .zx-app-sidebar__rail .zx-app-rail__item[data-active-descendant="true"]:not([aria-current="page"])::after {
368
- inset-block: 34%;
369
- opacity: .55;
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);
370
321
  }
371
322
 
372
- .zx-app-sidebar[data-orientation="horizontal"] .zx-app-rail__item[data-active-descendant="true"]:not([aria-current="page"])::after {
373
- inset-block: auto calc(-1 * var(--zx-space-1));
374
- inset-inline: 34%;
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);
375
327
  }
376
328
 
377
329
  /*
378
330
  * The count a minimized rail would otherwise lose. `data-zx-badge` is set by the component from
379
331
  * the item's own badge: a number that fits stays a number, and one that does not becomes a dot,
380
- * because a three-digit pill on a 32px target stops being a badge and starts being a label. The
332
+ * because a three-digit pill on a 30px target stops being a badge and starts being a label. The
381
333
  * figure itself moves into the control's accessible name either way, so nothing is dropped —
382
334
  * only redrawn.
383
335
  */
@@ -393,7 +345,7 @@
393
345
  color: var(--zx-color-on-accent);
394
346
  font-size: var(--zx-text-xs);
395
347
  font-variant-numeric: tabular-nums;
396
- font-weight: 600;
348
+ font-weight: var(--zx-weight-semibold);
397
349
  line-height: var(--zx-space-4);
398
350
  text-align: center;
399
351
  }
@@ -407,14 +359,6 @@
407
359
  line-height: 0;
408
360
  }
409
361
 
410
- /* On the filled pill the badge would be accent-on-accent. Invert it so the count keeps a fill
411
- of its own without introducing a third colour. */
412
- .zx-app-sidebar__rail .zx-app-rail__item[aria-current="page"] .zx-app-rail__item-badge {
413
- background: var(--zx-color-on-accent);
414
- box-shadow: none;
415
- color: var(--zx-color-accent);
416
- }
417
-
418
362
  /*
419
363
  * Motion. Everything here is decoration on a state change that is already expressed statically,
420
364
  * so all of it is opt-in.
@@ -431,12 +375,6 @@
431
375
  box-shadow var(--zx-dur-fast) var(--zx-ease);
432
376
  }
433
377
 
434
- .zx-app-sidebar__item::before,
435
- .zx-app-sidebar__rail .zx-app-rail__item::after {
436
- transition: opacity var(--zx-dur-base) var(--zx-ease),
437
- inset-block var(--zx-dur-base) var(--zx-ease);
438
- }
439
-
440
378
  .zx-app-sidebar__item:active,
441
379
  .zx-app-sidebar__rail .zx-app-rail__item:active { transform: scale(.985); }
442
380
 
@@ -456,8 +394,8 @@
456
394
  to { opacity: 1; transform: translateY(0); }
457
395
  }
458
396
 
459
- /* Forced colours drop backgrounds and box-shadows, so the pill and the marker both disappear.
460
- The system's own selected-item pair carries the state there instead. */
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. */
461
399
  @media (forced-colors: active) {
462
400
  .zx-app-sidebar__item[aria-current="page"],
463
401
  .zx-app-sidebar__rail .zx-app-rail__item[aria-current="page"],
@@ -465,9 +403,6 @@
465
403
  background: Highlight;
466
404
  color: HighlightText;
467
405
  }
468
-
469
- .zx-app-sidebar__item::before,
470
- .zx-app-sidebar__rail .zx-app-rail__item::after { background: Highlight; }
471
406
  }
472
407
 
473
408
  @media (prefers-reduced-transparency: reduce) {
@@ -78,7 +78,9 @@ export class AppSidebar extends Component {
78
78
  /** @returns {HTMLElement} */
79
79
  render() {
80
80
  this._createdRoot = this.el === null;
81
- 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'));
82
84
  this.el = root;
83
85
  this._original = this._createdRoot ? null : {
84
86
  attributes: Array.from(root.attributes, (attribute) => [attribute.name, attribute.value]),
@@ -107,12 +109,13 @@ export class AppSidebar extends Component {
107
109
  return this;
108
110
  }
109
111
 
110
- /** 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
+ */
111
117
  getItems() {
112
- return this._items.map((item) => ({
113
- ...item,
114
- children: item.children?.map((child) => ({ ...child }))
115
- }));
118
+ return copyItems(this._items);
116
119
  }
117
120
 
118
121
  /** Sets the active id without routing. @param {string|number|null} id @returns {this} */
@@ -644,5 +647,17 @@ function normalizeSide(orientation, side) {
644
647
 
645
648
  /** Fired when a destination/action is activated. @event AppSidebar#select @type {CustomEvent<AppNavSelectDetail>} */
646
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
+
647
662
  /** Fired when expanded/minimized presentation changes. @event AppSidebar#collapsechange @type {CustomEvent<{collapsed: boolean}>} */
648
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
+ }