@zeyos/zx 5.0.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 (154) hide show
  1. package/CHANGELOG.md +164 -0
  2. package/README.md +2 -1
  3. package/dist/types/components/account-menu/account-menu.d.ts +8 -0
  4. package/dist/types/components/activity-list/activity-list.d.ts +9 -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 +288 -14
  7. package/dist/types/components/calendar/calendar.d.ts +67 -5
  8. package/dist/types/components/card-view/card-view.d.ts +7 -4
  9. package/dist/types/components/card-view/record-card.d.ts +11 -0
  10. package/dist/types/components/context-menu/context-menu.d.ts +6 -2
  11. package/dist/types/components/data-filter/data-filter.d.ts +4 -4
  12. package/dist/types/components/dropdown/dropdown.d.ts +22 -7
  13. package/dist/types/components/field/field.d.ts +30 -0
  14. package/dist/types/components/fieldset/external-adapter.d.ts +83 -0
  15. package/dist/types/components/fieldset/fieldset.d.ts +7 -0
  16. package/dist/types/components/file-list/file-list.d.ts +35 -3
  17. package/dist/types/components/filter/filter.d.ts +127 -3
  18. package/dist/types/components/filter-panel/filter-panel.d.ts +36 -7
  19. package/dist/types/components/launcher/launcher.d.ts +8 -0
  20. package/dist/types/components/list-toolbar/list-toolbar.d.ts +15 -6
  21. package/dist/types/components/menu-button/menu-button.d.ts +6 -0
  22. package/dist/types/components/message/message.d.ts +14 -1
  23. package/dist/types/components/modal/modal.d.ts +30 -0
  24. package/dist/types/components/module-bar/module-bar.d.ts +395 -0
  25. package/dist/types/components/pagination/pagination.d.ts +17 -3
  26. package/dist/types/components/select/select.d.ts +6 -0
  27. package/dist/types/components/sheet/sheet.d.ts +15 -0
  28. package/dist/types/components/table/table.d.ts +265 -17
  29. package/dist/types/components/view/record-view.d.ts +59 -2
  30. package/dist/types/components/view-settings/view-settings.d.ts +879 -0
  31. package/dist/types/components/workspace-switcher/workspace-switcher.d.ts +327 -0
  32. package/dist/types/core/component.d.ts +12 -2
  33. package/dist/types/core/i18n.d.ts +27 -3
  34. package/dist/types/core/keyboard.d.ts +98 -1
  35. package/dist/types/core/messages.d.ts +382 -0
  36. package/dist/types/core/overlay-host.d.ts +28 -1
  37. package/dist/types/core/overlay-stack.d.ts +260 -0
  38. package/dist/types/index.d.ts +5 -2
  39. package/dist/types/internal/app-rail.d.ts +38 -0
  40. package/dist/types/internal/menu-items.d.ts +71 -3
  41. package/dist/types/zeyos/index.d.ts +1 -0
  42. package/dist/types/zeyos/launcher.d.ts +9 -0
  43. package/dist/types/zeyos/module-bar.d.ts +176 -0
  44. package/dist/types/zeyos/query.d.ts +31 -5
  45. package/dist/types/zeyos/table.d.ts +245 -8
  46. package/dist/types/zeyos/view.d.ts +16 -2
  47. package/dist/zx-zeyos.esm.js +4422 -430
  48. package/dist/zx-zeyos.esm.min.js +5 -5
  49. package/dist/zx.css +1417 -134
  50. package/dist/zx.esm.js +6986 -830
  51. package/dist/zx.esm.min.js +8 -8
  52. package/dist/zx.global.js +6986 -830
  53. package/dist/zx.global.min.js +8 -8
  54. package/dist/zx.min.css +1 -1
  55. package/docs/api.json +794 -17
  56. package/docs/llms.md +503 -63
  57. package/docs/llms.txt +6 -2
  58. package/package.json +1 -1
  59. package/src/components/account-menu/account-menu.js +12 -0
  60. package/src/components/activity-list/activity-list.js +27 -1
  61. package/src/components/app-icon/app-icon.css +13 -2
  62. package/src/components/app-icon/app-icon.js +11 -1
  63. package/src/components/app-rail/app-rail.css +19 -0
  64. package/src/components/app-sidebar/app-sidebar.css +204 -3
  65. package/src/components/app-sidebar/app-sidebar.js +646 -40
  66. package/src/components/breadcrumb/breadcrumb.css +1 -1
  67. package/src/components/calendar/calendar.css +1 -1
  68. package/src/components/calendar/calendar.js +26 -52
  69. package/src/components/card/card.css +2 -2
  70. package/src/components/card-view/card-view.css +5 -5
  71. package/src/components/card-view/card-view.js +13 -9
  72. package/src/components/card-view/record-card.js +14 -3
  73. package/src/components/code-editor/code-editor.css +1 -1
  74. package/src/components/code-editor/field-adapter.js +9 -2
  75. package/src/components/context-menu/context-menu.js +8 -2
  76. package/src/components/copy/copy-input.css +1 -1
  77. package/src/components/data-filter/data-filter.js +2 -2
  78. package/src/components/date-picker/time-picker.css +2 -2
  79. package/src/components/dock/dock.css +5 -4
  80. package/src/components/dropdown/dropdown.js +157 -72
  81. package/src/components/field/field.js +33 -3
  82. package/src/components/fieldset/external-adapter.js +363 -0
  83. package/src/components/fieldset/fieldset.js +10 -59
  84. package/src/components/file-list/file-list.js +144 -21
  85. package/src/components/filter/filter.css +86 -2
  86. package/src/components/filter/filter.js +300 -8
  87. package/src/components/filter-panel/filter-panel.js +149 -32
  88. package/src/components/finder/finder.css +1 -1
  89. package/src/components/kanban-view/kanban-view.css +3 -3
  90. package/src/components/kanban-view/kanban-view.js +5 -2
  91. package/src/components/launcher/launcher.css +1 -1
  92. package/src/components/launcher/launcher.js +29 -3
  93. package/src/components/list-toolbar/list-toolbar.js +39 -13
  94. package/src/components/master-panel/master-panel.js +0 -1
  95. package/src/components/menu-button/menu-button.css +63 -0
  96. package/src/components/menu-button/menu-button.js +97 -11
  97. package/src/components/message/message.js +60 -20
  98. package/src/components/modal/modal.css +1 -1
  99. package/src/components/modal/modal.js +66 -13
  100. package/src/components/module-bar/module-bar.css +486 -0
  101. package/src/components/module-bar/module-bar.js +1074 -0
  102. package/src/components/multi-value-editor/multi-value-editor.css +1 -1
  103. package/src/components/number-field/field-adapter.js +2 -1
  104. package/src/components/pagination/pagination.css +5 -5
  105. package/src/components/pagination/pagination.js +20 -4
  106. package/src/components/panel/panel.css +3 -2
  107. package/src/components/questionnaire/questionnaire.css +4 -4
  108. package/src/components/rating/field-adapter.js +2 -1
  109. package/src/components/rating/rating.css +1 -1
  110. package/src/components/search/search.css +4 -1
  111. package/src/components/select/select.js +9 -0
  112. package/src/components/sheet/sheet.css +1 -1
  113. package/src/components/sheet/sheet.js +74 -32
  114. package/src/components/slider/field-adapter.js +6 -1
  115. package/src/components/slider/slider.css +2 -2
  116. package/src/components/split-view/split-view.css +1 -1
  117. package/src/components/stat-tile/stat-tile.css +1 -1
  118. package/src/components/stepper/stepper.css +1 -1
  119. package/src/components/tabbox/tabbox.css +9 -5
  120. package/src/components/table/table.css +86 -11
  121. package/src/components/table/table.js +628 -57
  122. package/src/components/tag-picker/field-adapter.js +2 -1
  123. package/src/components/tooltip/tooltip.js +35 -17
  124. package/src/components/tree/tree.css +1 -1
  125. package/src/components/view/record-view.css +1 -1
  126. package/src/components/view/record-view.js +178 -18
  127. package/src/components/view-settings/view-settings.css +285 -0
  128. package/src/components/view-settings/view-settings.js +1830 -0
  129. package/src/components/workspace-switcher/workspace-switcher.css +188 -0
  130. package/src/components/workspace-switcher/workspace-switcher.js +463 -0
  131. package/src/core/component.js +88 -17
  132. package/src/core/fontawesome.js +5 -1
  133. package/src/core/i18n.js +47 -4
  134. package/src/core/icons.js +12 -2
  135. package/src/core/keyboard.js +206 -11
  136. package/src/core/messages.js +427 -0
  137. package/src/core/overlay-host.js +64 -9
  138. package/src/core/overlay-stack.js +487 -0
  139. package/src/core/position.js +22 -3
  140. package/src/index.js +5 -2
  141. package/src/internal/app-rail.js +134 -53
  142. package/src/internal/menu-items.js +61 -3
  143. package/src/zeyos/form.js +5 -3
  144. package/src/zeyos/index.js +3 -0
  145. package/src/zeyos/launcher.js +8 -3
  146. package/src/zeyos/module-bar.js +181 -0
  147. package/src/zeyos/query.js +44 -13
  148. package/src/zeyos/table.js +220 -80
  149. package/src/zeyos/view.js +43 -93
  150. package/styles/base.css +16 -1
  151. package/styles/print.css +1 -1
  152. package/styles/tokens/dark.css +4 -0
  153. package/styles/tokens/semantic.css +56 -28
  154. package/styles/zx.css +3 -0
package/docs/llms.txt CHANGED
@@ -35,7 +35,9 @@ exports and as `window.zx.*`.
35
35
  drag and resize, `optimistic: true|false`), backend-neutral nested `Filter`, simple client-side
36
36
  `DataFilter`, `FilterPanel` (typed filter form built from a server's own field metadata;
37
37
  `mode: 'draft'|'live'`, `serialize`), `ListToolbar` (search, live result count, view switch, and
38
- tools whose badge is part of the accessible name), `Pagination` (+ pure `paginationRange()`),
38
+ tools whose badge is part of the accessible name), `ViewSettings` (view-settings drawer content:
39
+ reorder, hide, and add fields, grouping, board axes; `bind()` to a record view),
40
+ `Pagination` (+ pure `paginationRange()`),
39
41
  `SortControl` (searchable field selector with a direction toggle, one atomic `{id,dir}`),
40
42
  `TreeView` (APG tree), `Finder` (Miller columns).
41
43
  - Forms: `Form`, `Fieldset`, `Questionnaire` (guided one-question-at-a-time flow with branching), `Field` (type registry), `CodeEditor` (structured text in a native textarea; never executes or injects the value), `ValueList`, `MultiValueEditor`, `FieldUpload`.
@@ -47,7 +49,9 @@ exports and as `window.zx.*`.
47
49
  canvas light; shared material roles govern chrome and overlays), `Avatar`, `Panel` (header/footer action
48
50
  buttons), `MasterPanel`, `Tabbox`
49
51
  (`variant: 'divided'|'bracket'|'line'|'segmented'`), `NavigationBar`, `AppSidebar` (expanded and
50
- minimized vertical states plus horizontal rail layouts), `AccountMenu`, `Launcher`,
52
+ minimized vertical states plus horizontal rail layouts; sections; a modules layout), `ModuleBar`
53
+ (colour-coded module strip or bar with flyout menus and an "All modules" catalogue), `AccountMenu`,
54
+ `WorkspaceSwitcher` (workspace tile, name and menu at the head of a navigation column), `Launcher`,
51
55
  `spinner()`/`ProgressBar`/`InlineLoading`, `skeleton()`/`skeletonText()`/`skeletonTable()`,
52
56
  `Toolbar` (APG toolbar with overflow menu), `SplitView`, `Dock`
53
57
  (collapsible resizable pane stack; tab groups, regions, adopts a `Sheet`), `Stepper`,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zeyos/zx",
3
- "version": "5.0.0",
3
+ "version": "5.2.0",
4
4
  "description": "Dependency-free JavaScript implementation of the Xenon Design System.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -124,6 +124,18 @@ export class AccountMenu extends Component {
124
124
  return this;
125
125
  }
126
126
 
127
+ /**
128
+ * Shows the avatar alone or the avatar with the name and secondary line — for a navigation
129
+ * column that narrows to icons and widens again. The trigger keeps its full accessible name
130
+ * either way.
131
+ * @param {boolean} compact
132
+ * @returns {this}
133
+ */
134
+ setCompact(compact) {
135
+ this.el.dataset.compact = String(Boolean(compact));
136
+ return this;
137
+ }
138
+
127
139
  /** Replaces account actions. @param {MenuButtonItem[]} items @returns {this} */
128
140
  setItems(items) {
129
141
  this._menu.setItems(items);
@@ -1,6 +1,7 @@
1
1
  // @ts-check
2
2
  import { Component } from '../../core/component.js';
3
3
  import { h, restoreTarget, snapshotTarget } from '../../core/dom.js';
4
+ import { captureItemFocus, restoreItemFocus } from '../../core/keyboard.js';
4
5
  import { uid } from '../../core/util.js';
5
6
  import { skeleton, skeletonText } from '../skeleton/skeleton.js';
6
7
  import { ActivityItem } from '../activity-item/activity-item.js';
@@ -208,8 +209,33 @@ export class ActivityList extends Component {
208
209
  if (!this._createdRoot) restoreTarget(this.el, this._original);
209
210
  }
210
211
 
211
- /** @private @returns {void} */
212
+ /**
213
+ * Rebuilds the list. Items are regrouped on every mutation, so the rows are new; focus that sat
214
+ * on an item's control is put back on the same control of the same activity (matched by `id`,
215
+ * else by position), so a live feed prepending entries does not throw the reader to `<body>`.
216
+ * @private
217
+ * @returns {void}
218
+ */
212
219
  _renderList() {
220
+ const root = /** @type {HTMLElement} */ (this.el);
221
+ const focus = captureItemFocus(root, (element) => {
222
+ const index = this._itemComponents.findIndex((item) => item.el?.contains(element));
223
+ if (index < 0) return null;
224
+ const component = this._itemComponents[index];
225
+ return { key: { id: component.getActivity().id ?? null, index }, element: component.el };
226
+ });
227
+ this._renderItems();
228
+ restoreItemFocus(root, focus, (key) => {
229
+ const memo = /** @type {{id: unknown, index: number}} */ (key);
230
+ const match = memo.id === null ? undefined
231
+ : this._itemComponents.find((item) => Object.is(item.getActivity().id ?? null, memo.id));
232
+ const fallback = match ?? this._itemComponents[Math.min(memo.index, this._itemComponents.length - 1)];
233
+ return fallback?.el ?? null;
234
+ });
235
+ }
236
+
237
+ /** @private @returns {void} */
238
+ _renderItems() {
213
239
  this._destroyItems();
214
240
  this._groupSequence = 0;
215
241
  const root = /** @type {HTMLElement} */ (this.el);
@@ -26,6 +26,17 @@
26
26
  border-radius: var(--zx-radius-full);
27
27
  }
28
28
 
29
+ /* A tone is identity from the theme's categorical palette. An inline `color` overrides it. */
30
+ .zx-app-icon[data-tone="blue"] { --zx-app-icon-color: var(--zx-color-tone-blue-solid); }
31
+ .zx-app-icon[data-tone="teal"] { --zx-app-icon-color: var(--zx-color-tone-teal-solid); }
32
+ .zx-app-icon[data-tone="green"] { --zx-app-icon-color: var(--zx-color-tone-green-solid); }
33
+ .zx-app-icon[data-tone="amber"] { --zx-app-icon-color: var(--zx-color-tone-amber-solid); }
34
+ .zx-app-icon[data-tone="orange"] { --zx-app-icon-color: var(--zx-color-tone-orange-solid); }
35
+ .zx-app-icon[data-tone="red"] { --zx-app-icon-color: var(--zx-color-tone-red-solid); }
36
+ .zx-app-icon[data-tone="pink"] { --zx-app-icon-color: var(--zx-color-tone-pink-solid); }
37
+ .zx-app-icon[data-tone="violet"] { --zx-app-icon-color: var(--zx-color-tone-violet-solid); }
38
+ .zx-app-icon[data-tone="slate"] { --zx-app-icon-color: var(--zx-color-tone-slate-solid); }
39
+
29
40
  .zx-app-icon__surface {
30
41
  position: absolute;
31
42
  z-index: 0;
@@ -62,7 +73,7 @@
62
73
  .zx-app-icon[data-glass="none"] .zx-app-icon__surface::after { display: none; }
63
74
 
64
75
  .zx-app-icon[data-selected] .zx-app-icon__surface {
65
- outline: none;
76
+ outline-color: transparent;
66
77
  box-shadow: 0 0 0 1px var(--zx-color-bg-page),
67
78
  0 0 0 3px var(--zx-app-icon-selected-ring),
68
79
  var(--zx-app-icon-shadow-hover),
@@ -166,7 +177,7 @@
166
177
 
167
178
  .zx-app-icon:is(a, button, [role="button"]):not(:disabled):not([aria-disabled="true"]):focus-visible .zx-app-icon__surface,
168
179
  :is(a, button, [role="button"]):not(:disabled):not([aria-disabled="true"]):focus-visible .zx-app-icon__surface {
169
- outline: none;
180
+ outline-color: transparent;
170
181
  box-shadow: var(--zx-focus-ring), var(--zx-app-icon-shadow-hover), var(--zx-app-icon-halo);
171
182
  }
172
183
 
@@ -3,10 +3,16 @@ import { h, restoreTarget, snapshotTarget } from '../../core/dom.js';
3
3
  import { icon as renderIcon } from '../../core/icons.js';
4
4
  import { isCssColor } from '../../core/util.js';
5
5
 
6
+ /** @typedef {'blue'|'teal'|'green'|'amber'|'orange'|'red'|'pink'|'violet'|'slate'} AppIconTone */
7
+ /** The categorical tones a `tone` option accepts. @type {ReadonlyArray<AppIconTone>} */
8
+ export const APP_ICON_TONES = Object.freeze(['blue', 'teal', 'green', 'amber', 'orange', 'red', 'pink', 'violet', 'slate']);
9
+
6
10
  /**
7
11
  * @typedef {Object} AppIconOptions
8
12
  * @property {string|Node|(()=>string|Node)|null} [icon=null] Icon name, node, or lazy renderer.
9
- * @property {string|null} [color=null] Identity colour; falls back to the accent colour.
13
+ * @property {string|null} [color=null] Identity colour, any CSS colour; takes precedence over `tone`.
14
+ * @property {AppIconTone|null} [tone=null] Categorical identity colour from the theme's tone palette,
15
+ * drawn with its `--zx-color-tone-<tone>-solid` fill. Without `color` or `tone` the icon takes the accent.
10
16
  * @property {number|string} [size=36] Edge length in pixels or any CSS length.
11
17
  * @property {number|string} [iconSize='52%'] Glyph size.
12
18
  * @property {string|null} [label=null] Accessible image label; null makes the icon decorative.
@@ -28,6 +34,7 @@ export class AppIcon extends Component {
28
34
  static defaults = {
29
35
  icon: null,
30
36
  color: null,
37
+ tone: null,
31
38
  size: 36,
32
39
  iconSize: '52%',
33
40
  label: null,
@@ -80,6 +87,9 @@ export class AppIcon extends Component {
80
87
  this.el.style.setProperty('--zx-app-icon-glyph-size', glyphSize);
81
88
  if (isCssColor(state.color)) this.el.style.setProperty('--zx-app-icon-color', String(state.color));
82
89
  else this.el.style.removeProperty('--zx-app-icon-color');
90
+ // An inline colour outranks the tone rule in the stylesheet, which is what makes `color` win.
91
+ if (APP_ICON_TONES.includes(state.tone)) this.el.dataset.tone = state.tone;
92
+ else delete this.el.dataset.tone;
83
93
  this.el.dataset.glass = normalizeGlass(state.glass);
84
94
  this.el.dataset.shape = normalizeShape(state.shape);
85
95
  this.el.toggleAttribute('data-selected', Boolean(state.selected));
@@ -34,6 +34,16 @@
34
34
  .zx-app-rail[data-orientation="horizontal"] .zx-app-rail__list { flex-direction: row; }
35
35
  .zx-app-rail__row { display: flex; }
36
36
 
37
+ /* Where a section of the tree ends on a rail: a short rule, since a rail has no room for a heading. */
38
+ .zx-app-rail__separator {
39
+ flex: none;
40
+ align-self: center;
41
+ background: var(--zx-color-border-subtle);
42
+ }
43
+
44
+ .zx-app-rail[data-orientation="vertical"] .zx-app-rail__separator { inline-size: var(--zx-space-4); block-size: 1px; margin-block: var(--zx-space-1); }
45
+ .zx-app-rail[data-orientation="horizontal"] .zx-app-rail__separator { inline-size: 1px; block-size: var(--zx-space-4); margin-inline: var(--zx-space-1); }
46
+
37
47
  .zx-app-rail__item {
38
48
  position: relative;
39
49
  display: inline-grid;
@@ -133,6 +143,15 @@
133
143
  line-height: 1.3;
134
144
  }
135
145
 
146
+ /* A section inside a flyout keeps its heading, in the same quiet caption as the panel's title. */
147
+ .zx-app-rail__flyout-section {
148
+ padding: var(--zx-space-2) var(--zx-app-rail-pad-x) var(--zx-space-1);
149
+ color: var(--zx-color-text-muted);
150
+ font-size: var(--zx-text-xs);
151
+ font-weight: var(--zx-weight-semibold);
152
+ line-height: 1.3;
153
+ }
154
+
136
155
  .zx-app-rail__flyout-list {
137
156
  display: grid;
138
157
  gap: var(--zx-app-rail-row-gap);
@@ -113,7 +113,9 @@
113
113
  single icon keeps the column on every row so the labels stay in one line down the list. Each
114
114
  list answers for its own rows only — a branch of plain labels under iconned parents drops the
115
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)),
116
+ .zx-app-sidebar__list:not(:has(> .zx-app-sidebar__row > .zx-app-sidebar__item .zx-app-sidebar__item-icon,
117
+ > .zx-app-sidebar__section > .zx-app-sidebar__section-list > .zx-app-sidebar__row > .zx-app-sidebar__item .zx-app-sidebar__item-icon)),
118
+ .zx-app-sidebar__section-list:not(:has(> .zx-app-sidebar__row > .zx-app-sidebar__item .zx-app-sidebar__item-icon)),
117
119
  .zx-app-sidebar__sublist:not(:has(> .zx-app-sidebar__row > .zx-app-sidebar__item .zx-app-sidebar__item-icon)) {
118
120
  --zx-app-sidebar-icon: 0px;
119
121
  --zx-app-sidebar-item-gap: 0px;
@@ -169,6 +171,43 @@
169
171
 
170
172
  .zx-app-sidebar__list > .zx-app-sidebar__row:first-child { margin-block-start: 0; }
171
173
 
174
+ /*
175
+ * Sections. A heading over rows that stay at its own level: not a branch, never collapsed, never
176
+ * focused. Sections are separated by the group gap, like the groups a branch opens, and the
177
+ * heading is the same quiet sentence-case caption a flyout uses for its title.
178
+ */
179
+ .zx-app-sidebar__section { list-style: none; }
180
+
181
+ .zx-app-sidebar__section + .zx-app-sidebar__section,
182
+ .zx-app-sidebar__section + .zx-app-sidebar__row,
183
+ .zx-app-sidebar__row + .zx-app-sidebar__section {
184
+ margin-block-start: var(--zx-app-sidebar-group-gap);
185
+ }
186
+
187
+ .zx-app-sidebar__section-header {
188
+ display: flex;
189
+ align-items: center;
190
+ justify-content: space-between;
191
+ gap: var(--zx-space-2);
192
+ min-block-size: calc(var(--zx-control-height) - var(--zx-space-1));
193
+ padding-inline: var(--zx-app-sidebar-pad-x) var(--zx-space-1);
194
+ color: var(--zx-color-text-muted);
195
+ font-size: var(--zx-text-xs);
196
+ font-weight: var(--zx-weight-semibold);
197
+ line-height: 1.3;
198
+ }
199
+
200
+ .zx-app-sidebar__section-label { min-inline-size: 0; overflow-wrap: break-word; }
201
+ .zx-app-sidebar__section-actions { display: inline-flex; flex: none; align-items: center; gap: var(--zx-space-1); }
202
+
203
+ .zx-app-sidebar__section-list {
204
+ display: grid;
205
+ gap: var(--zx-app-sidebar-row-gap);
206
+ margin: 0;
207
+ padding: 0;
208
+ list-style: none;
209
+ }
210
+
172
211
  .zx-app-sidebar__item {
173
212
  position: relative;
174
213
  display: grid;
@@ -294,7 +333,165 @@
294
333
  }
295
334
  .zx-app-sidebar__footer[hidden] { display: none; }
296
335
 
297
- .zx-app-sidebar__rail-header { display: grid; gap: var(--zx-space-1); }
336
+ .zx-app-sidebar__rail-header { display: grid; gap: var(--zx-space-1); justify-items: center; }
337
+
338
+ /*
339
+ * The modules layout: the module strip beside a panel of one module's menu. Both share the
340
+ * sidebar's chrome surface; the subtle rule between them is the one line tone cannot draw, since
341
+ * they are the same tone.
342
+ */
343
+ .zx-app-sidebar__modules {
344
+ display: flex;
345
+ min-inline-size: 0;
346
+ min-block-size: 0;
347
+ }
348
+
349
+ .zx-app-sidebar[data-side="right"] .zx-app-sidebar__modules { flex-direction: row-reverse; }
350
+
351
+ .zx-app-sidebar__strip.zx-module-bar { background: transparent; }
352
+ .zx-app-sidebar__modules > .zx-app-sidebar__strip { border-inline-end: 1px solid var(--zx-color-border-subtle); }
353
+ .zx-app-sidebar[data-side="right"] .zx-app-sidebar__modules > .zx-app-sidebar__strip {
354
+ border-inline-end: 0;
355
+ border-inline-start: 1px solid var(--zx-color-border-subtle);
356
+ }
357
+
358
+ .zx-app-sidebar[data-layout="modules"][data-orientation="horizontal"] > .zx-app-sidebar__strip { inline-size: 100%; }
359
+
360
+ /* The foot of the strip: the one expand/collapse control, then what the application put there.
361
+ With names beside the tiles it lines up with their tiles rather than sitting centred. */
362
+ .zx-app-sidebar__strip-foot {
363
+ display: grid;
364
+ justify-items: center;
365
+ gap: var(--zx-space-2);
366
+ }
367
+
368
+ .zx-app-sidebar__strip[data-labels="beside"] .zx-app-sidebar__strip-foot {
369
+ justify-items: stretch;
370
+ /* The toggle's centre on the tiles' centre: list inset + row inset, less the footer's own pad. */
371
+ padding-inline-start: calc(var(--zx-space-1) + var(--zx-space-2) - var(--zx-module-bar-pad)
372
+ + (var(--zx-module-bar-tile) - var(--zx-control-height)) / 2);
373
+ }
374
+
375
+ /* With names printed the strip's head spans the column, like the rows under it. */
376
+ .zx-app-sidebar__strip[data-labels="beside"] .zx-app-sidebar__rail-header { justify-items: stretch; }
377
+
378
+ .zx-app-sidebar__presentation {
379
+ display: inline-grid;
380
+ justify-self: center;
381
+ gap: 2px;
382
+ border-radius: var(--zx-radius-md);
383
+ background: var(--zx-color-bg-hover);
384
+ padding: 2px;
385
+ }
386
+
387
+ /* Named strip: the three options in a row at the start of the foot; icon strip: a column. */
388
+ .zx-app-sidebar__strip[data-labels="beside"] .zx-app-sidebar__presentation {
389
+ grid-auto-flow: column;
390
+ justify-self: start;
391
+ }
392
+
393
+ .zx-app-sidebar__presentation-option {
394
+ display: grid;
395
+ place-items: center;
396
+ inline-size: var(--zx-space-6);
397
+ block-size: var(--zx-space-6);
398
+ border: 0;
399
+ border-radius: var(--zx-radius-sm);
400
+ background: transparent;
401
+ padding: 0;
402
+ color: var(--zx-color-text-muted);
403
+ cursor: pointer;
404
+ }
405
+
406
+ .zx-app-sidebar__presentation-option:hover { color: var(--zx-color-text); }
407
+
408
+ /* The chosen presentation is where you are: the surface lifted out of the track, full text colour. */
409
+ .zx-app-sidebar__presentation-option[aria-checked="true"] {
410
+ background: var(--zx-color-bg-surface);
411
+ box-shadow: 0 0 0 1px var(--zx-color-border-subtle);
412
+ color: var(--zx-color-text);
413
+ }
414
+
415
+ .zx-app-sidebar__presentation-option:focus-visible {
416
+ outline-color: transparent;
417
+ box-shadow: var(--zx-focus-ring);
418
+ }
419
+
420
+ /*
421
+ * The pictogram: a window with its navigation drawn in it — a module strip, wide when it is named,
422
+ * and a side menu when it shows. Drawn from boxes in the text colour, so it needs no icon set and
423
+ * follows the theme, forced colours included.
424
+ */
425
+ .zx-app-sidebar__presentation-glyph {
426
+ display: flex;
427
+ gap: 1px;
428
+ box-sizing: border-box;
429
+ inline-size: 16px;
430
+ block-size: 12px;
431
+ border: 1.5px solid currentColor;
432
+ border-radius: 2px;
433
+ padding: 1px;
434
+ }
435
+
436
+ .zx-app-sidebar__presentation-strip,
437
+ .zx-app-sidebar__presentation-panel {
438
+ border-radius: 1px;
439
+ background: currentColor;
440
+ }
441
+
442
+ .zx-app-sidebar__presentation-strip { flex: 0 0 2px; }
443
+ .zx-app-sidebar__presentation-panel { flex: 0 0 4px; opacity: .5; }
444
+ .zx-app-sidebar__presentation-glyph[data-presentation="expanded"] > .zx-app-sidebar__presentation-strip { flex-basis: 4px; }
445
+ .zx-app-sidebar__presentation-glyph[data-presentation="collapsed"] > .zx-app-sidebar__presentation-panel { display: none; }
446
+
447
+ @media (forced-colors: active) {
448
+ .zx-app-sidebar__presentation-option[aria-checked="true"] {
449
+ background: Highlight;
450
+ color: HighlightText;
451
+ forced-color-adjust: none;
452
+ }
453
+ }
454
+
455
+ .zx-app-sidebar__panel {
456
+ display: grid;
457
+ grid-template-rows: auto minmax(0, 1fr) auto;
458
+ inline-size: min(14rem, 100vw - var(--zx-control-height) * 3);
459
+ min-block-size: 0;
460
+ }
461
+
462
+ .zx-app-sidebar__panel-title {
463
+ color: var(--zx-color-text);
464
+ font-size: var(--zx-text-md);
465
+ font-weight: var(--zx-weight-semibold);
466
+ line-height: 1.3;
467
+ overflow-wrap: break-word;
468
+ }
469
+
470
+ /* One scroller for the module's lead content, its menu, and its own content, so a long menu and
471
+ a long list under it read as one column. The nav loses its own scroll box inside it. */
472
+ .zx-app-sidebar__panel-body {
473
+ display: grid;
474
+ /* Rows take their content's height and the body scrolls; a row the grid could shrink would
475
+ draw the menu under the content beneath it. */
476
+ grid-auto-rows: max-content;
477
+ align-content: start;
478
+ min-block-size: 0;
479
+ overflow: auto;
480
+ }
481
+
482
+ .zx-app-sidebar__panel-body > .zx-app-sidebar__nav { overflow: visible; min-block-size: auto; }
483
+ .zx-app-sidebar__panel-body > [hidden] { display: none; }
484
+
485
+ .zx-app-sidebar__panel-lead {
486
+ display: grid;
487
+ gap: var(--zx-space-2);
488
+ padding: var(--zx-space-1) var(--zx-space-3) 0;
489
+ }
490
+
491
+ .zx-app-sidebar__panel-content {
492
+ min-inline-size: 0;
493
+ padding: 0 var(--zx-app-sidebar-gutter) var(--zx-space-3);
494
+ }
298
495
 
299
496
  /*
300
497
  * The minimized rail. Only the presentation changes; the same vocabulary applies — the neutral
@@ -379,9 +576,13 @@
379
576
  .zx-app-sidebar__rail .zx-app-rail__item:active { transform: scale(.985); }
380
577
 
381
578
  .zx-app-sidebar__expanded,
382
- .zx-app-sidebar__rail { animation: zx-app-sidebar-mode var(--zx-dur-base) var(--zx-ease); }
579
+ .zx-app-sidebar__rail,
580
+ .zx-app-sidebar__panel { animation: zx-app-sidebar-mode var(--zx-dur-base) var(--zx-ease); }
383
581
 
384
582
  .zx-app-sidebar__sublist[data-branch-motion="expand"] { animation: zx-app-sidebar-branch var(--zx-dur-base) var(--zx-ease); }
583
+
584
+ /* The panel's content cross-fades when it switches module; the width never animates. */
585
+ .zx-app-sidebar__panel-body[data-module-motion="enter"] { animation: zx-app-sidebar-mode var(--zx-dur-base) var(--zx-ease); }
385
586
  }
386
587
 
387
588
  @keyframes zx-app-sidebar-mode {