@zeyos/zx 3.0.0 → 4.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 (88) hide show
  1. package/CHANGELOG.md +51 -0
  2. package/DESIGN-SYSTEM.md +28 -68
  3. package/README.md +16 -37
  4. package/dist/types/components/app-sidebar/app-sidebar.d.ts +106 -32
  5. package/dist/types/components/card/card.d.ts +128 -0
  6. package/dist/types/components/chart/chart.d.ts +1 -0
  7. package/dist/types/components/launcher/launcher.d.ts +65 -6
  8. package/dist/types/components/select/select.d.ts +51 -0
  9. package/dist/types/components/table/table.d.ts +4 -3
  10. package/dist/types/core/dom.d.ts +8 -0
  11. package/dist/types/index.d.ts +2 -1
  12. package/dist/types/{components/app-rail → internal}/app-rail.d.ts +34 -19
  13. package/dist/types/zeyos/icons.d.ts +5 -0
  14. package/dist/types/zeyos/index.d.ts +2 -1
  15. package/dist/types/zeyos/launcher.d.ts +147 -0
  16. package/dist/types/zeyos/select.d.ts +39 -0
  17. package/dist/zx-zeyos.esm.js +1324 -46
  18. package/dist/zx-zeyos.esm.min.js +1 -1
  19. package/dist/zx.css +400 -12
  20. package/dist/zx.esm.js +1059 -189
  21. package/dist/zx.esm.min.js +3 -3
  22. package/dist/zx.global.js +1059 -189
  23. package/dist/zx.global.min.js +3 -3
  24. package/dist/zx.min.css +1 -1
  25. package/docs/api.json +258 -145
  26. package/docs/llms.md +93 -112
  27. package/docs/llms.txt +16 -15
  28. package/package.json +8 -17
  29. package/src/components/app-sidebar/app-sidebar.css +12 -0
  30. package/src/components/app-sidebar/app-sidebar.js +310 -110
  31. package/src/components/card/card.css +149 -0
  32. package/src/components/card/card.js +285 -0
  33. package/src/components/chart/chart.js +8 -3
  34. package/src/components/grid/grid.js +10 -3
  35. package/src/components/launcher/launcher.css +113 -3
  36. package/src/components/launcher/launcher.js +234 -26
  37. package/src/components/select/field-adapter.js +9 -4
  38. package/src/components/select/select.css +144 -10
  39. package/src/components/select/select.js +259 -27
  40. package/src/components/table/table.js +8 -7
  41. package/src/core/dom.js +20 -0
  42. package/src/index.js +2 -1
  43. package/src/{components/app-rail → internal}/app-rail.js +80 -30
  44. package/src/zeyos/icons.js +8 -3
  45. package/src/zeyos/index.js +7 -1
  46. package/src/zeyos/launcher.js +338 -0
  47. package/src/zeyos/modules.js +1 -0
  48. package/src/zeyos/select.js +115 -1
  49. package/styles/tokens/global.css +1 -1
  50. package/styles/tokens/modules.css +1 -0
  51. package/styles/zx.css +1 -0
  52. package/MIGRATION.md +0 -749
  53. package/dist/types/compat/base.d.ts +0 -156
  54. package/dist/types/compat/globals.d.ts +0 -20
  55. package/dist/types/compat/index.d.ts +0 -128
  56. package/dist/types/compat/map/basic.d.ts +0 -175
  57. package/dist/types/compat/map/data.d.ts +0 -103
  58. package/dist/types/compat/map/date-time.d.ts +0 -80
  59. package/dist/types/compat/map/factory.d.ts +0 -9
  60. package/dist/types/compat/map/form.d.ts +0 -98
  61. package/dist/types/compat/map/helpers.d.ts +0 -12
  62. package/dist/types/compat/map/http.d.ts +0 -36
  63. package/dist/types/compat/map/layout.d.ts +0 -106
  64. package/dist/types/compat/map/options.d.ts +0 -80
  65. package/dist/types/compat/map/select.d.ts +0 -95
  66. package/dist/types/compat/map/stubs.d.ts +0 -32
  67. package/dist/types/compat/map/utilities.d.ts +0 -37
  68. package/dist/types/compat-entry.d.ts +0 -5
  69. package/dist/zx-compat.esm.js +0 -17848
  70. package/dist/zx-compat.esm.min.js +0 -1
  71. package/dist/zx-compat.global.js +0 -29008
  72. package/dist/zx-compat.global.min.js +0 -3
  73. package/src/compat/base.js +0 -291
  74. package/src/compat/globals.js +0 -167
  75. package/src/compat/index.js +0 -84
  76. package/src/compat/map/basic.js +0 -464
  77. package/src/compat/map/data.js +0 -254
  78. package/src/compat/map/date-time.js +0 -177
  79. package/src/compat/map/factory.js +0 -45
  80. package/src/compat/map/form.js +0 -330
  81. package/src/compat/map/helpers.js +0 -46
  82. package/src/compat/map/http.js +0 -117
  83. package/src/compat/map/layout.js +0 -256
  84. package/src/compat/map/options.js +0 -267
  85. package/src/compat/map/select.js +0 -198
  86. package/src/compat/map/stubs.js +0 -26
  87. package/src/compat/map/utilities.js +0 -113
  88. package/src/compat-entry.js +0 -11
@@ -0,0 +1,149 @@
1
+ .zx-card {
2
+ --zx-card-media-size: 8rem;
3
+ position: relative;
4
+ inline-size: 100%;
5
+ container-type: inline-size;
6
+ overflow: hidden;
7
+ border-radius: var(--zx-radius-lg);
8
+ color: var(--zx-color-text);
9
+ }
10
+
11
+ .zx-card[data-variant="outlined"] {
12
+ border: 1px solid var(--zx-color-border);
13
+ background: var(--zx-color-bg-surface);
14
+ }
15
+
16
+ .zx-card[data-variant="raised"] {
17
+ border: 1px solid transparent;
18
+ background: var(--zx-color-bg-surface);
19
+ box-shadow: var(--zx-overlay-shadow);
20
+ }
21
+
22
+ .zx-card[data-variant="filled"] {
23
+ border: 1px solid transparent;
24
+ background: var(--zx-color-bg-muted);
25
+ }
26
+
27
+ .zx-card[data-linked="true"]:hover {
28
+ background: var(--zx-color-bg-hover);
29
+ }
30
+
31
+ .zx-card:has(.zx-card__primary:focus-visible) {
32
+ outline: 0;
33
+ box-shadow: var(--zx-focus-ring);
34
+ }
35
+
36
+ .zx-card__layout,
37
+ .zx-card__inner {
38
+ min-inline-size: 0;
39
+ }
40
+
41
+ .zx-card__inner {
42
+ display: grid;
43
+ gap: var(--zx-space-3);
44
+ padding: var(--zx-space-5);
45
+ }
46
+
47
+ .zx-card[data-orientation="horizontal"] .zx-card__layout {
48
+ display: grid;
49
+ grid-template-columns: var(--zx-card-media-size) minmax(0, 1fr);
50
+ align-items: stretch;
51
+ }
52
+
53
+ .zx-card__media {
54
+ min-inline-size: 0;
55
+ overflow: hidden;
56
+ }
57
+
58
+ .zx-card[data-orientation="horizontal"] .zx-card__media {
59
+ min-block-size: var(--zx-card-media-size);
60
+ }
61
+
62
+ .zx-card__media > :is(img, video, svg) {
63
+ display: block;
64
+ inline-size: 100%;
65
+ block-size: 100%;
66
+ object-fit: cover;
67
+ }
68
+
69
+ .zx-card__header {
70
+ display: flex;
71
+ align-items: start;
72
+ justify-content: space-between;
73
+ gap: var(--zx-space-3);
74
+ }
75
+
76
+ .zx-card__header[hidden],
77
+ .zx-card__media[hidden],
78
+ .zx-card__content[hidden],
79
+ .zx-card__footer[hidden],
80
+ .zx-card__actions[hidden] {
81
+ display: none;
82
+ }
83
+
84
+ .zx-card__heading {
85
+ min-inline-size: 0;
86
+ margin: 0;
87
+ font-size: var(--zx-text-lg);
88
+ font-weight: 700;
89
+ line-height: 1.3;
90
+ letter-spacing: -0.012em;
91
+ }
92
+
93
+ .zx-card__primary,
94
+ .zx-card__title {
95
+ color: inherit;
96
+ }
97
+
98
+ .zx-card__primary {
99
+ text-decoration: none;
100
+ }
101
+
102
+ .zx-card[data-linked="true"] .zx-card__primary::after {
103
+ content: "";
104
+ position: absolute;
105
+ inset: 0;
106
+ z-index: 1;
107
+ }
108
+
109
+ .zx-card__primary:focus-visible {
110
+ outline: 0;
111
+ }
112
+
113
+ .zx-card__actions,
114
+ .zx-card__content :is(a, button, input, select, textarea, summary, [tabindex]:not([tabindex="-1"])),
115
+ .zx-card__footer :is(a, button, input, select, textarea, summary, [tabindex]:not([tabindex="-1"])) {
116
+ position: relative;
117
+ z-index: 2;
118
+ }
119
+
120
+ .zx-card__actions {
121
+ display: flex;
122
+ flex: none;
123
+ flex-wrap: wrap;
124
+ align-items: center;
125
+ justify-content: end;
126
+ gap: var(--zx-space-2);
127
+ }
128
+
129
+ .zx-card__content {
130
+ color: var(--zx-color-text-muted);
131
+ line-height: 1.6;
132
+ }
133
+
134
+ .zx-card__footer {
135
+ padding-block-start: var(--zx-space-3);
136
+ border-block-start: 1px solid var(--zx-color-border);
137
+ color: var(--zx-color-text-muted);
138
+ font-size: var(--zx-text-sm);
139
+ }
140
+
141
+ @container (max-width: 28rem) {
142
+ .zx-card[data-orientation="horizontal"] .zx-card__layout {
143
+ grid-template-columns: minmax(0, 1fr);
144
+ }
145
+
146
+ .zx-card[data-orientation="horizontal"] .zx-card__media {
147
+ min-block-size: 0;
148
+ }
149
+ }
@@ -0,0 +1,285 @@
1
+ import { Component } from '../../core/component.js';
2
+ import { h, restoreTarget, safeHref, snapshotTarget } from '../../core/dom.js';
3
+ import { isElement, uid } from '../../core/util.js';
4
+ import { button } from '../button/button.js';
5
+
6
+ /** @typedef {string|Node|Component|null} CardContent */
7
+ /** @typedef {import('../button/button.js').ButtonOptions} ButtonOptions */
8
+ /** @typedef {Element|ButtonOptions} CardAction */
9
+ /**
10
+ * @typedef {Object} CardLinkDescriptor
11
+ * @property {string} href Native link destination; executable/data schemes are rejected.
12
+ * @property {string} [target] Native browsing context.
13
+ * @property {string} [rel] Native link relationship.
14
+ * @property {(event: MouseEvent) => void} [onclick] Native click listener.
15
+ */
16
+ /** @typedef {string|CardLinkDescriptor|null} CardLink */
17
+
18
+ /**
19
+ * @typedef {Object} CardOptions
20
+ * @property {string} [title=''] Card title.
21
+ * @property {CardContent} [media=null] Optional image, avatar, icon, or other media.
22
+ * @property {CardContent} [content=null] Main card content; enhanced children are adopted by default.
23
+ * @property {CardAction[]} [actions=[]] Secondary native controls beside the title.
24
+ * @property {CardContent} [footer=null] Optional metadata or trailing content.
25
+ * @property {CardLink} [link=null] Optional native primary link rendered around the title.
26
+ * @property {'outlined'|'raised'|'filled'} [variant='outlined'] Surface treatment.
27
+ * @property {'vertical'|'horizontal'} [orientation='vertical'] Media/content arrangement.
28
+ * @property {1|2|3|4|5|6} [headingLevel=3] Semantic title heading level.
29
+ */
30
+
31
+ /**
32
+ * Semantic content or record surface with optional media, native primary link, actions, and footer.
33
+ * The root is never interactive: linked cards keep a real title anchor, so secondary controls are
34
+ * siblings rather than invalid nested actions.
35
+ * @extends {Component<CardOptions>}
36
+ */
37
+ export class Card extends Component {
38
+ static cssName = 'card';
39
+
40
+ /** @type {Readonly<CardOptions>} */
41
+ static defaults = {
42
+ title: '',
43
+ media: null,
44
+ content: null,
45
+ actions: [],
46
+ footer: null,
47
+ link: null,
48
+ variant: 'outlined',
49
+ orientation: 'vertical',
50
+ headingLevel: 3
51
+ };
52
+
53
+ /**
54
+ * Creates or enhances one card.
55
+ * @param {Element|string|null} [target=null] Existing card container, selector, or null.
56
+ * @param {CardOptions} [options={}] Card options.
57
+ */
58
+ constructor(target = null, options = {}) {
59
+ super(target, options);
60
+ }
61
+
62
+ /** @returns {HTMLElement} */
63
+ render() {
64
+ const created = !this.el;
65
+ const root = /** @type {HTMLElement} */ (this.el ?? h('article'));
66
+ this.el = root;
67
+ this._createdRoot = created;
68
+ this._original = created ? null : snapshotTarget(root);
69
+ this._cleaned = false;
70
+ this._title = '';
71
+ this._link = null;
72
+ this._actionAbort = null;
73
+ this._linkAbort = null;
74
+
75
+ const originalContent = created ? [] : Array.from(root.childNodes);
76
+ try {
77
+ const headingLevel = normalizeHeadingLevel(this.options.headingLevel);
78
+ const headingId = uid('zx-card-heading');
79
+ const heading = h(`h${headingLevel}`, {
80
+ ref: 'heading',
81
+ class: 'zx-card__heading',
82
+ id: headingId
83
+ });
84
+ const media = h('div', { ref: 'media', class: 'zx-card__media', hidden: true });
85
+ const actions = h('div', {
86
+ ref: 'actions',
87
+ class: 'zx-card__actions',
88
+ role: 'group',
89
+ ariaLabel: 'Card actions',
90
+ hidden: true
91
+ });
92
+ const content = h('div', { ref: 'content', class: 'zx-card__content', hidden: true });
93
+ const footer = h('footer', { ref: 'footer', class: 'zx-card__footer', hidden: true });
94
+ const header = h('header', { ref: 'header', class: 'zx-card__header' }, heading, actions);
95
+
96
+ root.replaceChildren(h('div', { class: 'zx-card__layout' }, media,
97
+ h('div', { class: 'zx-card__inner' }, header, content, footer)));
98
+ root.dataset.variant = normalizeVariant(this.options.variant);
99
+ root.dataset.orientation = normalizeOrientation(this.options.orientation);
100
+
101
+ this.setTitle(this.options.title);
102
+ this.setMedia(this.options.media);
103
+ if (this.options.content !== null) this.setContent(this.options.content);
104
+ else if (originalContent.length) {
105
+ content.replaceChildren(...originalContent);
106
+ content.hidden = false;
107
+ }
108
+ this.setActions(this.options.actions);
109
+ this.setFooter(this.options.footer);
110
+ this.setLink(this.options.link);
111
+ return root;
112
+ } catch (error) {
113
+ this._actionAbort?.abort();
114
+ this._linkAbort?.abort();
115
+ if (!created) restoreTarget(root, this._original);
116
+ throw error;
117
+ }
118
+ }
119
+
120
+ /** Replaces the card title. @param {string} title Next title. @returns {this} */
121
+ setTitle(title) {
122
+ const next = String(title ?? '');
123
+ if (this._link && !next.trim()) throw new TypeError('A linked Card requires a non-empty title');
124
+ this._title = next;
125
+ this._renderHeading();
126
+ return this;
127
+ }
128
+
129
+ /** Replaces or removes the media region. @param {CardContent} content Media. @returns {this} */
130
+ setMedia(content) {
131
+ replaceCardContent(this.refs.media, content, 'Card media');
132
+ this.refs.media.hidden = content === null;
133
+ return this;
134
+ }
135
+
136
+ /** Replaces or clears the main content. @param {CardContent} content Content. @returns {this} */
137
+ setContent(content) {
138
+ replaceCardContent(this.refs.content, content, 'Card content');
139
+ this.refs.content.hidden = content === null;
140
+ return this;
141
+ }
142
+
143
+ /** Replaces the secondary action controls. @param {CardAction[]} actions Actions. @returns {this} */
144
+ setActions(actions) {
145
+ if (!Array.isArray(actions)) throw new TypeError('Card actions must be an array');
146
+ const nextAbort = new AbortController();
147
+ let controls;
148
+ try {
149
+ controls = actions.map((action) => {
150
+ if (isElement(action)) return action;
151
+ if (!action || typeof action !== 'object' || Array.isArray(action)) {
152
+ throw new TypeError('Card actions must be Elements or button descriptors');
153
+ }
154
+ const descriptor = { ...action };
155
+ const onclick = descriptor.onclick;
156
+ delete descriptor.onclick;
157
+ const control = button(descriptor);
158
+ if (onclick !== undefined && typeof onclick !== 'function') {
159
+ throw new TypeError('Card action onclick must be a function');
160
+ }
161
+ if (onclick) control.addEventListener('click', onclick, { signal: nextAbort.signal });
162
+ return control;
163
+ });
164
+ } catch (error) {
165
+ nextAbort.abort();
166
+ throw error;
167
+ }
168
+ this._actionAbort?.abort();
169
+ this._actionAbort = nextAbort;
170
+ this.refs.actions.replaceChildren(...controls);
171
+ this.refs.actions.hidden = controls.length === 0;
172
+ this._syncHeader();
173
+ return this;
174
+ }
175
+
176
+ /** Replaces or removes the footer. @param {CardContent} content Footer. @returns {this} */
177
+ setFooter(content) {
178
+ replaceCardContent(this.refs.footer, content, 'Card footer');
179
+ this.refs.footer.hidden = content === null;
180
+ return this;
181
+ }
182
+
183
+ /** Sets or removes the title's native primary link. @param {CardLink} link Link. @returns {this} */
184
+ setLink(link) {
185
+ const next = normalizeCardLink(link);
186
+ if (next && !this._title.trim()) throw new TypeError('A linked Card requires a non-empty title');
187
+ this._link = next;
188
+ this.el.dataset.linked = next ? 'true' : 'false';
189
+ this._renderHeading();
190
+ return this;
191
+ }
192
+
193
+ /** Restores an enhanced target exactly, or removes an owned card. @returns {void} */
194
+ destroy() {
195
+ if (this._cleaned) return;
196
+ this._cleaned = true;
197
+ this._actionAbort?.abort();
198
+ this._linkAbort?.abort();
199
+ super.destroy();
200
+ if (!this._createdRoot) restoreTarget(this.el, this._original);
201
+ }
202
+
203
+ /** @returns {void} */
204
+ _renderHeading() {
205
+ const title = this._title;
206
+ const nextAbort = new AbortController();
207
+ const node = this._link ? h('a', {
208
+ class: 'zx-card__primary',
209
+ href: this._link.href,
210
+ target: this._link.target,
211
+ rel: this._link.rel
212
+ }, title) : h('span', { class: 'zx-card__title' }, title);
213
+ if (this._link?.onclick) node.addEventListener('click', this._link.onclick, { signal: nextAbort.signal });
214
+ this._linkAbort?.abort();
215
+ this._linkAbort = nextAbort;
216
+ this.refs.heading.replaceChildren(node);
217
+ this.refs.heading.hidden = title === '';
218
+ if (title) this.el.setAttribute('aria-labelledby', this.refs.heading.id);
219
+ else this.el.removeAttribute('aria-labelledby');
220
+ this._syncHeader();
221
+ }
222
+
223
+ /** @returns {void} */
224
+ _syncHeader() {
225
+ if (!this.refs.header) return;
226
+ this.refs.header.hidden = this._title === '' && this.refs.actions.childElementCount === 0;
227
+ }
228
+ }
229
+
230
+ /** @param {Element} host @param {CardContent} content @param {string} label */
231
+ function replaceCardContent(host, content, label) {
232
+ if (content === null) {
233
+ host.replaceChildren();
234
+ return;
235
+ }
236
+ if (typeof content === 'string') {
237
+ host.textContent = content;
238
+ return;
239
+ }
240
+ const node = content instanceof Component ? content.toElement() : content;
241
+ if (node instanceof Node) {
242
+ host.replaceChildren(node);
243
+ return;
244
+ }
245
+ throw new TypeError(`${label} must be a string, Node, Component, or null`);
246
+ }
247
+
248
+ /** @param {CardLink} value @returns {CardLinkDescriptor|null} */
249
+ function normalizeCardLink(value) {
250
+ if (value === null) return null;
251
+ const descriptor = typeof value === 'string' ? { href: value } : value;
252
+ if (!descriptor || typeof descriptor !== 'object' || Array.isArray(descriptor)) {
253
+ throw new TypeError('Card link must be a URL string, descriptor, or null');
254
+ }
255
+ const href = safeHref(descriptor.href);
256
+ if (href === null) {
257
+ throw new TypeError('Card link requires a safe non-empty href');
258
+ }
259
+ if (descriptor.onclick !== undefined && typeof descriptor.onclick !== 'function') {
260
+ throw new TypeError('Card link onclick must be a function');
261
+ }
262
+ const target = descriptor.target == null ? undefined : String(descriptor.target);
263
+ let rel = descriptor.rel == null ? '' : String(descriptor.rel).trim();
264
+ if (target === '_blank' && !/(^|\s)noopener(\s|$)/i.test(rel)) {
265
+ rel = `${rel} noopener`.trim();
266
+ }
267
+ return { href, target, rel: rel || undefined, onclick: descriptor.onclick };
268
+ }
269
+
270
+ /** @param {unknown} value @returns {'outlined'|'raised'|'filled'} */
271
+ function normalizeVariant(value) {
272
+ return ['raised', 'filled'].includes(String(value))
273
+ ? /** @type {'raised'|'filled'} */ (value) : 'outlined';
274
+ }
275
+
276
+ /** @param {unknown} value @returns {'vertical'|'horizontal'} */
277
+ function normalizeOrientation(value) {
278
+ return value === 'horizontal' ? 'horizontal' : 'vertical';
279
+ }
280
+
281
+ /** @param {unknown} value @returns {1|2|3|4|5|6} */
282
+ function normalizeHeadingLevel(value) {
283
+ const level = Math.trunc(Number(value));
284
+ return /** @type {1|2|3|4|5|6} */ (level >= 1 && level <= 6 ? level : 3);
285
+ }
@@ -106,6 +106,7 @@ export class Chart extends Component {
106
106
  this._chartOptions = cloneChartValue(this.options.chartOptions);
107
107
  this._loading = Boolean(this.options.loading);
108
108
  this._error = null;
109
+ this._hasError = false;
109
110
  this._handle = null;
110
111
  this._destroyed = false;
111
112
 
@@ -151,6 +152,7 @@ export class Chart extends Component {
151
152
  setData(data) {
152
153
  this._data = normalizeChartData(data);
153
154
  this._error = null;
155
+ this._hasError = false;
154
156
  this._loading = false;
155
157
  this._renderSummary();
156
158
  this._mountOrUpdate(true);
@@ -191,6 +193,7 @@ export class Chart extends Component {
191
193
  /** Shows an error and destroys the current engine. @param {unknown} error @returns {this} */
192
194
  setError(error) {
193
195
  this._error = error ?? new Error(String(this.options.errorText));
196
+ this._hasError = true;
194
197
  this._loading = false;
195
198
  this._destroyHandle();
196
199
  this._syncState();
@@ -201,6 +204,7 @@ export class Chart extends Component {
201
204
  /** Clears the error and remounts when data and an adapter are available. @returns {this} */
202
205
  clearError() {
203
206
  this._error = null;
207
+ this._hasError = false;
204
208
  this._mountOrUpdate(false);
205
209
  return this;
206
210
  }
@@ -240,7 +244,7 @@ export class Chart extends Component {
240
244
  /** @param {boolean} emitUpdate @returns {void} */
241
245
  _mountOrUpdate(emitUpdate) {
242
246
  this._syncState();
243
- if (this._loading || this._error || isEmptyChartData(this._data)) {
247
+ if (this._loading || this._hasError || isEmptyChartData(this._data)) {
244
248
  this._destroyHandle();
245
249
  return;
246
250
  }
@@ -288,7 +292,7 @@ export class Chart extends Component {
288
292
  /** @returns {void} */
289
293
  _syncState() {
290
294
  const empty = isEmptyChartData(this._data);
291
- const state = this._loading ? 'loading' : this._error ? 'error' : empty ? 'empty' : 'ready';
295
+ const state = this._loading ? 'loading' : this._hasError ? 'error' : empty ? 'empty' : 'ready';
292
296
  this.el.dataset.state = state;
293
297
  this.refs.viewport.hidden = state !== 'ready';
294
298
  let text = '';
@@ -509,7 +513,8 @@ function themeChartOptions(root) {
509
513
 
510
514
  /** @param {unknown} error @param {unknown} fallback @returns {string} */
511
515
  function errorMessage(error, fallback) {
512
- return error && typeof error === 'object' && 'message' in error ? String(error.message) : String(fallback);
516
+ const message = error && typeof error === 'object' && 'message' in error ? String(error.message) : '';
517
+ return message.trim() ? message : String(fallback);
513
518
  }
514
519
 
515
520
  /** Fired after the adapter creates an engine instance. @event Chart#ready @type {CustomEvent<{instance: unknown}>} */
@@ -85,9 +85,9 @@ export function billingItemsConfig(options = {}) {
85
85
  } = options;
86
86
  const fields = { ...DEFAULT_FIELDS, ...requestedFields };
87
87
  const calculateLineTotal = typeof lineTotal === 'function' ? lineTotal : (row) => {
88
- const quantity = Number(row?.[fields.quantity]);
89
- const price = Number(row?.[fields.unitPrice]);
90
- return Number.isFinite(quantity) && Number.isFinite(price) ? quantity * price : null;
88
+ const quantity = billingNumber(row?.[fields.quantity]);
89
+ const price = billingNumber(row?.[fields.unitPrice]);
90
+ return quantity !== null && price !== null ? quantity * price : null;
91
91
  };
92
92
  const columns = Array.isArray(suppliedColumns) ? suppliedColumns.map((column) => ({ ...column }))
93
93
  : billingColumns(fields, { units, currencies, currency, locale, decimals, columnOverrides });
@@ -211,3 +211,10 @@ export function billingColumns(fields, { units, currencies, currency, locale, de
211
211
  function hasChoices(value) {
212
212
  return Array.isArray(value) ? value.length > 0 : Boolean(value && typeof value === 'object' && Object.keys(value).length);
213
213
  }
214
+
215
+ /** Missing billing operands stay missing; explicit numeric zero remains a valid value. @param {unknown} value @returns {number|null} */
216
+ function billingNumber(value) {
217
+ if (value == null || typeof value === 'string' && value.trim() === '') return null;
218
+ const number = Number(value);
219
+ return Number.isFinite(number) ? number : null;
220
+ }
@@ -1,5 +1,5 @@
1
1
  .zx-launcher {
2
- inline-size: min(680px, calc(100vw - (2 * var(--zx-space-4))));
2
+ inline-size: min(760px, calc(100vw - (2 * var(--zx-space-4))));
3
3
  max-block-size: min(720px, calc(100vh - (2 * var(--zx-space-6))));
4
4
  border: 1px solid var(--zx-color-border);
5
5
  border-radius: var(--zx-radius-xl);
@@ -15,19 +15,23 @@
15
15
 
16
16
  .zx-launcher__surface {
17
17
  display: grid;
18
- grid-template-rows: auto auto minmax(0, 1fr);
18
+ grid-template-rows: auto auto minmax(0, 1fr) auto;
19
19
  max-block-size: inherit;
20
20
  }
21
21
 
22
22
  .zx-launcher__search {
23
23
  display: grid;
24
- grid-template-columns: auto 1fr auto;
24
+ grid-template-columns: auto minmax(0, 1fr) auto auto;
25
25
  align-items: center;
26
26
  gap: var(--zx-space-3);
27
27
  border-block-end: 1px solid var(--zx-color-border);
28
28
  padding: var(--zx-space-3) var(--zx-space-4);
29
29
  }
30
30
 
31
+ .zx-launcher__search[data-shortcut="false"] {
32
+ grid-template-columns: auto minmax(0, 1fr) auto;
33
+ }
34
+
31
35
  .zx-launcher__input {
32
36
  min-inline-size: 0;
33
37
  border: 0;
@@ -43,6 +47,23 @@
43
47
 
44
48
  .zx-launcher__input::-webkit-search-cancel-button { display: none; }
45
49
 
50
+ .zx-launcher__shortcut,
51
+ .zx-launcher__hints kbd {
52
+ border: 1px solid var(--zx-color-border-control);
53
+ border-radius: var(--zx-radius-sm);
54
+ background: var(--zx-color-bg-muted);
55
+ color: var(--zx-color-text-muted);
56
+ font-family: var(--zx-font-mono);
57
+ font-size: var(--zx-text-xs);
58
+ font-weight: 600;
59
+ line-height: 1.5;
60
+ }
61
+
62
+ .zx-launcher__shortcut {
63
+ padding-inline: var(--zx-space-2);
64
+ white-space: nowrap;
65
+ }
66
+
46
67
  .zx-launcher__status {
47
68
  margin: 0;
48
69
  padding: var(--zx-space-5);
@@ -67,6 +88,18 @@
67
88
  text-transform: uppercase;
68
89
  }
69
90
 
91
+ .zx-launcher__group[data-layout="grid"] {
92
+ display: grid;
93
+ grid-template-columns: repeat(auto-fill, minmax(112px, 1fr));
94
+ gap: var(--zx-space-2);
95
+ padding: 0 var(--zx-space-1) var(--zx-space-2);
96
+ }
97
+
98
+ .zx-launcher__group[data-layout="grid"] .zx-launcher__group-label {
99
+ grid-column: 1 / -1;
100
+ padding-inline: var(--zx-space-2);
101
+ }
102
+
70
103
  .zx-launcher__option {
71
104
  display: flex;
72
105
  align-items: center;
@@ -87,6 +120,42 @@
87
120
  background: var(--zx-color-bg-hover);
88
121
  }
89
122
 
123
+ .zx-launcher__option[aria-current="page"] {
124
+ box-shadow: inset 2px 0 var(--zx-color-accent);
125
+ }
126
+
127
+ .zx-launcher__option[data-kind="application"] {
128
+ display: grid;
129
+ grid-template-rows: auto minmax(0, auto);
130
+ justify-items: center;
131
+ align-content: center;
132
+ min-block-size: 92px;
133
+ padding: var(--zx-space-3) var(--zx-space-2);
134
+ text-align: center;
135
+ }
136
+
137
+ .zx-launcher__option[data-kind="application"][aria-current="page"] {
138
+ box-shadow: inset 0 -2px var(--zx-color-accent);
139
+ }
140
+
141
+ .zx-launcher__option[data-kind="application"] .zx-launcher__icon {
142
+ min-block-size: 36px;
143
+ align-items: center;
144
+ }
145
+
146
+ .zx-launcher__option[data-kind="application"] .zx-launcher__copy {
147
+ inline-size: 100%;
148
+ }
149
+
150
+ .zx-launcher__option[data-kind="application"] .zx-launcher__description,
151
+ .zx-launcher__option[data-kind="application"] .zx-launcher__badge {
152
+ display: none;
153
+ }
154
+
155
+ .zx-launcher__option[data-kind="application"] .zx-launcher__label {
156
+ font-size: var(--zx-text-sm);
157
+ }
158
+
90
159
  .zx-launcher__option[aria-disabled="true"] {
91
160
  color: var(--zx-color-text-muted);
92
161
  cursor: not-allowed;
@@ -98,3 +167,44 @@
98
167
  .zx-launcher__label { overflow: hidden; font-weight: 600; text-overflow: ellipsis; white-space: nowrap; }
99
168
  .zx-launcher__description { overflow: hidden; color: var(--zx-color-text-muted); font-size: var(--zx-text-sm); text-overflow: ellipsis; white-space: nowrap; }
100
169
  .zx-launcher__badge { margin-inline-start: auto; color: var(--zx-color-text-muted); font-size: var(--zx-text-xs); }
170
+
171
+ .zx-launcher__hints {
172
+ display: flex;
173
+ flex-wrap: wrap;
174
+ justify-content: end;
175
+ gap: var(--zx-space-4);
176
+ padding: var(--zx-space-2) var(--zx-space-4);
177
+ border-block-start: 1px solid var(--zx-color-border);
178
+ color: var(--zx-color-text-muted);
179
+ font-size: var(--zx-text-xs);
180
+ }
181
+
182
+ .zx-launcher__hints span {
183
+ display: inline-flex;
184
+ align-items: center;
185
+ gap: var(--zx-space-1);
186
+ }
187
+
188
+ .zx-launcher__hints kbd {
189
+ min-inline-size: 1.5rem;
190
+ padding-inline: 4px;
191
+ text-align: center;
192
+ }
193
+
194
+ @media (max-width: 520px) {
195
+ .zx-launcher__group[data-layout="grid"] {
196
+ grid-template-columns: repeat(3, minmax(0, 1fr));
197
+ }
198
+
199
+ .zx-launcher__shortcut {
200
+ display: none;
201
+ }
202
+
203
+ .zx-launcher__search {
204
+ grid-template-columns: auto minmax(0, 1fr) auto;
205
+ }
206
+
207
+ .zx-launcher__hints {
208
+ justify-content: center;
209
+ }
210
+ }