@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,15 +2,18 @@
2
2
  * Four tab appearances, all square-cornered by default:
3
3
  *
4
4
  * divided (default) flat blocks on a muted track, split by hairline dividers, the active
5
- * block lifted onto the panel surface under an accent rule — the workhorse for a
6
- * record page or a panel that owns its content box.
5
+ * block lifted onto the panel surface it opens — the workhorse for a record page or
6
+ * a panel that owns its content box.
7
7
  * bracket folder tabs that outline and fuse into the panel below, the active tab bridging
8
8
  * the seam — for document-like views where the tabs belong to the sheet.
9
- * segmented a compact group on a muted fill, the active tab raised onto a control surface —
10
- * for switching a view inside a toolbar or card header, where the row reads as one
11
- * control rather than as navigation.
12
- * line a full-width rule with the active tab underlined — for page- and section-level
13
- * navigation, where the row spans the content it switches.
9
+ * segmented a compact framed group, the active tab on the neutral current fill — for switching
10
+ * a view inside a toolbar or card header, where the row reads as one control rather
11
+ * than as navigation.
12
+ * line a full-width rule with the active tab underlined in the text colour — for page- and
13
+ * section-level navigation, where the row spans the content it switches.
14
+ *
15
+ * The active tab is where you are, not something you picked, so no variant marks it with the
16
+ * accent: it takes the text colour and a heavier label, and its own variant's neutral ground.
14
17
  *
15
18
  * `divided` and `bracket` draw the panel box themselves; `line` and `segmented` leave the panel
16
19
  * bare so the surrounding layout supplies the chrome.
@@ -57,7 +60,7 @@
57
60
  color: var(--zx-color-text-muted);
58
61
  font: inherit;
59
62
  font-size: var(--zx-control-font-size);
60
- font-weight: 500;
63
+ font-weight: var(--zx-weight-medium);
61
64
  white-space: nowrap;
62
65
  cursor: pointer;
63
66
  }
@@ -68,6 +71,7 @@
68
71
 
69
72
  .zx-tabbox__tab[aria-selected="true"] {
70
73
  color: var(--zx-color-text);
74
+ font-weight: var(--zx-weight-semibold);
71
75
  }
72
76
 
73
77
  .zx-tabbox__tab[aria-disabled="true"] {
@@ -90,8 +94,6 @@
90
94
 
91
95
  .zx-tabbox[data-variant="divided"] .zx-tabbox__tab {
92
96
  block-size: calc(var(--zx-control-height) + 8px);
93
- /* The top rule is the selected marker; transparent here keeps every block the same height. */
94
- border-block-start: 2px solid transparent;
95
97
  border-inline-end: 1px solid var(--zx-color-border);
96
98
  padding-inline: var(--zx-space-4);
97
99
  }
@@ -104,10 +106,9 @@
104
106
  background: var(--zx-color-bg-hover);
105
107
  }
106
108
 
109
+ /* The active block is lifted onto the panel surface it opens; nothing is docked to its edge. */
107
110
  .zx-tabbox[data-variant="divided"] .zx-tabbox__tab[aria-selected="true"] {
108
- border-block-start-color: var(--zx-color-accent);
109
111
  background: var(--zx-color-bg-surface);
110
- font-weight: 650;
111
112
  }
112
113
 
113
114
  .zx-tabbox[data-variant="divided"] .zx-tabbox__tab:focus-visible {
@@ -141,7 +142,6 @@
141
142
  .zx-tabbox[data-variant="bracket"] .zx-tabbox__tab[aria-selected="true"] {
142
143
  border-color: var(--zx-color-border);
143
144
  background: var(--zx-color-bg-surface);
144
- font-weight: 650;
145
145
  }
146
146
 
147
147
  .zx-tabbox[data-variant="bracket"] .zx-tabbox__tab[aria-selected="true"]::after {
@@ -175,9 +175,10 @@
175
175
  border-block-end-color: var(--zx-color-border-strong);
176
176
  }
177
177
 
178
+ /* The underline is the text colour, not the accent: a tab row is navigation, and the screen's
179
+ one solid accent belongs to its primary action. */
178
180
  .zx-tabbox[data-variant="line"] .zx-tabbox__tab[aria-selected="true"] {
179
- border-block-end-color: var(--zx-color-accent);
180
- font-weight: 600;
181
+ border-block-end-color: var(--zx-color-text);
181
182
  }
182
183
 
183
184
  .zx-tabbox[data-variant="line"] .zx-tabbox__tab:focus-visible {
@@ -188,22 +189,22 @@
188
189
 
189
190
  /* ------------------------------------------------------------- segmented variant -- */
190
191
 
192
+ /* One framed control: a hairline round the group, the segments flat inside it, and the active one
193
+ on the neutral current fill — the same recipe as the view switch in ListToolbar and Calendar. */
191
194
  .zx-tabbox[data-variant="segmented"] .zx-tabbox__tablist {
192
195
  inline-size: fit-content;
193
196
  max-inline-size: 100%;
194
197
  align-items: center;
195
198
  gap: 2px;
196
199
  block-size: var(--zx-control-height);
200
+ border: 1px solid var(--zx-color-border-control);
197
201
  border-radius: var(--zx-tabbox-radius);
198
- background: var(--zx-color-bg-muted);
199
- padding: 3px;
202
+ padding: 2px;
200
203
  }
201
204
 
202
205
  .zx-tabbox[data-variant="segmented"] .zx-tabbox__tab {
203
206
  block-size: 100%;
204
- /* A transparent border keeps the active and inactive boxes the same size. */
205
- border: 1px solid transparent;
206
- /* Nested inside the track's 3px padding, so the inner corner is one step tighter. */
207
+ /* Nested inside the frame's 2px padding, so the inner corner is one step tighter. */
207
208
  border-radius: max(0px, calc(var(--zx-tabbox-radius) - 2px));
208
209
  padding-inline: var(--zx-space-3);
209
210
  font-size: var(--zx-text-sm);
@@ -214,15 +215,12 @@
214
215
  }
215
216
 
216
217
  .zx-tabbox[data-variant="segmented"] .zx-tabbox__tab[aria-selected="true"] {
217
- border-color: var(--zx-color-border-control);
218
- background: var(--zx-color-bg-control);
219
- box-shadow: var(--zx-shadow-2);
218
+ background: var(--zx-color-bg-current);
220
219
  }
221
220
 
222
221
  .zx-tabbox[data-variant="segmented"] .zx-tabbox__tab:focus-visible {
223
222
  z-index: 1;
224
223
  outline: 0;
225
- border-color: var(--zx-color-ring);
226
224
  box-shadow: var(--zx-focus-ring);
227
225
  }
228
226
 
@@ -237,18 +235,25 @@
237
235
 
238
236
  /* ---------------------------------------------------------------- badge & close -- */
239
237
 
238
+ /* A count, not a call to action: a neutral overlay that reads on every variant's ground, in the
239
+ muted text colour, taking the full text colour with its tab. */
240
240
  .zx-tabbox__badge {
241
241
  min-inline-size: 1.25rem;
242
242
  border-radius: var(--zx-radius-full);
243
- background: var(--zx-color-accent);
243
+ background: var(--zx-color-bg-current);
244
244
  padding-inline: var(--zx-space-1);
245
- color: var(--zx-color-on-accent);
245
+ color: var(--zx-color-text-muted);
246
246
  font-size: var(--zx-text-xs);
247
- font-weight: 700;
247
+ font-variant-numeric: tabular-nums;
248
+ font-weight: var(--zx-weight-medium);
248
249
  line-height: 1.25rem;
249
250
  text-align: center;
250
251
  }
251
252
 
253
+ .zx-tabbox__tab[aria-selected="true"] .zx-tabbox__badge {
254
+ color: var(--zx-color-text);
255
+ }
256
+
252
257
  /* Chrome comes from the shared .zx-icon-btn ghost utility; only the box is reset here, because
253
258
  a full control-height square would widen every closable tab. The extra .zx-tabbox raises the
254
259
  specificity to match `.zx-scope .zx-icon-btn` in base.css, which sets the size. */
@@ -37,6 +37,9 @@ const VARIANTS = new Set(['divided', 'bracket', 'line', 'segmented']);
37
37
  * the active tab across a full-width rule; `segmented` renders a compact group that reads as one
38
38
  * control, for a toolbar or card header.
39
39
  * @property {boolean} [keepAlive=true] Keep inactive panel elements mounted.
40
+ * @property {string} [label] Accessible name of the tab list — what the tabs switch between
41
+ * ("Record sections"). A tab list needs one whenever the page has more than one, and screen
42
+ * readers announce it on entry.
40
43
  * @property {(event: CustomEvent<{name: string, previous: string|null}>) => void} [onchange] Change callback.
41
44
  * @property {(event: CustomEvent<{name: string}>) => void} [onclose] Close callback, fired when the
42
45
  * user dismisses a closable tab.
@@ -101,7 +104,8 @@ export class Tabbox extends Component {
101
104
  class: 'zx-tabbox__tablist',
102
105
  ref: 'tablist',
103
106
  role: 'tablist',
104
- ariaOrientation: 'horizontal'
107
+ ariaOrientation: 'horizontal',
108
+ ariaLabel: this.options.label || null
105
109
  });
106
110
  const panels = h('div', {
107
111
  class: 'zx-tabbox__panels',
@@ -64,9 +64,9 @@
64
64
  grid-template-columns: minmax(0, 1fr) auto;
65
65
  align-items: center;
66
66
  gap: var(--zx-space-2);
67
- min-block-size: var(--zx-control-height);
67
+ min-block-size: calc(var(--zx-control-height) - 2px);
68
68
  border-radius: var(--zx-radius-sm);
69
- padding-inline: var(--zx-space-2);
69
+ padding: var(--zx-space-1) var(--zx-space-2);
70
70
  }
71
71
 
72
72
  .zx-table__column-option:hover { background: var(--zx-color-overlay-hover); }
@@ -132,16 +132,28 @@
132
132
  }
133
133
  }
134
134
 
135
+ /*
136
+ * The reorder and selection columns are one control height wide, the control centred in the slot.
137
+ * Their cells drop the inline padding: a fixed layout holds the column to exactly that width, and
138
+ * the padding would push the control off its middle. The controls are blocks centred by their
139
+ * margins: inline, they would sit on the text baseline with the descender room left below them, a
140
+ * few pixels above the middle of the row.
141
+ */
135
142
  .zx-table__reorder-column,
136
- .zx-table__reorder-header,
137
- .zx-table__reorder-cell {
143
+ .zx-table__selection-column {
138
144
  inline-size: var(--zx-control-height);
139
- text-align: center;
145
+ }
146
+
147
+ .zx-table :is(.zx-table__reorder-header, .zx-table__reorder-cell,
148
+ .zx-table__selection-header, .zx-table__selection-cell) {
149
+ inline-size: var(--zx-control-height);
150
+ padding-inline: 0;
140
151
  }
141
152
 
142
153
  .zx-table__row-handle {
143
- display: inline-grid;
154
+ display: grid;
144
155
  place-items: center;
156
+ margin-inline: auto;
145
157
  inline-size: 26px;
146
158
  block-size: 26px;
147
159
  min-block-size: 0;
@@ -171,7 +183,9 @@
171
183
  border: 1px solid var(--zx-color-border);
172
184
  border-radius: var(--zx-radius-md);
173
185
  background: var(--zx-color-bg-surface);
174
- scrollbar-gutter: stable;
186
+ /* No reserved gutter: a table that does not scroll would carry a blank strip beside its last
187
+ column wherever scrollbars take up room. One that scrolls gets its scrollbar when it does. */
188
+ scrollbar-gutter: auto;
175
189
  }
176
190
 
177
191
  /* Loading state (Table.setLoading): dim rows and show an indeterminate top progress bar. */
@@ -214,6 +228,8 @@
214
228
  border-spacing: 0;
215
229
  background: var(--zx-color-bg-surface);
216
230
  color: var(--zx-color-text);
231
+ /* Figures line up down a column whatever the cell's alignment. Only digits change width. */
232
+ font-variant-numeric: tabular-nums;
217
233
  }
218
234
 
219
235
  /* Auto layout (intrinsic/px columns): let content set the width and scroll horizontally. */
@@ -227,23 +243,32 @@
227
243
  table-layout: fixed;
228
244
  }
229
245
 
230
- .zx-table__selection-column {
231
- inline-size: var(--zx-control-height);
232
- }
233
-
246
+ /*
247
+ * Rows are separated by the subtle divider, the header from the body by the ordinary one. The
248
+ * header has no band of its own: it sits on the table's surface in the muted text colour at a
249
+ * medium weight, so it labels the columns without competing with the values in them — and stays
250
+ * opaque, which a sticky header has to be.
251
+ */
234
252
  .zx-table th,
235
253
  .zx-table td {
236
- border-block-end: 1px solid var(--zx-table-border-color, var(--zx-color-border));
254
+ border-block-end: 1px solid var(--zx-table-border-color, var(--zx-color-border-subtle));
237
255
  padding: var(--zx-space-2) var(--zx-space-3);
238
256
  text-align: start;
239
257
  vertical-align: middle;
240
258
  }
241
259
 
242
260
  .zx-table th {
243
- background: var(--zx-table-header-bg, var(--zx-color-bg-raised));
261
+ border-block-end-color: var(--zx-table-border-color, var(--zx-color-border));
262
+ background: var(--zx-table-header-bg, var(--zx-color-bg-surface));
244
263
  color: var(--zx-color-text-muted);
245
264
  font-size: var(--zx-text-sm);
246
- font-weight: 650;
265
+ font-weight: var(--zx-weight-medium);
266
+ }
267
+
268
+ /* The column the rows are ordered by says so in its label as well as its arrow. */
269
+ .zx-table th[aria-sort="ascending"],
270
+ .zx-table th[aria-sort="descending"] {
271
+ color: var(--zx-color-text);
247
272
  }
248
273
 
249
274
  .zx-table__scroll[data-sticky-header="true"] th {
@@ -284,9 +309,25 @@
284
309
  background: inherit;
285
310
  color: inherit;
286
311
  padding: 0;
312
+ /* The header's type, not a control's: base.css sets buttons in the control size and weight, and
313
+ the browser resets their tracking, so a sortable label would not match its unsortable
314
+ neighbours. */
315
+ font: inherit;
316
+ letter-spacing: inherit;
287
317
  text-align: inherit;
288
318
  }
289
319
 
320
+ /* A sortable figure column keeps its label over the figures: label at the end edge, the direction
321
+ arrow before it. */
322
+ .zx-table th:is([data-align="end"], [data-align="right"]) .zx-table__sort-button {
323
+ flex-direction: row-reverse;
324
+ justify-content: flex-start;
325
+ }
326
+
327
+ .zx-table th[data-align="center"] .zx-table__sort-button {
328
+ justify-content: center;
329
+ }
330
+
290
331
  .zx-table__sort-button:hover {
291
332
  background: var(--zx-table-row-hover-bg, var(--zx-color-bg-hover));
292
333
  color: var(--zx-color-text);
@@ -298,12 +339,12 @@
298
339
 
299
340
  .zx-table th[aria-sort="ascending"] .zx-table__sort-button::after {
300
341
  content: "↑";
301
- color: var(--zx-color-accent);
342
+ color: var(--zx-color-text-muted);
302
343
  }
303
344
 
304
345
  .zx-table th[aria-sort="descending"] .zx-table__sort-button::after {
305
346
  content: "↓";
306
- color: var(--zx-color-accent);
347
+ color: var(--zx-color-text-muted);
307
348
  }
308
349
 
309
350
  .zx-table__body tr:last-child td {
@@ -314,28 +355,65 @@
314
355
  background: var(--zx-color-bg-page);
315
356
  }
316
357
 
358
+ /* Checked rows of a multi-selection are picks and keep the accent tint. The selected row of a
359
+ single-selection table — no checkbox column — is the record you are on, the current row of a
360
+ master–detail list, and takes the neutral current fill instead. */
317
361
  .zx-table__body tr[aria-selected="true"] {
318
362
  background: var(--zx-table-row-selected-bg, var(--zx-color-bg-selected));
319
363
  }
320
364
 
365
+ .zx-table__body tr[aria-selected="true"]:not(:has(> .zx-table__selection-cell)) {
366
+ background: var(--zx-table-row-selected-bg, var(--zx-color-bg-current));
367
+ }
368
+
321
369
  .zx-table__body tr[data-row]:hover {
322
370
  background: var(--zx-color-bg-hover);
323
371
  }
324
372
 
373
+ /* ------------------------------------------------------------------ plain -- */
374
+
375
+ /*
376
+ * `variant: 'plain'` — for a table inside a card or panel that already draws the box: no outer
377
+ * frame, no header band, no zebra, hairline rows. The cells keep their padding, so the hover and
378
+ * selection fills never run up against the text.
379
+ */
380
+ .zx-table[data-variant="plain"] .zx-table__scroll {
381
+ border: 0;
382
+ border-radius: 0;
383
+ background: transparent;
384
+ }
385
+
386
+ .zx-table[data-variant="plain"] .zx-table__table {
387
+ background: transparent;
388
+ }
389
+
390
+ .zx-table[data-variant="plain"] th {
391
+ background: var(--zx-table-header-bg, var(--zx-color-bg-surface));
392
+ border-block-end-color: var(--zx-table-border-color, var(--zx-color-border-subtle));
393
+ }
394
+
395
+ .zx-table[data-variant="plain"] .zx-table__scroll[data-sticky-header="false"] th {
396
+ background: transparent;
397
+ }
398
+
399
+ .zx-table[data-variant="plain"] .zx-table__scroll[data-zebra="true"] .zx-table__body tr:nth-child(even):not([aria-selected="true"]) {
400
+ background: transparent;
401
+ }
402
+
403
+ .zx-table[data-variant="plain"] .zx-table__body tr:last-child td {
404
+ border-block-end: 0;
405
+ }
406
+
407
+
325
408
  .zx-table__checkbox {
409
+ display: block;
326
410
  inline-size: var(--zx-space-4);
327
411
  min-block-size: auto;
328
412
  block-size: var(--zx-space-4);
329
- margin: 0;
413
+ margin: 0 auto;
330
414
  accent-color: var(--zx-color-accent);
331
415
  }
332
416
 
333
- .zx-table__selection-header,
334
- .zx-table__selection-cell {
335
- inline-size: var(--zx-control-height);
336
- text-align: center;
337
- }
338
-
339
417
  .zx-table__empty {
340
418
  padding-block: var(--zx-space-6);
341
419
  color: var(--zx-color-text-muted);
@@ -583,12 +661,9 @@
583
661
  content: none;
584
662
  }
585
663
 
586
- .zx-table[data-stacked] .zx-table__selection-cell {
587
- justify-content: flex-start;
588
- }
589
-
590
- .zx-table[data-stacked] .zx-table__reorder-cell {
591
- justify-content: flex-start;
664
+ /* A card's checkbox and handle lead their line, where the auto margins would centre them. */
665
+ .zx-table[data-stacked] :is(.zx-table__checkbox, .zx-table__row-handle) {
666
+ margin-inline: 0;
592
667
  }
593
668
 
594
669
 
@@ -15,6 +15,7 @@ import { sortRows } from './sort.js';
15
15
  /** @typedef {'asc'|'desc'} TableSortDirection */
16
16
  /** @typedef {false|'single'|'multi'} TableSelectionMode */
17
17
  /** @typedef {false|'cell'|'row'} TableEditMode */
18
+ /** @typedef {'default'|'plain'} TableVariant */
18
19
  /** @typedef {'text'|'number'|'select'|'date'|'checkbox'|'textarea'|'custom'} TableEditorType */
19
20
  /** @typedef {'text'|'number'|'currency'|'percent'|'unit'} TableColumnType */
20
21
  /** @typedef {{value: unknown, label: string}} TableEditorOption */
@@ -59,7 +60,11 @@ import { sortRows } from './sort.js';
59
60
  /**
60
61
  * @typedef {Object} TableColumn
61
62
  * @property {string} id Row property and stable column identifier.
62
- * @property {string} label Visible header label.
63
+ * @property {string} label Visible header label. An empty label leaves the header blank — an
64
+ * icon or actions column — and the header is then named by `ariaLabel`, or by the id.
65
+ * @property {string} [ariaLabel] Accessible name of the column header, and the name the column
66
+ * chooser and the cell editors use. Needed when `label` is empty or terse (`'#'`); include the
67
+ * visible label's words in it when there is one.
63
68
  * @property {boolean} [sortable=false] Whether the header changes the active sort.
64
69
  * @property {string} [width='auto'] CSS width, `auto`, or an `fr` proportion. `fr` values are
65
70
  * converted into proportional percentages on `<col>` elements and enable `table-layout: fixed`;
@@ -128,6 +133,9 @@ import { sortRows } from './sort.js';
128
133
  * `setSort()` is inert: sorting a grouped table scatters the positions out of their sections, and
129
134
  * doing that silently is worse than not offering it. `sortMode: 'server'` still owns its own order.
130
135
  * @property {boolean} [zebra=true] Whether alternate rows use the zebra background.
136
+ * @property {TableVariant} [variant='default'] Frame treatment. `'default'` draws the table's own
137
+ * bordered box; `'plain'` is for a table inside a card or panel that already draws one — no outer
138
+ * frame, no header band, no zebra, hairline row dividers. Reflected as `data-variant` on the root.
131
139
  * @property {TableEditMode} [editMode=false] Inline editing mode. `'cell'` edits one cell, `'row'`
132
140
  * opens every editable cell of the row together and commits them as a unit. Editing is completely
133
141
  * inert while this is `false`.
@@ -288,6 +296,7 @@ export class Table extends Component {
288
296
  rowKind: null,
289
297
  rowKinds: null,
290
298
  zebra: true,
299
+ variant: 'default',
291
300
  editMode: false,
292
301
  editTrigger: 'double',
293
302
  hierarchy: false,
@@ -361,6 +370,8 @@ export class Table extends Component {
361
370
  }
362
371
  this._reconcileExpanded();
363
372
 
373
+ if (normalizeTableVariant(this.options.variant) === 'plain') root.dataset.variant = 'plain';
374
+
364
375
  this._table = h('table', { class: 'zx-table__table' });
365
376
  if (this._hierarchy) this._table.setAttribute('role', 'treegrid');
366
377
  this._colgroup = h('colgroup', { class: 'zx-table__columns' });
@@ -638,7 +649,7 @@ export class Table extends Component {
638
649
  const column = this._columns.find((candidate) => candidate.id === String(id));
639
650
  const index = this.getColumnOrder().indexOf(String(id));
640
651
  if (column && index >= 0) {
641
- this._columnStatus.textContent = `Moved ${column.label} to position ${index + 1} of ${this._columns.length}.`;
652
+ this._columnStatus.textContent = `Moved ${tableColumnName(column)} to position ${index + 1} of ${this._columns.length}.`;
642
653
  }
643
654
  }
644
655
  return this;
@@ -1303,7 +1314,7 @@ export class Table extends Component {
1303
1314
  class: 'zx-table__column-status', role: 'status', ariaLive: 'polite'
1304
1315
  });
1305
1316
  const details = h('details', { class: 'zx-table__column-controls' },
1306
- h('summary', { class: 'zx-button zx-button--sm' }, icon('fields', { size: 13 }),
1317
+ h('summary', { class: 'zx-btn', dataset: { size: 'sm' } }, icon('fields'),
1307
1318
  this._message('table.columns', 'Columns', [])),
1308
1319
  list);
1309
1320
  this._renderColumnControls();
@@ -1330,21 +1341,23 @@ export class Table extends Component {
1330
1341
  disabled: checked && visible.length === 1,
1331
1342
  dataset: { columnId: column.id, columnAction: 'toggle' }
1332
1343
  }) : null;
1344
+ // The chooser lists every column by name, a blank-headed one included.
1345
+ const name = tableColumnName(column);
1333
1346
  const label = toggle
1334
- ? h('label', { for: controlId }, toggle, h('span', {}, column.label))
1335
- : h('span', { class: 'zx-table__column-label' }, column.label);
1347
+ ? h('label', { for: controlId }, toggle, h('span', {}, name))
1348
+ : h('span', { class: 'zx-table__column-label' }, name);
1336
1349
  const order = this.options.columnReorder ? h('span', {
1337
1350
  class: 'zx-table__column-order', role: 'group',
1338
- ariaLabel: this._message('table.moveColumn', 'Move %1', [column.label])
1351
+ ariaLabel: this._message('table.moveColumn', 'Move %1', [name])
1339
1352
  },
1340
1353
  h('button', {
1341
1354
  class: 'zx-table__column-move', type: 'button', disabled: index === 0,
1342
- ariaLabel: `Move ${column.label} earlier`,
1355
+ ariaLabel: `Move ${name} earlier`,
1343
1356
  dataset: { columnId: column.id, columnAction: 'up' }
1344
1357
  }, icon('chevron-up', { size: 11 })),
1345
1358
  h('button', {
1346
1359
  class: 'zx-table__column-move', type: 'button', disabled: index === this._columns.length - 1,
1347
- ariaLabel: `Move ${column.label} later`,
1360
+ ariaLabel: `Move ${name} later`,
1348
1361
  dataset: { columnId: column.id, columnAction: 'down' }
1349
1362
  }, icon('chevron-down', { size: 11 }))) : null;
1350
1363
  return h('div', { class: 'zx-table__column-option', dataset: { columnId: column.id } }, label, order);
@@ -1602,16 +1615,23 @@ export class Table extends Component {
1602
1615
  // What this header was built from, so `_renderBody()` can tell whether it is still the right one.
1603
1616
  this._headerSpanning = this._spanning;
1604
1617
  for (const column of this._visibleColumns()) {
1605
- const th = h('th', { scope: 'col' });
1618
+ // `data-column` on the header and on every body cell, in every mode, so a read-only table
1619
+ // can be styled per column as readily as an editable one.
1620
+ const th = h('th', { scope: 'col', dataset: { column: column.id } });
1606
1621
  const align = column.align ?? (isNumericColumn(column) ? 'end' : null);
1607
1622
  if (align) th.dataset.align = align;
1608
1623
  if (column.headerTitle) th.title = column.headerTitle;
1624
+ // A blank header (an icon or actions column) still needs a name, or every cell beneath it
1625
+ // is announced with an empty column header.
1626
+ const name = tableHeaderName(column);
1627
+ if (name !== null) th.setAttribute('aria-label', name);
1609
1628
  // Withheld, not disabled: a header that invites a sort the component will refuse is worse
1610
1629
  // than a header that never offered one. The label still reads exactly the same.
1611
1630
  if (column.sortable && !this._spanning) {
1612
1631
  const button = h('button', {
1613
1632
  class: 'zx-table__sort-button',
1614
- type: 'button'
1633
+ type: 'button',
1634
+ ariaLabel: name ?? undefined
1615
1635
  }, h('span', { class: 'zx-table__header-label' }, column.label));
1616
1636
  this.listen(button, 'click', () => {
1617
1637
  const direction = this._sort?.id === column.id && this._sort.dir === 'asc' ? 'desc' : 'asc';
@@ -1769,9 +1789,9 @@ export class Table extends Component {
1769
1789
  // Written unconditionally: `responsive` can be switched on after the rows exist, and a
1770
1790
  // stacked cell has no column header above it to say what its value means.
1771
1791
  cell.dataset.label = column.label;
1792
+ cell.dataset.column = column.id;
1772
1793
  if (column.popin === false) cell.dataset.popin = 'false';
1773
1794
  if (this._editMode !== false) {
1774
- cell.dataset.column = column.id;
1775
1795
  if (resolveEditable(column, row) !== false) {
1776
1796
  cell.dataset.editable = 'true';
1777
1797
  cell.tabIndex = -1;
@@ -1850,7 +1870,9 @@ export class Table extends Component {
1850
1870
  return;
1851
1871
  }
1852
1872
 
1853
- if (isInteractiveCellContent(target)) return;
1873
+ // Only an editable table holds a click on a control inside a cell back from the row: there the
1874
+ // click could otherwise open an editor. A read-only table activates the row as it always has.
1875
+ if (this._editMode !== false && isInteractiveCellContent(target)) return;
1854
1876
 
1855
1877
  if (this._editMode !== false && this._editTrigger === 'single') {
1856
1878
  const cell = target?.closest('td[data-editable="true"]');
@@ -1881,7 +1903,7 @@ export class Table extends Component {
1881
1903
  if (!rowElement || !this._tbody.contains(rowElement)) return;
1882
1904
  const meta = this._rowMeta.get(rowElement);
1883
1905
  if (!meta) return;
1884
- if (isInteractiveCellContent(target)) return;
1906
+ if (this._editMode !== false && isInteractiveCellContent(target)) return;
1885
1907
  if (this._editMode !== false && this._editTrigger === 'double') {
1886
1908
  const cell = target?.closest('td[data-editable="true"]');
1887
1909
  if (cell && this._tbody.contains(cell)) {
@@ -2027,7 +2049,7 @@ export class Table extends Component {
2027
2049
  ...props,
2028
2050
  class: textarea ? 'zx-table__editor-input zx-table__editor-textarea' : 'zx-table__editor-input',
2029
2051
  ...(textarea ? {} : { type: 'text' }),
2030
- ariaLabel: props.ariaLabel ?? `${column.label} row ${index + 1}`,
2052
+ ariaLabel: props.ariaLabel ?? `${tableColumnName(column)} row ${index + 1}`,
2031
2053
  value: formatCellValue(column, value, row)
2032
2054
  }));
2033
2055
  host.append(input);
@@ -2046,8 +2068,10 @@ export class Table extends Component {
2046
2068
  const control = entry?.control;
2047
2069
  if (!control || typeof control.focus !== 'function') return;
2048
2070
  control.focus();
2049
- // Opening a text editor selects its content, so typing replaces the old value.
2050
- if (entry.type === 'text' && typeof control.select === 'function') control.select();
2071
+ // Opening a text, number, or date editor selects its content, so typing replaces the old value
2072
+ // ("6" over "1.00" gives 6, not 1.006). Selected after focus: NumberField rewrites its text on
2073
+ // focus, from the grouped display to a plain re-parseable number.
2074
+ if (editorSelectsOnEntry(entry.type) && typeof control.select === 'function') control.select();
2051
2075
  }
2052
2076
 
2053
2077
  /**
@@ -3008,6 +3032,15 @@ function initialExpanded(option) {
3008
3032
  return new Set(Array.isArray(option) ? option : []);
3009
3033
  }
3010
3034
 
3035
+ /**
3036
+ * Resolves the `variant` option to one the stylesheet knows; anything else is the default frame.
3037
+ * @param {unknown} value Requested variant.
3038
+ * @returns {TableVariant}
3039
+ */
3040
+ export function normalizeTableVariant(value) {
3041
+ return value === 'plain' ? 'plain' : 'default';
3042
+ }
3043
+
3011
3044
  /** @param {unknown} mode @returns {TableEditMode} */
3012
3045
  function normalizeEditMode(mode) {
3013
3046
  if (mode === 'cell' || mode === 'row') return mode;
@@ -3059,6 +3092,47 @@ export function normalizeTableColumns(columns) {
3059
3092
  });
3060
3093
  }
3061
3094
 
3095
+ /**
3096
+ * The name a column goes by wherever it is named rather than shown: its `ariaLabel`, else its
3097
+ * visible label, else its id. The column chooser lists columns by it and cell editors are labelled
3098
+ * with it, so a column with a blank header still has a name in both.
3099
+ * @param {Partial<TableColumn>|null|undefined} column Column descriptor.
3100
+ * @returns {string} Non-empty name, unless the column has no id either.
3101
+ */
3102
+ export function tableColumnName(column) {
3103
+ for (const candidate of [column?.ariaLabel, column?.label]) {
3104
+ const text = candidate == null ? '' : String(candidate).trim();
3105
+ if (text) return text;
3106
+ }
3107
+ return String(column?.id ?? '');
3108
+ }
3109
+
3110
+ /**
3111
+ * The accessible name a column header needs on top of its content, or null when its visible label
3112
+ * already names it. That is the `ariaLabel` when one is given, and the column id when the label is
3113
+ * blank — an icon or actions column — so no header is announced as an empty column.
3114
+ * @param {Partial<TableColumn>|null|undefined} column Column descriptor.
3115
+ * @returns {string|null} Name to set as `aria-label`, or null.
3116
+ */
3117
+ export function tableHeaderName(column) {
3118
+ const label = column?.label == null ? '' : String(column.label).trim();
3119
+ const explicit = column?.ariaLabel == null ? '' : String(column.ariaLabel).trim();
3120
+ if (explicit) return explicit;
3121
+ return label ? null : tableColumnName(column) || null;
3122
+ }
3123
+
3124
+ /**
3125
+ * Whether opening an editor of this type selects its content, so the first keystroke replaces the
3126
+ * value instead of being appended to it. True for the single-line text, number, and date editors;
3127
+ * a textarea keeps its caret, and a select, checkbox, or custom editor keeps its own focus
3128
+ * behaviour.
3129
+ * @param {TableEditorType|string} type Resolved editor type.
3130
+ * @returns {boolean}
3131
+ */
3132
+ export function editorSelectsOnEntry(type) {
3133
+ return type === 'text' || type === 'number' || type === 'date';
3134
+ }
3135
+
3062
3136
  /**
3063
3137
  * Reconciles saved or partial column IDs with every current column.
3064
3138
  * @param {TableColumn[]} columns Current columns.