@aiaiai-pt/design-system 0.35.0 → 0.37.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/components/EChart.svelte +78 -4
- package/components/renderer/DonutChartWidget.svelte +11 -0
- package/components/renderer/DonutChartWidget.svelte.d.ts +4 -0
- package/components/renderer/EChartWidget.svelte +7 -1
- package/components/renderer/LineChartWidget.svelte +12 -0
- package/components/renderer/LineChartWidget.svelte.d.ts +4 -0
- package/components/renderer/display.d.ts +71 -0
- package/components/renderer/display.ts +161 -0
- package/components/renderer/registry.ts +7 -0
- package/package.json +1 -1
package/components/EChart.svelte
CHANGED
|
@@ -54,6 +54,11 @@
|
|
|
54
54
|
valueHeader = "Value",
|
|
55
55
|
/** @type {string} BCP-47 locale for number formatting. */
|
|
56
56
|
locale = "en",
|
|
57
|
+
/** @type {"bar" | "line" | "donut"} Chart kind (#176 Tier 1). `bar` is the
|
|
58
|
+
* horizontal categorical bar; `line` plots the same series as a value line
|
|
59
|
+
* over the category axis; `donut` is a ring pie. The data table below is
|
|
60
|
+
* the accessible source of truth for every kind. */
|
|
61
|
+
kind = "bar",
|
|
57
62
|
/** @type {string} CSS block-size for the chart canvas. */
|
|
58
63
|
height = "20rem",
|
|
59
64
|
/** @type {string} */
|
|
@@ -90,9 +95,73 @@
|
|
|
90
95
|
const textSecondary = token("--color-text-secondary", "#6b7280");
|
|
91
96
|
const border = token("--color-border", "#e5e7eb");
|
|
92
97
|
|
|
93
|
-
//
|
|
94
|
-
//
|
|
95
|
-
|
|
98
|
+
// Donut: a ring pie. No axes; the slice palette derives from the accent
|
|
99
|
+
// (alpha ramp) so it stays on-theme without a hardcoded colour list.
|
|
100
|
+
if (kind === "donut") {
|
|
101
|
+
const palette = items.map((_, i) => {
|
|
102
|
+
const alpha = Math.max(0.35, 1 - i * 0.12);
|
|
103
|
+
return `color-mix(in srgb, ${accent} ${Math.round(alpha * 100)}%, transparent)`;
|
|
104
|
+
});
|
|
105
|
+
return {
|
|
106
|
+
animation: false,
|
|
107
|
+
tooltip: {
|
|
108
|
+
trigger: "item",
|
|
109
|
+
/** @param {{ name: string, value: number, percent: number }} p */
|
|
110
|
+
formatter: (p) => `${p.name}: ${fmt(p.value)} (${p.percent}%)`,
|
|
111
|
+
},
|
|
112
|
+
color: palette,
|
|
113
|
+
series: [
|
|
114
|
+
{
|
|
115
|
+
type: "pie",
|
|
116
|
+
radius: ["45%", "72%"],
|
|
117
|
+
data: items.map((it) => ({ name: it.label, value: it.value })),
|
|
118
|
+
label: { color: textPrimary },
|
|
119
|
+
labelLine: { lineStyle: { color: border } },
|
|
120
|
+
},
|
|
121
|
+
],
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// Line: value over the category axis, in the consumer's order (desc by
|
|
126
|
+
// value). Shares the same accent + token theming as the bar.
|
|
127
|
+
if (kind === "line") {
|
|
128
|
+
return {
|
|
129
|
+
animation: false,
|
|
130
|
+
grid: { left: 8, right: 16, top: 8, bottom: 8, containLabel: true },
|
|
131
|
+
tooltip: {
|
|
132
|
+
trigger: "axis",
|
|
133
|
+
/** @param {{ name: string, value: number }[]} p */
|
|
134
|
+
formatter: (p) => `${p[0]?.name}: ${fmt(p[0]?.value)}`,
|
|
135
|
+
},
|
|
136
|
+
xAxis: {
|
|
137
|
+
type: "category",
|
|
138
|
+
data: items.map((it) => it.label),
|
|
139
|
+
axisLabel: { color: textPrimary },
|
|
140
|
+
axisLine: { lineStyle: { color: border } },
|
|
141
|
+
axisTick: { show: false },
|
|
142
|
+
},
|
|
143
|
+
yAxis: {
|
|
144
|
+
type: "value",
|
|
145
|
+
axisLabel: { color: textSecondary, formatter: (/** @type {number} */ v) => fmt(v) },
|
|
146
|
+
axisLine: { lineStyle: { color: border } },
|
|
147
|
+
splitLine: { lineStyle: { color: border } },
|
|
148
|
+
},
|
|
149
|
+
series: [
|
|
150
|
+
{
|
|
151
|
+
type: "line",
|
|
152
|
+
data: items.map((it) => it.value),
|
|
153
|
+
itemStyle: { color: accent },
|
|
154
|
+
lineStyle: { color: accent },
|
|
155
|
+
symbolSize: 7,
|
|
156
|
+
smooth: false,
|
|
157
|
+
},
|
|
158
|
+
],
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// Bar (default). ECharts plots the category axis bottom-up; reverse so the
|
|
163
|
+
// highest value sits at the top (matching ResultsChart's top-down order).
|
|
164
|
+
// `items` is already sorted desc by the consumer.
|
|
96
165
|
const ordered = [...items].reverse();
|
|
97
166
|
|
|
98
167
|
return {
|
|
@@ -150,6 +219,8 @@
|
|
|
150
219
|
|
|
151
220
|
core.use([
|
|
152
221
|
charts.BarChart,
|
|
222
|
+
charts.LineChart,
|
|
223
|
+
charts.PieChart,
|
|
153
224
|
components.GridComponent,
|
|
154
225
|
components.TooltipComponent,
|
|
155
226
|
renderers.CanvasRenderer,
|
|
@@ -187,7 +258,10 @@
|
|
|
187
258
|
// touch reactive deps so the effect re-runs when they change
|
|
188
259
|
void items;
|
|
189
260
|
void locale;
|
|
190
|
-
|
|
261
|
+
void kind;
|
|
262
|
+
// A kind switch changes which axes/series exist; replace (not merge) the
|
|
263
|
+
// option so stale axis components from the previous kind are cleared.
|
|
264
|
+
if (_chart) _chart.setOption(buildOption(), true);
|
|
191
265
|
});
|
|
192
266
|
</script>
|
|
193
267
|
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import EChartWidget from "./EChartWidget.svelte";
|
|
3
|
+
import type { WidgetProps } from "./types";
|
|
4
|
+
|
|
5
|
+
// `donut-chart` registry key (#176 Tier 1). Thin wrapper binding the ECharts
|
|
6
|
+
// `kind` to a ring pie — the registry maps this key here. Same aggregate data
|
|
7
|
+
// path + a11y data table as the bar/line.
|
|
8
|
+
let p: WidgetProps = $props();
|
|
9
|
+
</script>
|
|
10
|
+
|
|
11
|
+
<EChartWidget {...p} kind="donut" />
|
|
@@ -11,7 +11,12 @@
|
|
|
11
11
|
// is never the only encoding). Same data path as ResultsChartWidget: reads
|
|
12
12
|
// the `{ items }` rows and projects `{ label, value }` via `toRankedItems`,
|
|
13
13
|
// so NO resolve-data / provider change is needed. Soft-empty on no rows.
|
|
14
|
-
|
|
14
|
+
// `kind` is bound by the registry wrapper (line-chart / donut-chart), NOT by
|
|
15
|
+
// the dispatcher (which renders this widget with the standard WidgetProps and
|
|
16
|
+
// no kind → the `chart` key stays the ECharts bar). #176 Tier 1.
|
|
17
|
+
let { data, props, ownsH1, locale, kind = "bar" }: WidgetProps & {
|
|
18
|
+
kind?: "bar" | "line" | "donut";
|
|
19
|
+
} = $props();
|
|
15
20
|
|
|
16
21
|
const rows = $derived(
|
|
17
22
|
data && typeof data === "object" && Array.isArray((data as { items?: unknown }).items)
|
|
@@ -63,6 +68,7 @@
|
|
|
63
68
|
{caption}
|
|
64
69
|
{labelHeader}
|
|
65
70
|
{valueHeader}
|
|
71
|
+
{kind}
|
|
66
72
|
locale={locale ?? "en"}
|
|
67
73
|
/>
|
|
68
74
|
</section>
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import EChartWidget from "./EChartWidget.svelte";
|
|
3
|
+
import type { WidgetProps } from "./types";
|
|
4
|
+
|
|
5
|
+
// `line-chart` registry key (#176 Tier 1). Thin wrapper that binds the ECharts
|
|
6
|
+
// `kind` the dispatcher can't pass — the registry maps this key to this
|
|
7
|
+
// component, which renders the shared EChart widget as a line. Same aggregate
|
|
8
|
+
// data path + a11y data table as the bar.
|
|
9
|
+
let p: WidgetProps = $props();
|
|
10
|
+
</script>
|
|
11
|
+
|
|
12
|
+
<EChartWidget {...p} kind="line" />
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Host-agnostic display transforms (#499 criterion A) — the pure value→text +
|
|
3
|
+
* status-badge + geo helpers the list/detail surfaces of BOTH hosts (the
|
|
4
|
+
* citizen portal AND the staff admin) share. Previously duplicated: the portal
|
|
5
|
+
* kept them in `portal/src/lib/renderer/{cells,columns}.ts`; the admin re-rolled
|
|
6
|
+
* equivalents inline (`renderCell`/`formatValue`). Lifting them here makes the
|
|
7
|
+
* value-redaction boundary one tested function instead of two drifting ones.
|
|
8
|
+
*
|
|
9
|
+
* Two host forks are parameters, not separate copies (the reconciliation #499
|
|
10
|
+
* called out before sharing):
|
|
11
|
+
* - object fallback: the portal REDACTS an unknown object (never leak a blob
|
|
12
|
+
* into the public DOM — a hard rule); the admin JSON-dumps it for operators.
|
|
13
|
+
* → `formatScalar(value, { objectFallback: "redact" | "json" })`, default
|
|
14
|
+
* "redact" (the safe default; a caller opts INTO dumping).
|
|
15
|
+
* - date detection: the portal detects timestamps by VALUE shape (the public
|
|
16
|
+
* schema strips created_at/updated_at, so type-driven detection misses
|
|
17
|
+
* them); the admin knows the field is a datetime from its schema TYPE.
|
|
18
|
+
* → `displayCell(value, locale, { treatAsDate })` — `treatAsDate` forces
|
|
19
|
+
* date formatting (admin, from schema type); otherwise value-shape
|
|
20
|
+
* detection (`isIsoTimestamp`) applies (portal).
|
|
21
|
+
*
|
|
22
|
+
* Everything here is PURE and dependency-free.
|
|
23
|
+
*/
|
|
24
|
+
export type ObjectFallback = "redact" | "json";
|
|
25
|
+
/**
|
|
26
|
+
* Pure value → display string. Scalars stringify; arrays join their non-empty
|
|
27
|
+
* formatted members; a disclosed relationship object ({id,label}|{name}|{title})
|
|
28
|
+
* renders its human label then its id. An UNKNOWN object renders per
|
|
29
|
+
* `objectFallback`: "redact" (default) → "" (never dumps internals — the public
|
|
30
|
+
* security boundary); "json" → pretty JSON (the admin operator view).
|
|
31
|
+
*/
|
|
32
|
+
export declare function formatScalar(value: unknown, opts?: {
|
|
33
|
+
objectFallback?: ObjectFallback;
|
|
34
|
+
}): string;
|
|
35
|
+
/** True for an ISO-8601 timestamp STRING (not a bare date). Value-shape
|
|
36
|
+
* detection — used where the schema can't tell us the field is a datetime. */
|
|
37
|
+
export declare function isIsoTimestamp(value: unknown): value is string;
|
|
38
|
+
/** Locale date for an ISO timestamp (date-only, UTC-stable so SSR == hydrate);
|
|
39
|
+
* the raw string when unparseable. */
|
|
40
|
+
export declare function formatTimestamp(value: string, locale: string): string;
|
|
41
|
+
/**
|
|
42
|
+
* The single display boundary for list cells / detail values. Formats a value
|
|
43
|
+
* as a locale date when it's a timestamp — either because `opts.treatAsDate`
|
|
44
|
+
* forces it (the admin, from the schema field type) or because the value itself
|
|
45
|
+
* looks like an ISO timestamp (the portal, value-shape). Everything else goes
|
|
46
|
+
* through `formatScalar` (with the host's object-fallback policy).
|
|
47
|
+
*/
|
|
48
|
+
export declare function displayCell(value: unknown, locale: string, opts?: {
|
|
49
|
+
treatAsDate?: boolean;
|
|
50
|
+
objectFallback?: ObjectFallback;
|
|
51
|
+
}): string;
|
|
52
|
+
/** Operator copy for a status value (falls back to the raw value). `labels` is
|
|
53
|
+
* untrusted DATA — a non-object/array is ignored. */
|
|
54
|
+
export declare function statusLabel(value: unknown, labels: unknown): string;
|
|
55
|
+
/** DS Badge variant for a status value (neutral when unmapped). `variants` is
|
|
56
|
+
* untrusted DATA — a non-object/array is ignored. */
|
|
57
|
+
export declare function statusVariant(value: unknown, variants: unknown): string;
|
|
58
|
+
/** Resolve a status value to a `{ label, variant }` pair in one call. */
|
|
59
|
+
export declare function resolveStatusBadge(value: unknown, opts?: {
|
|
60
|
+
labels?: unknown;
|
|
61
|
+
variants?: unknown;
|
|
62
|
+
}): {
|
|
63
|
+
label: string;
|
|
64
|
+
variant: string;
|
|
65
|
+
};
|
|
66
|
+
/**
|
|
67
|
+
* Extract a `[lon, lat]` pair from a GeoJSON-ish Point value (the shape both a
|
|
68
|
+
* detail row's location FK and a MapPicker value use), or null when it isn't a
|
|
69
|
+
* usable point. Pure — the map widget decides how to render the result.
|
|
70
|
+
*/
|
|
71
|
+
export declare function geoPoint(value: unknown): [number, number] | null;
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Host-agnostic display transforms (#499 criterion A) — the pure value→text +
|
|
3
|
+
* status-badge + geo helpers the list/detail surfaces of BOTH hosts (the
|
|
4
|
+
* citizen portal AND the staff admin) share. Previously duplicated: the portal
|
|
5
|
+
* kept them in `portal/src/lib/renderer/{cells,columns}.ts`; the admin re-rolled
|
|
6
|
+
* equivalents inline (`renderCell`/`formatValue`). Lifting them here makes the
|
|
7
|
+
* value-redaction boundary one tested function instead of two drifting ones.
|
|
8
|
+
*
|
|
9
|
+
* Two host forks are parameters, not separate copies (the reconciliation #499
|
|
10
|
+
* called out before sharing):
|
|
11
|
+
* - object fallback: the portal REDACTS an unknown object (never leak a blob
|
|
12
|
+
* into the public DOM — a hard rule); the admin JSON-dumps it for operators.
|
|
13
|
+
* → `formatScalar(value, { objectFallback: "redact" | "json" })`, default
|
|
14
|
+
* "redact" (the safe default; a caller opts INTO dumping).
|
|
15
|
+
* - date detection: the portal detects timestamps by VALUE shape (the public
|
|
16
|
+
* schema strips created_at/updated_at, so type-driven detection misses
|
|
17
|
+
* them); the admin knows the field is a datetime from its schema TYPE.
|
|
18
|
+
* → `displayCell(value, locale, { treatAsDate })` — `treatAsDate` forces
|
|
19
|
+
* date formatting (admin, from schema type); otherwise value-shape
|
|
20
|
+
* detection (`isIsoTimestamp`) applies (portal).
|
|
21
|
+
*
|
|
22
|
+
* Everything here is PURE and dependency-free.
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
export type ObjectFallback = "redact" | "json";
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Pure value → display string. Scalars stringify; arrays join their non-empty
|
|
29
|
+
* formatted members; a disclosed relationship object ({id,label}|{name}|{title})
|
|
30
|
+
* renders its human label then its id. An UNKNOWN object renders per
|
|
31
|
+
* `objectFallback`: "redact" (default) → "" (never dumps internals — the public
|
|
32
|
+
* security boundary); "json" → pretty JSON (the admin operator view).
|
|
33
|
+
*/
|
|
34
|
+
export function formatScalar(
|
|
35
|
+
value: unknown,
|
|
36
|
+
opts: { objectFallback?: ObjectFallback } = {},
|
|
37
|
+
): string {
|
|
38
|
+
// `== null` catches both null and undefined.
|
|
39
|
+
if (value == null) return "";
|
|
40
|
+
if (typeof value === "string") return value;
|
|
41
|
+
if (typeof value === "number" || typeof value === "boolean") {
|
|
42
|
+
return String(value);
|
|
43
|
+
}
|
|
44
|
+
if (Array.isArray(value)) {
|
|
45
|
+
return value
|
|
46
|
+
.map((v) => formatScalar(v, opts))
|
|
47
|
+
.filter(Boolean)
|
|
48
|
+
.join(", ");
|
|
49
|
+
}
|
|
50
|
+
if (typeof value === "object") {
|
|
51
|
+
const o = value as Record<string, unknown>;
|
|
52
|
+
for (const k of ["label", "name", "title"]) {
|
|
53
|
+
const v = o[k];
|
|
54
|
+
if (typeof v === "string" && v) return v;
|
|
55
|
+
}
|
|
56
|
+
const id = o.id;
|
|
57
|
+
if (typeof id === "string" || typeof id === "number") return String(id);
|
|
58
|
+
// No human handle and no id → fall back per host policy.
|
|
59
|
+
if (opts.objectFallback === "json") {
|
|
60
|
+
try {
|
|
61
|
+
return JSON.stringify(value, null, 2);
|
|
62
|
+
} catch {
|
|
63
|
+
return "";
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
return "";
|
|
67
|
+
}
|
|
68
|
+
return "";
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const ISO_TIMESTAMP_RE =
|
|
72
|
+
/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|[+-]\d{2}:?\d{2})$/;
|
|
73
|
+
|
|
74
|
+
/** True for an ISO-8601 timestamp STRING (not a bare date). Value-shape
|
|
75
|
+
* detection — used where the schema can't tell us the field is a datetime. */
|
|
76
|
+
export function isIsoTimestamp(value: unknown): value is string {
|
|
77
|
+
return typeof value === "string" && ISO_TIMESTAMP_RE.test(value);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** Locale date for an ISO timestamp (date-only, UTC-stable so SSR == hydrate);
|
|
81
|
+
* the raw string when unparseable. */
|
|
82
|
+
export function formatTimestamp(value: string, locale: string): string {
|
|
83
|
+
const parsed = new Date(value);
|
|
84
|
+
if (Number.isNaN(parsed.getTime())) return value;
|
|
85
|
+
try {
|
|
86
|
+
return new Intl.DateTimeFormat(locale || "en", {
|
|
87
|
+
dateStyle: "medium",
|
|
88
|
+
timeZone: "UTC",
|
|
89
|
+
}).format(parsed);
|
|
90
|
+
} catch {
|
|
91
|
+
return value;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* The single display boundary for list cells / detail values. Formats a value
|
|
97
|
+
* as a locale date when it's a timestamp — either because `opts.treatAsDate`
|
|
98
|
+
* forces it (the admin, from the schema field type) or because the value itself
|
|
99
|
+
* looks like an ISO timestamp (the portal, value-shape). Everything else goes
|
|
100
|
+
* through `formatScalar` (with the host's object-fallback policy).
|
|
101
|
+
*/
|
|
102
|
+
export function displayCell(
|
|
103
|
+
value: unknown,
|
|
104
|
+
locale: string,
|
|
105
|
+
opts: { treatAsDate?: boolean; objectFallback?: ObjectFallback } = {},
|
|
106
|
+
): string {
|
|
107
|
+
if (opts.treatAsDate && typeof value === "string" && value) {
|
|
108
|
+
return formatTimestamp(value, locale);
|
|
109
|
+
}
|
|
110
|
+
if (isIsoTimestamp(value)) return formatTimestamp(value, locale);
|
|
111
|
+
return formatScalar(value, { objectFallback: opts.objectFallback });
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/** Operator copy for a status value (falls back to the raw value). `labels` is
|
|
115
|
+
* untrusted DATA — a non-object/array is ignored. */
|
|
116
|
+
export function statusLabel(value: unknown, labels: unknown): string {
|
|
117
|
+
const raw = formatScalar(value);
|
|
118
|
+
if (labels && typeof labels === "object" && !Array.isArray(labels)) {
|
|
119
|
+
const mapped = (labels as Record<string, unknown>)[raw];
|
|
120
|
+
if (typeof mapped === "string" && mapped) return mapped;
|
|
121
|
+
}
|
|
122
|
+
return raw;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/** DS Badge variant for a status value (neutral when unmapped). `variants` is
|
|
126
|
+
* untrusted DATA — a non-object/array is ignored. */
|
|
127
|
+
export function statusVariant(value: unknown, variants: unknown): string {
|
|
128
|
+
const raw = formatScalar(value);
|
|
129
|
+
if (variants && typeof variants === "object" && !Array.isArray(variants)) {
|
|
130
|
+
const mapped = (variants as Record<string, unknown>)[raw];
|
|
131
|
+
if (typeof mapped === "string" && mapped) return mapped;
|
|
132
|
+
}
|
|
133
|
+
return "neutral";
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/** Resolve a status value to a `{ label, variant }` pair in one call. */
|
|
137
|
+
export function resolveStatusBadge(
|
|
138
|
+
value: unknown,
|
|
139
|
+
opts: { labels?: unknown; variants?: unknown } = {},
|
|
140
|
+
): { label: string; variant: string } {
|
|
141
|
+
return {
|
|
142
|
+
label: statusLabel(value, opts.labels),
|
|
143
|
+
variant: statusVariant(value, opts.variants),
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Extract a `[lon, lat]` pair from a GeoJSON-ish Point value (the shape both a
|
|
149
|
+
* detail row's location FK and a MapPicker value use), or null when it isn't a
|
|
150
|
+
* usable point. Pure — the map widget decides how to render the result.
|
|
151
|
+
*/
|
|
152
|
+
export function geoPoint(value: unknown): [number, number] | null {
|
|
153
|
+
if (!value || typeof value !== "object") return null;
|
|
154
|
+
const coords = (value as { coordinates?: unknown }).coordinates;
|
|
155
|
+
if (!Array.isArray(coords) || coords.length < 2) return null;
|
|
156
|
+
const lon = coords[0];
|
|
157
|
+
const lat = coords[1];
|
|
158
|
+
if (typeof lon !== "number" || typeof lat !== "number") return null;
|
|
159
|
+
if (!Number.isFinite(lon) || !Number.isFinite(lat)) return null;
|
|
160
|
+
return [lon, lat];
|
|
161
|
+
}
|
|
@@ -22,7 +22,9 @@ import {
|
|
|
22
22
|
} from "./dispatch";
|
|
23
23
|
import type { Block, OntologySchema, WidgetKind, WidgetProps } from "./types";
|
|
24
24
|
|
|
25
|
+
import DonutChartWidget from "./DonutChartWidget.svelte";
|
|
25
26
|
import EChartWidget from "./EChartWidget.svelte";
|
|
27
|
+
import LineChartWidget from "./LineChartWidget.svelte";
|
|
26
28
|
import ResultsChartWidget from "./ResultsChartWidget.svelte";
|
|
27
29
|
import StatGridWidget from "./StatGridWidget.svelte";
|
|
28
30
|
|
|
@@ -69,6 +71,11 @@ const _entries: RegistryEntry<WidgetComponent>[] = [
|
|
|
69
71
|
byKind("stat-grid", "kpi", StatGridWidget),
|
|
70
72
|
byTypeOnKind("results-chart", "aggregate", ResultsChartWidget),
|
|
71
73
|
byTypeOnKind("chart", "aggregate", EChartWidget),
|
|
74
|
+
// #176 Tier 1 — chart KINDS as registry entries, so the authoring `type`
|
|
75
|
+
// picker is a real chart picker. All three ECharts kinds share the same
|
|
76
|
+
// aggregate data path + a11y data table; only the rendered geometry differs.
|
|
77
|
+
byTypeOnKind("line-chart", "aggregate", LineChartWidget),
|
|
78
|
+
byTypeOnKind("donut-chart", "aggregate", DonutChartWidget),
|
|
72
79
|
];
|
|
73
80
|
|
|
74
81
|
/**
|