@zeyos/zx 2.3.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.
- package/CHANGELOG.md +138 -0
- package/DESIGN-SYSTEM.md +96 -0
- package/README.md +29 -39
- package/dist/types/components/account-menu/account-menu.d.ts +144 -0
- package/dist/types/components/app-sidebar/app-sidebar.d.ts +243 -0
- package/dist/types/components/avatar/avatar.d.ts +100 -0
- package/dist/types/components/card/card.d.ts +128 -0
- package/dist/types/components/chart/chart.d.ts +322 -0
- package/dist/types/components/date-picker/month-picker.d.ts +6 -0
- package/dist/types/components/dialog/dialog.d.ts +9 -0
- package/dist/types/components/dock/dock.d.ts +495 -0
- package/dist/types/components/dropdown/dropdown.d.ts +2 -2
- package/dist/types/components/field/field.d.ts +14 -0
- package/dist/types/components/field-upload/field-upload.d.ts +37 -4
- package/dist/types/components/form/form.d.ts +25 -3
- package/dist/types/components/grid/grid.d.ts +172 -0
- package/dist/types/components/launcher/launcher.d.ts +359 -0
- package/dist/types/components/layout/layout.d.ts +4 -4
- package/dist/types/components/menu-button/menu-button.d.ts +10 -2
- package/dist/types/components/modal/modal.d.ts +25 -0
- package/dist/types/components/multi-value-editor/multi-value-editor.d.ts +7 -1
- package/dist/types/components/questionnaire/items.d.ts +327 -0
- package/dist/types/components/questionnaire/questionnaire.d.ts +449 -0
- package/dist/types/components/select/select.d.ts +51 -0
- package/dist/types/components/sheet/sheet-stack.d.ts +167 -0
- package/dist/types/components/sheet/sheet.d.ts +294 -0
- package/dist/types/components/split-view/split-view.d.ts +2 -21
- package/dist/types/components/table/table.d.ts +223 -15
- package/dist/types/components/tooltip/tooltip.d.ts +2 -2
- package/dist/types/components/value-list/value-list.d.ts +24 -3
- package/dist/types/core/dom.d.ts +122 -0
- package/dist/types/core/drag-axis.d.ts +111 -0
- package/dist/types/core/position.d.ts +4 -2
- package/dist/types/index.d.ts +13 -2
- package/dist/types/internal/app-rail.d.ts +265 -0
- package/dist/types/zeyos/icons.d.ts +5 -0
- package/dist/types/zeyos/index.d.ts +2 -1
- package/dist/types/zeyos/launcher.d.ts +147 -0
- package/dist/types/zeyos/select.d.ts +39 -0
- package/dist/zx-zeyos.esm.js +1870 -133
- package/dist/zx-zeyos.esm.min.js +1 -1
- package/dist/zx.css +2006 -267
- package/dist/zx.esm.js +8987 -2757
- package/dist/zx.esm.min.js +3 -3
- package/dist/zx.global.js +8987 -2757
- package/dist/zx.global.min.js +3 -3
- package/dist/zx.min.css +1 -1
- package/docs/api.json +4396 -2344
- package/docs/llms.md +463 -86
- package/docs/llms.txt +32 -19
- package/package.json +10 -17
- package/src/components/account-menu/account-menu.css +70 -0
- package/src/components/account-menu/account-menu.js +208 -0
- package/src/components/app-rail/app-rail.css +107 -0
- package/src/components/app-sidebar/app-sidebar.css +80 -0
- package/src/components/app-sidebar/app-sidebar.js +588 -0
- package/src/components/avatar/avatar.css +61 -0
- package/src/components/avatar/avatar.js +222 -0
- package/src/components/button/button.css +0 -1
- package/src/components/card/card.css +149 -0
- package/src/components/card/card.js +285 -0
- package/src/components/chart/chart.css +61 -0
- package/src/components/chart/chart.js +523 -0
- package/src/components/date-picker/month-picker.js +9 -0
- package/src/components/dialog/dialog.js +11 -4
- package/src/components/dock/dock.css +228 -0
- package/src/components/dock/dock.js +977 -0
- package/src/components/dropdown/dropdown.js +1 -1
- package/src/components/field/field.js +2 -0
- package/src/components/field-upload/field-upload.js +9 -4
- package/src/components/form/form.js +6 -3
- package/src/components/grid/grid.css +6 -0
- package/src/components/grid/grid.js +220 -0
- package/src/components/launcher/launcher.css +210 -0
- package/src/components/launcher/launcher.js +765 -0
- package/src/components/layout/layout.js +2 -2
- package/src/components/menu-button/menu-button.js +27 -2
- package/src/components/modal/modal.css +5 -1
- package/src/components/modal/modal.js +37 -2
- package/src/components/multi-value-editor/multi-value-editor.js +2 -1
- package/src/components/questionnaire/items.js +370 -0
- package/src/components/questionnaire/questionnaire.css +387 -0
- package/src/components/questionnaire/questionnaire.js +1033 -0
- package/src/components/select/field-adapter.js +9 -4
- package/src/components/select/select.css +144 -11
- package/src/components/select/select.js +259 -27
- package/src/components/sheet/sheet-stack.js +304 -0
- package/src/components/sheet/sheet.css +351 -0
- package/src/components/sheet/sheet.js +551 -0
- package/src/components/split-view/split-view.js +76 -161
- package/src/components/table/table.css +43 -0
- package/src/components/table/table.js +531 -52
- package/src/components/tooltip/tooltip.js +5 -2
- package/src/components/value-list/value-list.js +6 -3
- package/src/core/dom.js +322 -0
- package/src/core/drag-axis.js +203 -0
- package/src/core/position.js +68 -22
- package/src/index.js +20 -2
- package/src/internal/app-rail.js +718 -0
- package/src/zeyos/icons.js +8 -3
- package/src/zeyos/index.js +7 -1
- package/src/zeyos/launcher.js +338 -0
- package/src/zeyos/modules.js +1 -0
- package/src/zeyos/select.js +115 -1
- package/styles/tokens/dark.css +2 -3
- package/styles/tokens/global.css +6 -6
- package/styles/tokens/modules.css +1 -0
- package/styles/tokens/semantic.css +5 -3
- package/styles/tokens/themes.css +1 -1
- package/styles/zx.css +11 -1
- package/MIGRATION.md +0 -598
- package/dist/types/compat/base.d.ts +0 -156
- package/dist/types/compat/globals.d.ts +0 -20
- package/dist/types/compat/index.d.ts +0 -128
- package/dist/types/compat/map/basic.d.ts +0 -175
- package/dist/types/compat/map/data.d.ts +0 -102
- package/dist/types/compat/map/date-time.d.ts +0 -80
- package/dist/types/compat/map/factory.d.ts +0 -9
- package/dist/types/compat/map/form.d.ts +0 -98
- package/dist/types/compat/map/helpers.d.ts +0 -12
- package/dist/types/compat/map/http.d.ts +0 -36
- package/dist/types/compat/map/layout.d.ts +0 -106
- package/dist/types/compat/map/options.d.ts +0 -80
- package/dist/types/compat/map/select.d.ts +0 -95
- package/dist/types/compat/map/stubs.d.ts +0 -32
- package/dist/types/compat/map/utilities.d.ts +0 -37
- package/dist/types/compat-entry.d.ts +0 -5
- package/dist/types/components/permission/permission.d.ts +0 -96
- package/dist/zx-compat.esm.js +0 -17484
- package/dist/zx-compat.esm.min.js +0 -1
- package/dist/zx-compat.global.js +0 -23635
- package/dist/zx-compat.global.min.js +0 -3
- package/src/compat/base.js +0 -291
- package/src/compat/globals.js +0 -165
- package/src/compat/index.js +0 -84
- package/src/compat/map/basic.js +0 -464
- package/src/compat/map/data.js +0 -232
- package/src/compat/map/date-time.js +0 -177
- package/src/compat/map/factory.js +0 -45
- package/src/compat/map/form.js +0 -330
- package/src/compat/map/helpers.js +0 -46
- package/src/compat/map/http.js +0 -117
- package/src/compat/map/layout.js +0 -256
- package/src/compat/map/options.js +0 -267
- package/src/compat/map/select.js +0 -198
- package/src/compat/map/stubs.js +0 -26
- package/src/compat/map/utilities.js +0 -113
- package/src/compat-entry.js +0 -11
- package/src/components/permission/permission.css +0 -47
- package/src/components/permission/permission.js +0 -201
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
.zx-chart {
|
|
2
|
+
display: grid;
|
|
3
|
+
gap: var(--zx-space-4);
|
|
4
|
+
min-inline-size: 0;
|
|
5
|
+
color: var(--zx-color-text);
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
.zx-chart__viewport {
|
|
9
|
+
position: relative;
|
|
10
|
+
inline-size: 100%;
|
|
11
|
+
min-block-size: 220px;
|
|
12
|
+
aspect-ratio: var(--zx-chart-aspect-ratio, 16 / 9);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
.zx-chart__viewport[hidden],
|
|
16
|
+
.zx-chart__status[hidden] { display: none; }
|
|
17
|
+
|
|
18
|
+
.zx-chart__canvas {
|
|
19
|
+
position: absolute;
|
|
20
|
+
inset: 0;
|
|
21
|
+
inline-size: 100% !important;
|
|
22
|
+
block-size: 100% !important;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
.zx-chart__description {
|
|
26
|
+
margin: 0;
|
|
27
|
+
color: var(--zx-color-text-muted);
|
|
28
|
+
font-size: var(--zx-text-sm);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
.zx-chart__description:empty { display: none; }
|
|
32
|
+
|
|
33
|
+
.zx-chart__status {
|
|
34
|
+
display: grid;
|
|
35
|
+
min-block-size: 180px;
|
|
36
|
+
place-items: center;
|
|
37
|
+
border: 1px dashed var(--zx-color-border);
|
|
38
|
+
border-radius: var(--zx-radius-lg);
|
|
39
|
+
color: var(--zx-color-text-muted);
|
|
40
|
+
text-align: center;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
.zx-chart__summary[data-summary="hidden"] {
|
|
44
|
+
position: absolute;
|
|
45
|
+
inline-size: 1px;
|
|
46
|
+
block-size: 1px;
|
|
47
|
+
overflow: hidden;
|
|
48
|
+
clip-path: inset(50%);
|
|
49
|
+
white-space: nowrap;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
.zx-chart__table {
|
|
53
|
+
inline-size: 100%;
|
|
54
|
+
border-collapse: collapse;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
.zx-chart__table caption { margin-block-end: var(--zx-space-2); font-weight: 700; text-align: start; }
|
|
58
|
+
.zx-chart__table th,
|
|
59
|
+
.zx-chart__table td { border-block-end: 1px solid var(--zx-color-border); padding: var(--zx-space-2); text-align: end; }
|
|
60
|
+
.zx-chart__table th:first-child,
|
|
61
|
+
.zx-chart__table td:first-child { text-align: start; }
|
|
@@ -0,0 +1,523 @@
|
|
|
1
|
+
import { Component } from '../../core/component.js';
|
|
2
|
+
import { h } from '../../core/dom.js';
|
|
3
|
+
import { uid } from '../../core/util.js';
|
|
4
|
+
|
|
5
|
+
/** @typedef {{label?: string, data?: unknown[], [key: string]: unknown}} ChartDataset */
|
|
6
|
+
/** @typedef {{labels?: unknown[], datasets?: ChartDataset[], [key: string]: unknown}} ChartData */
|
|
7
|
+
/**
|
|
8
|
+
* @typedef {Object} ChartSpec
|
|
9
|
+
* @property {string} type Chart type.
|
|
10
|
+
* @property {ChartData} data Chart data.
|
|
11
|
+
* @property {Record<string, unknown>} options Engine-specific options.
|
|
12
|
+
* @property {(detail: ChartSelectDetail) => void} onSelect Point-selection callback.
|
|
13
|
+
*/
|
|
14
|
+
/**
|
|
15
|
+
* @typedef {Object} ChartHandle
|
|
16
|
+
* @property {(spec: ChartSpec) => void} update Update the mounted chart.
|
|
17
|
+
* @property {() => void} resize Resize the mounted chart.
|
|
18
|
+
* @property {() => void} destroy Destroy the mounted chart.
|
|
19
|
+
* @property {unknown} [instance] Engine instance.
|
|
20
|
+
*/
|
|
21
|
+
/** @typedef {{create: (canvas: HTMLCanvasElement, spec: ChartSpec) => ChartHandle}} ChartAdapter */
|
|
22
|
+
/**
|
|
23
|
+
* @typedef {Object} ChartJsAdapterOptions
|
|
24
|
+
* Generated API marker for the adapter class; its constructor accepts an injected engine and
|
|
25
|
+
* optional defaults directly rather than a component options object.
|
|
26
|
+
*/
|
|
27
|
+
/**
|
|
28
|
+
* @typedef {Object} ChartSelectDetail
|
|
29
|
+
* @property {number} datasetIndex Dataset index.
|
|
30
|
+
* @property {number} index Data index.
|
|
31
|
+
* @property {ChartDataset|null} dataset Dataset definition.
|
|
32
|
+
* @property {unknown} label Label at the selected index.
|
|
33
|
+
* @property {unknown} value Data value.
|
|
34
|
+
* @property {Event|null} nativeEvent Engine-native event when available.
|
|
35
|
+
*/
|
|
36
|
+
/**
|
|
37
|
+
* @typedef {Object} ChartOptions
|
|
38
|
+
* @property {ChartAdapter|null} [adapter=null] Injected engine adapter.
|
|
39
|
+
* @property {string} [type='bar'] Chart type passed to the adapter.
|
|
40
|
+
* @property {ChartData} [data={labels: [], datasets: []}] Chart data.
|
|
41
|
+
* @property {Record<string, unknown>} [chartOptions={}] Engine-specific options.
|
|
42
|
+
* @property {string} [label='Chart'] Accessible canvas and summary label; blank text falls back to `Chart`.
|
|
43
|
+
* @property {string} [description=''] Accessible description.
|
|
44
|
+
* @property {'hidden'|'visible'} [summary='hidden'] Semantic data-table presentation.
|
|
45
|
+
* @property {boolean} [loading=false] Initial loading state.
|
|
46
|
+
* @property {string} [emptyText='No chart data'] Empty-state text.
|
|
47
|
+
* @property {string} [loadingText='Loading chart…'] Loading-state text.
|
|
48
|
+
* @property {string} [errorText='Unable to render chart'] Error fallback text.
|
|
49
|
+
* @property {string} [aspectRatio='16 / 9'] Responsive container aspect ratio.
|
|
50
|
+
* @property {(event: CustomEvent<{instance: unknown}>) => void} [onready] Engine-ready listener.
|
|
51
|
+
* @property {(event: CustomEvent<{data: ChartData, type: string}>) => void} [onupdate] Update listener.
|
|
52
|
+
* @property {(event: CustomEvent<ChartSelectDetail>) => void} [onselect] Point-selection listener.
|
|
53
|
+
* @property {(event: CustomEvent<{error: unknown}>) => void} [onerror] Engine error listener.
|
|
54
|
+
*/
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Accessible, engine-neutral chart host with explicit adapter lifecycle.
|
|
58
|
+
* @fires Chart#ready
|
|
59
|
+
* @fires Chart#update
|
|
60
|
+
* @fires Chart#select
|
|
61
|
+
* @fires Chart#error
|
|
62
|
+
* @extends {Component<ChartOptions>}
|
|
63
|
+
*/
|
|
64
|
+
export class Chart extends Component {
|
|
65
|
+
static cssName = 'chart';
|
|
66
|
+
|
|
67
|
+
/** @type {Readonly<ChartOptions>} */
|
|
68
|
+
static defaults = {
|
|
69
|
+
adapter: null,
|
|
70
|
+
type: 'bar',
|
|
71
|
+
data: { labels: [], datasets: [] },
|
|
72
|
+
chartOptions: {},
|
|
73
|
+
label: 'Chart',
|
|
74
|
+
description: '',
|
|
75
|
+
summary: 'hidden',
|
|
76
|
+
loading: false,
|
|
77
|
+
emptyText: 'No chart data',
|
|
78
|
+
loadingText: 'Loading chart…',
|
|
79
|
+
errorText: 'Unable to render chart',
|
|
80
|
+
aspectRatio: '16 / 9'
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Creates or enhances a chart host.
|
|
85
|
+
* @param {Element|string|null} target Chart target.
|
|
86
|
+
* @param {ChartOptions} [options={}] Chart options.
|
|
87
|
+
*/
|
|
88
|
+
constructor(target = null, options = {}) {
|
|
89
|
+
super(target, options);
|
|
90
|
+
this._mountOrUpdate(false);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** @returns {HTMLElement} */
|
|
94
|
+
render() {
|
|
95
|
+
this._createdRoot = this.el === null;
|
|
96
|
+
const root = /** @type {HTMLElement} */ (this.el ?? h('div'));
|
|
97
|
+
this.el = root;
|
|
98
|
+
this._original = this._createdRoot ? null : {
|
|
99
|
+
attributes: Array.from(root.attributes, (attribute) => [attribute.name, attribute.value]),
|
|
100
|
+
children: Array.from(root.childNodes)
|
|
101
|
+
};
|
|
102
|
+
this._adapter = this.options.adapter;
|
|
103
|
+
this._type = String(this.options.type || 'bar');
|
|
104
|
+
this._data = normalizeChartData(this.options.data);
|
|
105
|
+
this._label = normalizeChartLabel(this.options.label);
|
|
106
|
+
this._chartOptions = cloneChartValue(this.options.chartOptions);
|
|
107
|
+
this._loading = Boolean(this.options.loading);
|
|
108
|
+
this._error = null;
|
|
109
|
+
this._hasError = false;
|
|
110
|
+
this._handle = null;
|
|
111
|
+
this._destroyed = false;
|
|
112
|
+
|
|
113
|
+
const descriptionId = uid('zx-chart-description');
|
|
114
|
+
const canvas = /** @type {HTMLCanvasElement} */ (h('canvas', {
|
|
115
|
+
ref: 'canvas',
|
|
116
|
+
class: 'zx-chart__canvas',
|
|
117
|
+
role: 'img',
|
|
118
|
+
ariaLabel: this._label,
|
|
119
|
+
ariaDescribedby: descriptionId
|
|
120
|
+
}));
|
|
121
|
+
const viewport = h('div', {
|
|
122
|
+
ref: 'viewport',
|
|
123
|
+
class: 'zx-chart__viewport'
|
|
124
|
+
}, canvas);
|
|
125
|
+
viewport.style.setProperty('--zx-chart-aspect-ratio', String(this.options.aspectRatio));
|
|
126
|
+
const description = h('p', {
|
|
127
|
+
ref: 'description',
|
|
128
|
+
class: 'zx-chart__description',
|
|
129
|
+
id: descriptionId
|
|
130
|
+
}, this.options.description);
|
|
131
|
+
const status = h('div', {
|
|
132
|
+
ref: 'status',
|
|
133
|
+
class: 'zx-chart__status',
|
|
134
|
+
role: 'status',
|
|
135
|
+
ariaLive: 'polite'
|
|
136
|
+
});
|
|
137
|
+
const summary = h('div', {
|
|
138
|
+
ref: 'summary',
|
|
139
|
+
class: 'zx-chart__summary',
|
|
140
|
+
dataset: { summary: this.options.summary === 'visible' ? 'visible' : 'hidden' }
|
|
141
|
+
});
|
|
142
|
+
root.replaceChildren(viewport, description, status, summary);
|
|
143
|
+
this._renderSummary();
|
|
144
|
+
this._syncState();
|
|
145
|
+
|
|
146
|
+
this._observer = typeof ResizeObserver === 'function' ? new ResizeObserver(() => this.resize()) : null;
|
|
147
|
+
this._observer?.observe(viewport);
|
|
148
|
+
return root;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/** Replaces chart data and updates or mounts the engine. @param {ChartData} data @returns {this} */
|
|
152
|
+
setData(data) {
|
|
153
|
+
this._data = normalizeChartData(data);
|
|
154
|
+
this._error = null;
|
|
155
|
+
this._hasError = false;
|
|
156
|
+
this._loading = false;
|
|
157
|
+
this._renderSummary();
|
|
158
|
+
this._mountOrUpdate(true);
|
|
159
|
+
return this;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/** Changes the chart type and updates/recreates the engine as needed. @param {string} type @returns {this} */
|
|
163
|
+
setType(type) {
|
|
164
|
+
this._type = String(type || 'bar');
|
|
165
|
+
this._mountOrUpdate(true);
|
|
166
|
+
return this;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/** Replaces engine-specific options. @param {Record<string, unknown>} options @returns {this} */
|
|
170
|
+
update(options) {
|
|
171
|
+
this._chartOptions = cloneChartValue(options && typeof options === 'object' ? options : {});
|
|
172
|
+
this._mountOrUpdate(true);
|
|
173
|
+
return this;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/** Replaces the engine adapter. @param {ChartAdapter|null} adapter @returns {this} */
|
|
177
|
+
setAdapter(adapter) {
|
|
178
|
+
this._destroyHandle();
|
|
179
|
+
this._adapter = adapter;
|
|
180
|
+
this._mountOrUpdate(false);
|
|
181
|
+
return this;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/** Toggles loading state. Loading charts do not keep an engine instance alive. @param {boolean} [loading=true] @returns {this} */
|
|
185
|
+
setLoading(loading = true) {
|
|
186
|
+
this._loading = Boolean(loading);
|
|
187
|
+
if (this._loading) this._destroyHandle();
|
|
188
|
+
else this._mountOrUpdate(false);
|
|
189
|
+
this._syncState();
|
|
190
|
+
return this;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/** Shows an error and destroys the current engine. @param {unknown} error @returns {this} */
|
|
194
|
+
setError(error) {
|
|
195
|
+
this._error = error ?? new Error(String(this.options.errorText));
|
|
196
|
+
this._hasError = true;
|
|
197
|
+
this._loading = false;
|
|
198
|
+
this._destroyHandle();
|
|
199
|
+
this._syncState();
|
|
200
|
+
this.emit('error', { error: this._error });
|
|
201
|
+
return this;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/** Clears the error and remounts when data and an adapter are available. @returns {this} */
|
|
205
|
+
clearError() {
|
|
206
|
+
this._error = null;
|
|
207
|
+
this._hasError = false;
|
|
208
|
+
this._mountOrUpdate(false);
|
|
209
|
+
return this;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/** Requests an engine resize. @returns {this} */
|
|
213
|
+
resize() {
|
|
214
|
+
try { this._handle?.resize?.(); } catch (error) { this.setError(error); }
|
|
215
|
+
return this;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/** Re-applies computed theme defaults through the adapter update path. @returns {this} */
|
|
219
|
+
refreshTheme() {
|
|
220
|
+
this._mountOrUpdate(true);
|
|
221
|
+
return this;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/** Returns the underlying engine instance when the adapter exposes it. @returns {unknown} */
|
|
225
|
+
getInstance() {
|
|
226
|
+
return this._handle?.instance ?? null;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/** Destroys the engine/observer and restores or removes the host. @returns {void} */
|
|
230
|
+
destroy() {
|
|
231
|
+
if (this._destroyed) return;
|
|
232
|
+
this._destroyed = true;
|
|
233
|
+
const original = this._original;
|
|
234
|
+
this._observer?.disconnect();
|
|
235
|
+
this._destroyHandle();
|
|
236
|
+
super.destroy();
|
|
237
|
+
if (!this._createdRoot && original) {
|
|
238
|
+
for (const attribute of Array.from(this.el.attributes)) this.el.removeAttribute(attribute.name);
|
|
239
|
+
for (const [name, value] of original.attributes) this.el.setAttribute(name, value);
|
|
240
|
+
this.el.replaceChildren(...original.children);
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/** @param {boolean} emitUpdate @returns {void} */
|
|
245
|
+
_mountOrUpdate(emitUpdate) {
|
|
246
|
+
this._syncState();
|
|
247
|
+
if (this._loading || this._hasError || isEmptyChartData(this._data)) {
|
|
248
|
+
this._destroyHandle();
|
|
249
|
+
return;
|
|
250
|
+
}
|
|
251
|
+
if (!this._adapter || typeof this._adapter.create !== 'function') {
|
|
252
|
+
this._destroyHandle();
|
|
253
|
+
return;
|
|
254
|
+
}
|
|
255
|
+
const spec = this._spec();
|
|
256
|
+
try {
|
|
257
|
+
if (this._handle) this._handle.update(spec);
|
|
258
|
+
else {
|
|
259
|
+
this._handle = this._adapter.create(/** @type {HTMLCanvasElement} */ (this.refs.canvas), spec);
|
|
260
|
+
if (!this._handle || typeof this._handle.destroy !== 'function') {
|
|
261
|
+
throw new TypeError('Chart adapter create() must return a lifecycle handle');
|
|
262
|
+
}
|
|
263
|
+
this.emit('ready', { instance: this.getInstance() });
|
|
264
|
+
}
|
|
265
|
+
if (emitUpdate) this.emit('update', { data: this._data, type: this._type });
|
|
266
|
+
} catch (error) {
|
|
267
|
+
this.setError(error);
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/** @returns {ChartSpec} */
|
|
272
|
+
_spec() {
|
|
273
|
+
return {
|
|
274
|
+
type: this._type,
|
|
275
|
+
data: cloneChartValue(this._data),
|
|
276
|
+
options: {
|
|
277
|
+
...themeChartOptions(/** @type {HTMLElement} */ (this.el)),
|
|
278
|
+
...cloneChartValue(this._chartOptions)
|
|
279
|
+
},
|
|
280
|
+
onSelect: (detail) => this.emit('select', detail)
|
|
281
|
+
};
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/** @returns {void} */
|
|
285
|
+
_destroyHandle() {
|
|
286
|
+
if (!this._handle) return;
|
|
287
|
+
const handle = this._handle;
|
|
288
|
+
this._handle = null;
|
|
289
|
+
try { handle.destroy(); } catch { /* teardown must remain safe */ }
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
/** @returns {void} */
|
|
293
|
+
_syncState() {
|
|
294
|
+
const empty = isEmptyChartData(this._data);
|
|
295
|
+
const state = this._loading ? 'loading' : this._hasError ? 'error' : empty ? 'empty' : 'ready';
|
|
296
|
+
this.el.dataset.state = state;
|
|
297
|
+
this.refs.viewport.hidden = state !== 'ready';
|
|
298
|
+
let text = '';
|
|
299
|
+
if (state === 'loading') text = String(this.options.loadingText);
|
|
300
|
+
else if (state === 'error') text = errorMessage(this._error, this.options.errorText);
|
|
301
|
+
else if (state === 'empty') text = String(this.options.emptyText);
|
|
302
|
+
this.refs.status.textContent = text;
|
|
303
|
+
this.refs.status.hidden = !text;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
/** @returns {void} */
|
|
307
|
+
_renderSummary() {
|
|
308
|
+
this.refs.summary.replaceChildren(chartSummaryTable(this._data, this._label));
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
/** @param {unknown} value @returns {string} */
|
|
313
|
+
function normalizeChartLabel(value) {
|
|
314
|
+
return String(value ?? '').trim() || 'Chart';
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
/**
|
|
318
|
+
* Adapter for an injected Chart.js 4 constructor or namespace.
|
|
319
|
+
*/
|
|
320
|
+
export class ChartJsAdapter {
|
|
321
|
+
/**
|
|
322
|
+
* @param {Function|{Chart?: Function}} engine Injected Chart.js constructor or namespace.
|
|
323
|
+
* @param {Record<string, unknown>} [defaults={}] Adapter-wide Chart.js options.
|
|
324
|
+
*/
|
|
325
|
+
constructor(engine, defaults = {}) {
|
|
326
|
+
const constructor = typeof engine === 'function' ? engine : engine?.Chart;
|
|
327
|
+
if (typeof constructor !== 'function') throw new TypeError('ChartJsAdapter requires an injected Chart.js constructor');
|
|
328
|
+
this.engine = constructor;
|
|
329
|
+
this.defaults = cloneChartValue(defaults);
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
/**
|
|
333
|
+
* Creates one Chart.js lifecycle handle.
|
|
334
|
+
* @param {HTMLCanvasElement} canvas Canvas.
|
|
335
|
+
* @param {ChartSpec} spec Initial chart specification.
|
|
336
|
+
* @returns {ChartHandle}
|
|
337
|
+
*/
|
|
338
|
+
create(canvas, spec) {
|
|
339
|
+
const Engine = this.engine;
|
|
340
|
+
let instance = null;
|
|
341
|
+
let currentType = '';
|
|
342
|
+
let destroyed = false;
|
|
343
|
+
|
|
344
|
+
const build = (next) => {
|
|
345
|
+
let created = null;
|
|
346
|
+
const config = chartJsConfig(next, this.defaults, () => created);
|
|
347
|
+
created = new Engine(canvas, config);
|
|
348
|
+
instance = created;
|
|
349
|
+
currentType = next.type;
|
|
350
|
+
};
|
|
351
|
+
build(spec);
|
|
352
|
+
|
|
353
|
+
return {
|
|
354
|
+
get instance() { return instance; },
|
|
355
|
+
update: (next) => {
|
|
356
|
+
if (destroyed) return;
|
|
357
|
+
if (next.type !== currentType) {
|
|
358
|
+
const previous = instance;
|
|
359
|
+
instance = null;
|
|
360
|
+
previous?.destroy?.();
|
|
361
|
+
build(next);
|
|
362
|
+
return;
|
|
363
|
+
}
|
|
364
|
+
const config = chartJsConfig(next, this.defaults, () => instance);
|
|
365
|
+
instance.data = config.data;
|
|
366
|
+
instance.options = config.options;
|
|
367
|
+
instance.update();
|
|
368
|
+
},
|
|
369
|
+
resize: () => { if (!destroyed) instance?.resize?.(); },
|
|
370
|
+
destroy: () => {
|
|
371
|
+
if (destroyed) return;
|
|
372
|
+
destroyed = true;
|
|
373
|
+
instance?.destroy?.();
|
|
374
|
+
instance = null;
|
|
375
|
+
}
|
|
376
|
+
};
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
/** @param {ChartSpec} spec @param {Record<string, unknown>} defaults @param {() => any} getInstance @returns {Record<string, unknown>} */
|
|
381
|
+
function chartJsConfig(spec, defaults, getInstance) {
|
|
382
|
+
const supplied = cloneChartValue(spec.options);
|
|
383
|
+
const options = {
|
|
384
|
+
responsive: true,
|
|
385
|
+
maintainAspectRatio: false,
|
|
386
|
+
...cloneChartValue(defaults),
|
|
387
|
+
...supplied
|
|
388
|
+
};
|
|
389
|
+
const originalClick = options.onClick;
|
|
390
|
+
options.onClick = (event, elements, chart) => {
|
|
391
|
+
if (typeof originalClick === 'function') originalClick(event, elements, chart);
|
|
392
|
+
const instance = getInstance() ?? chart;
|
|
393
|
+
const points = Array.isArray(elements) && elements.length ? elements
|
|
394
|
+
: instance?.getElementsAtEventForMode?.(event, 'nearest', { intersect: true }, true) ?? [];
|
|
395
|
+
const point = points[0];
|
|
396
|
+
if (!point) return;
|
|
397
|
+
const dataset = spec.data.datasets?.[point.datasetIndex] ?? null;
|
|
398
|
+
spec.onSelect({
|
|
399
|
+
datasetIndex: point.datasetIndex,
|
|
400
|
+
index: point.index,
|
|
401
|
+
dataset,
|
|
402
|
+
label: spec.data.labels?.[point.index],
|
|
403
|
+
value: dataset?.data?.[point.index],
|
|
404
|
+
nativeEvent: event?.native ?? event ?? null
|
|
405
|
+
});
|
|
406
|
+
};
|
|
407
|
+
return { type: spec.type, data: cloneChartValue(spec.data), options };
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
/**
|
|
411
|
+
* Clones arrays and plain objects while preserving callbacks and engine-specific object instances.
|
|
412
|
+
* @template T
|
|
413
|
+
* @param {T} value Value.
|
|
414
|
+
* @returns {T}
|
|
415
|
+
*/
|
|
416
|
+
export function cloneChartValue(value) {
|
|
417
|
+
return cloneValue(value, new WeakMap());
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
/** @template T @param {T} value @param {WeakMap<object, object>} seen @returns {T} */
|
|
421
|
+
function cloneValue(value, seen) {
|
|
422
|
+
if (Array.isArray(value)) {
|
|
423
|
+
if (seen.has(value)) return /** @type {T} */ (seen.get(value));
|
|
424
|
+
const copy = [];
|
|
425
|
+
seen.set(value, copy);
|
|
426
|
+
copy.push(...value.map((item) => cloneValue(item, seen)));
|
|
427
|
+
return /** @type {T} */ (copy);
|
|
428
|
+
}
|
|
429
|
+
if (value && typeof value === 'object' && Object.getPrototypeOf(value) === Object.prototype) {
|
|
430
|
+
if (seen.has(value)) return /** @type {T} */ (seen.get(value));
|
|
431
|
+
const copy = {};
|
|
432
|
+
seen.set(value, copy);
|
|
433
|
+
for (const [key, item] of Object.entries(value)) copy[key] = cloneValue(item, seen);
|
|
434
|
+
return /** @type {T} */ (copy);
|
|
435
|
+
}
|
|
436
|
+
return value;
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
/** @param {unknown} data @returns {ChartData} */
|
|
440
|
+
function normalizeChartData(data) {
|
|
441
|
+
if (!data || typeof data !== 'object') return { labels: [], datasets: [] };
|
|
442
|
+
return cloneChartValue({
|
|
443
|
+
...data,
|
|
444
|
+
labels: Array.isArray(data.labels) ? data.labels : [],
|
|
445
|
+
datasets: Array.isArray(data.datasets) ? data.datasets : []
|
|
446
|
+
});
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
/** @param {ChartData} data @returns {boolean} */
|
|
450
|
+
export function isEmptyChartData(data) {
|
|
451
|
+
return !Array.isArray(data?.datasets) || !data.datasets.some((dataset) => Array.isArray(dataset?.data) && dataset.data.length > 0);
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
/**
|
|
455
|
+
* Builds the semantic table mirrored alongside the canvas.
|
|
456
|
+
* @param {ChartData} data Chart data.
|
|
457
|
+
* @param {string} label Caption.
|
|
458
|
+
* @returns {HTMLTableElement}
|
|
459
|
+
*/
|
|
460
|
+
export function chartSummaryTable(data, label = 'Chart data') {
|
|
461
|
+
const summary = chartSummaryModel(data);
|
|
462
|
+
const table = /** @type {HTMLTableElement} */ (h('table', { class: 'zx-chart__table' },
|
|
463
|
+
h('caption', {}, label),
|
|
464
|
+
h('thead', {}, h('tr', {}, h('th', { scope: 'col' }, 'Label'),
|
|
465
|
+
summary.headers.map((header) => h('th', { scope: 'col' }, header)))),
|
|
466
|
+
h('tbody', {}, summary.rows.map((row) => h('tr', {},
|
|
467
|
+
h('th', { scope: 'row' }, row.label),
|
|
468
|
+
row.values.map((value) => h('td', {}, value)))))));
|
|
469
|
+
return table;
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
/**
|
|
473
|
+
* Produces the exact text matrix rendered by the semantic chart summary.
|
|
474
|
+
* @param {ChartData} data Chart data.
|
|
475
|
+
* @returns {{headers: string[], rows: Array<{label: string, values: string[]}>}}
|
|
476
|
+
*/
|
|
477
|
+
export function chartSummaryModel(data) {
|
|
478
|
+
const datasets = Array.isArray(data?.datasets) ? data.datasets : [];
|
|
479
|
+
const labels = Array.isArray(data?.labels) ? data.labels : [];
|
|
480
|
+
const length = Math.max(labels.length,
|
|
481
|
+
...datasets.map((dataset) => Array.isArray(dataset.data) ? dataset.data.length : 0), 0);
|
|
482
|
+
return {
|
|
483
|
+
headers: datasets.map((dataset, index) => String(dataset.label ?? `Series ${index + 1}`)),
|
|
484
|
+
rows: Array.from({ length }, (_, index) => ({
|
|
485
|
+
label: String(labels[index] ?? index + 1),
|
|
486
|
+
values: datasets.map((dataset) => summaryValue(dataset.data?.[index]))
|
|
487
|
+
}))
|
|
488
|
+
};
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
/** @param {unknown} value @returns {string} */
|
|
492
|
+
function summaryValue(value) {
|
|
493
|
+
if (value == null) return '';
|
|
494
|
+
if (typeof value === 'object') {
|
|
495
|
+
if ('y' in value) return String(value.y ?? '');
|
|
496
|
+
if ('r' in value) return String(value.r ?? '');
|
|
497
|
+
try { return JSON.stringify(value); } catch { return String(value); }
|
|
498
|
+
}
|
|
499
|
+
return String(value);
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
/** @param {HTMLElement} root @returns {Record<string, unknown>} */
|
|
503
|
+
function themeChartOptions(root) {
|
|
504
|
+
if (typeof getComputedStyle !== 'function') return {};
|
|
505
|
+
const style = getComputedStyle(root);
|
|
506
|
+
const color = style.getPropertyValue('--zx-color-text').trim();
|
|
507
|
+
const borderColor = style.getPropertyValue('--zx-color-border').trim();
|
|
508
|
+
return {
|
|
509
|
+
color: color || undefined,
|
|
510
|
+
borderColor: borderColor || undefined
|
|
511
|
+
};
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
/** @param {unknown} error @param {unknown} fallback @returns {string} */
|
|
515
|
+
function errorMessage(error, fallback) {
|
|
516
|
+
const message = error && typeof error === 'object' && 'message' in error ? String(error.message) : '';
|
|
517
|
+
return message.trim() ? message : String(fallback);
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
/** Fired after the adapter creates an engine instance. @event Chart#ready @type {CustomEvent<{instance: unknown}>} */
|
|
521
|
+
/** Fired after an existing chart updates. @event Chart#update @type {CustomEvent<{data: ChartData, type: string}>} */
|
|
522
|
+
/** Fired when the engine reports a selected point. @event Chart#select @type {CustomEvent<ChartSelectDetail>} */
|
|
523
|
+
/** Fired when adapter creation/update fails. @event Chart#error @type {CustomEvent<{error: unknown}>} */
|
|
@@ -22,6 +22,15 @@ export class MonthPicker extends DatePicker {
|
|
|
22
22
|
/** @type {MonthPickerOptions} */
|
|
23
23
|
static defaults = { value: null, min: null, max: null };
|
|
24
24
|
|
|
25
|
+
/**
|
|
26
|
+
* Creates a month picker with its narrower month-based option contract.
|
|
27
|
+
* @param {Element|string|null} target Existing element, selector, or null.
|
|
28
|
+
* @param {MonthPickerOptions} [options={}] Month-picker options.
|
|
29
|
+
*/
|
|
30
|
+
constructor(target, options = {}) {
|
|
31
|
+
super(target, options);
|
|
32
|
+
}
|
|
33
|
+
|
|
25
34
|
/** @returns {HTMLElement} */
|
|
26
35
|
render() {
|
|
27
36
|
const root = /** @type {HTMLElement} */ (this.el ?? h('div'));
|
|
@@ -95,7 +95,7 @@ export class Dialog extends Modal {
|
|
|
95
95
|
*/
|
|
96
96
|
constructor(_target = null, options = {}) {
|
|
97
97
|
super(null, options);
|
|
98
|
-
this
|
|
98
|
+
this._applySize(this.options.size);
|
|
99
99
|
this.#baseTitle = String(this.options.title ?? '');
|
|
100
100
|
this.#applyTitle(this.#baseTitle);
|
|
101
101
|
this.#baseButtons = copyButtons(this.options.buttons);
|
|
@@ -141,7 +141,7 @@ export class Dialog extends Modal {
|
|
|
141
141
|
h('footer', { class: 'zx-dialog__footer', ref: 'footer' })
|
|
142
142
|
));
|
|
143
143
|
dialog.dataset.state = 'closed';
|
|
144
|
-
|
|
144
|
+
this.mountTarget().append(dialog);
|
|
145
145
|
return dialog;
|
|
146
146
|
}
|
|
147
147
|
|
|
@@ -368,8 +368,15 @@ export class Dialog extends Modal {
|
|
|
368
368
|
});
|
|
369
369
|
}
|
|
370
370
|
|
|
371
|
-
/**
|
|
372
|
-
|
|
371
|
+
/**
|
|
372
|
+
* Applies the dialog width. Subclasses that size themselves on a different axis override this.
|
|
373
|
+
*
|
|
374
|
+
* Called from the constructor before any subclass field initializers have run, so an override
|
|
375
|
+
* may only touch the DOM — reading an uninitialized private field here throws.
|
|
376
|
+
* @param {'sm'|'md'|'lg'|number} size Preset or pixel width.
|
|
377
|
+
* @returns {void}
|
|
378
|
+
*/
|
|
379
|
+
_applySize(size) {
|
|
373
380
|
const presets = { sm: 400, md: 600, lg: 840 };
|
|
374
381
|
const width = typeof size === 'number' && Number.isFinite(size) ? size : (presets[size] ?? presets.md);
|
|
375
382
|
this.el.style.inlineSize = `${Math.max(0, width)}px`;
|