@zeyos/zx 3.0.0 → 4.1.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 (106) hide show
  1. package/CHANGELOG.md +95 -0
  2. package/DESIGN-SYSTEM.md +87 -71
  3. package/README.md +16 -37
  4. package/dist/types/components/app-icon/app-icon.d.ts +88 -0
  5. package/dist/types/components/app-sidebar/app-sidebar.d.ts +106 -32
  6. package/dist/types/components/card/card.d.ts +128 -0
  7. package/dist/types/components/chart/chart.d.ts +1 -0
  8. package/dist/types/components/filter/filter-model.d.ts +87 -0
  9. package/dist/types/components/filter/filter.d.ts +331 -0
  10. package/dist/types/components/launcher/launcher.d.ts +65 -6
  11. package/dist/types/components/select/select.d.ts +97 -4
  12. package/dist/types/components/table/table.d.ts +107 -3
  13. package/dist/types/components/tag-picker/tag-picker.d.ts +16 -0
  14. package/dist/types/core/dom.d.ts +9 -1
  15. package/dist/types/core/util.d.ts +9 -0
  16. package/dist/types/index.d.ts +6 -2
  17. package/dist/types/{components/app-rail → internal}/app-rail.d.ts +34 -19
  18. package/dist/types/zeyos/icons.d.ts +24 -2
  19. package/dist/types/zeyos/index.d.ts +3 -2
  20. package/dist/types/zeyos/launcher.d.ts +147 -0
  21. package/dist/types/zeyos/select.d.ts +41 -0
  22. package/dist/zx-zeyos.esm.js +2018 -113
  23. package/dist/zx-zeyos.esm.min.js +1 -1
  24. package/dist/zx.css +815 -16
  25. package/dist/zx.esm.js +2661 -338
  26. package/dist/zx.esm.min.js +3 -3
  27. package/dist/zx.global.js +2661 -338
  28. package/dist/zx.global.min.js +3 -3
  29. package/dist/zx.min.css +1 -1
  30. package/docs/api.json +602 -110
  31. package/docs/llms.md +181 -123
  32. package/docs/llms.txt +24 -21
  33. package/package.json +8 -17
  34. package/src/components/app-icon/app-icon.css +105 -0
  35. package/src/components/app-icon/app-icon.js +134 -0
  36. package/src/components/app-sidebar/app-sidebar.css +12 -0
  37. package/src/components/app-sidebar/app-sidebar.js +310 -110
  38. package/src/components/card/card.css +149 -0
  39. package/src/components/card/card.js +285 -0
  40. package/src/components/chart/chart.js +8 -3
  41. package/src/components/filter/filter-model.js +226 -0
  42. package/src/components/filter/filter.css +147 -0
  43. package/src/components/filter/filter.js +554 -0
  44. package/src/components/grid/grid.css +1 -1
  45. package/src/components/grid/grid.js +14 -4
  46. package/src/components/launcher/launcher.css +113 -3
  47. package/src/components/launcher/launcher.js +234 -26
  48. package/src/components/message/message.css +36 -3
  49. package/src/components/select/field-adapter.js +9 -4
  50. package/src/components/select/select.css +161 -10
  51. package/src/components/select/select.js +361 -48
  52. package/src/components/table/table.css +85 -0
  53. package/src/components/table/table.js +426 -31
  54. package/src/components/tag-picker/tag-picker.css +19 -0
  55. package/src/components/tag-picker/tag-picker.js +42 -5
  56. package/src/core/dom.js +21 -1
  57. package/src/core/util.js +20 -0
  58. package/src/index.js +9 -2
  59. package/src/{components/app-rail → internal}/app-rail.js +80 -30
  60. package/src/zeyos/icons.js +38 -20
  61. package/src/zeyos/index.js +8 -2
  62. package/src/zeyos/launcher.js +338 -0
  63. package/src/zeyos/modules.js +1 -0
  64. package/src/zeyos/select.js +116 -1
  65. package/styles/tokens/global.css +1 -1
  66. package/styles/tokens/modules.css +1 -0
  67. package/styles/tokens/semantic.css +9 -0
  68. package/styles/tokens/themes.css +2 -2
  69. package/styles/zx.css +3 -0
  70. package/MIGRATION.md +0 -749
  71. package/dist/types/compat/base.d.ts +0 -156
  72. package/dist/types/compat/globals.d.ts +0 -20
  73. package/dist/types/compat/index.d.ts +0 -128
  74. package/dist/types/compat/map/basic.d.ts +0 -175
  75. package/dist/types/compat/map/data.d.ts +0 -103
  76. package/dist/types/compat/map/date-time.d.ts +0 -80
  77. package/dist/types/compat/map/factory.d.ts +0 -9
  78. package/dist/types/compat/map/form.d.ts +0 -98
  79. package/dist/types/compat/map/helpers.d.ts +0 -12
  80. package/dist/types/compat/map/http.d.ts +0 -36
  81. package/dist/types/compat/map/layout.d.ts +0 -106
  82. package/dist/types/compat/map/options.d.ts +0 -80
  83. package/dist/types/compat/map/select.d.ts +0 -95
  84. package/dist/types/compat/map/stubs.d.ts +0 -32
  85. package/dist/types/compat/map/utilities.d.ts +0 -37
  86. package/dist/types/compat-entry.d.ts +0 -5
  87. package/dist/zx-compat.esm.js +0 -17848
  88. package/dist/zx-compat.esm.min.js +0 -1
  89. package/dist/zx-compat.global.js +0 -29008
  90. package/dist/zx-compat.global.min.js +0 -3
  91. package/src/compat/base.js +0 -291
  92. package/src/compat/globals.js +0 -167
  93. package/src/compat/index.js +0 -84
  94. package/src/compat/map/basic.js +0 -464
  95. package/src/compat/map/data.js +0 -254
  96. package/src/compat/map/date-time.js +0 -177
  97. package/src/compat/map/factory.js +0 -45
  98. package/src/compat/map/form.js +0 -330
  99. package/src/compat/map/helpers.js +0 -46
  100. package/src/compat/map/http.js +0 -117
  101. package/src/compat/map/layout.js +0 -256
  102. package/src/compat/map/options.js +0 -267
  103. package/src/compat/map/select.js +0 -198
  104. package/src/compat/map/stubs.js +0 -26
  105. package/src/compat/map/utilities.js +0 -113
  106. 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}>} */