@lieselthuriot/widget-renderer 0.0.0-stage → 0.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.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Liesel Thuriot
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,244 @@
1
- # Temporary Holding Version
1
+ # Widget Renderer
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Reusable Angular components for rendering compact, grid-based widgets with text, imagery, actions, charts, progress indicators, loading states, and sensitive-content controls.
4
+
5
+ The package is designed to receive a plain `WidgetContract` from an API or application. Consumers do not need to add editor-only element IDs or hydrate the data before rendering.
6
+
7
+ - **Live editor:** [widget-studio.liesel.dev](https://widget-studio.liesel.dev). Design a widget and copy its JSON.
8
+ - **Dashboard grid:** [`@lieselthuriot/widget-landscape`](https://www.npmjs.com/package/@lieselthuriot/widget-landscape)
9
+ - **C# contracts:** [`WidgetStudio.Contracts`](https://www.nuget.org/packages/WidgetStudio.Contracts) produces JSON in the same format from .NET.
10
+
11
+ ## Installation
12
+
13
+ ```bash
14
+ npm install @lieselthuriot/widget-renderer
15
+ ```
16
+
17
+ `@angular/common`, `@angular/core`, and `echarts` are peer dependencies. The package supports Angular 21 and 22 and ECharts 6.x. npm, pnpm, and Bun install missing peer dependencies automatically; with Yarn, also add `echarts`.
18
+
19
+ ## Features
20
+
21
+ - Render `mini`, `compact`, `tall`, `square`, `wide`, `mega`, `banner`, and `hero` widgets from a version 2 contract
22
+ - Place elements with configurable rows, columns, spans, and anchors
23
+ - Support text, images, buttons, dividers, pie, donut, funnel, gauge, bar, line, and progress elements
24
+ - Render charts with ECharts using SVG output
25
+ - Validate incoming data with structured paths and messages
26
+ - Normalize optional contract fields such as button defaults and text padding
27
+ - Blur sensitive backgrounds and elements until explicitly revealed
28
+ - Emit refresh and button-action events without making network requests
29
+ - Provide accessible names and text summaries for charts and progress bars
30
+ - Respect reduced-motion preferences for loading and active progress animations
31
+ - Localize renderer-owned accessible labels, with built-in German, French, and Dutch locales
32
+
33
+ ## Usage
34
+
35
+ Import the standalone `WidgetRenderer` component into an Angular component and bind a `WidgetContract`:
36
+
37
+ ```ts
38
+ import { Component } from '@angular/core';
39
+ import { ButtonElement, WidgetContract, WidgetRenderer } from '@lieselthuriot/widget-renderer';
40
+
41
+ @Component({
42
+ selector: 'app-dashboard-card',
43
+ imports: [WidgetRenderer],
44
+ template: `
45
+ <widget-renderer
46
+ [widget]="widget"
47
+ [darkMode]="darkMode"
48
+ [loading]="loading"
49
+ (refreshRequested)="refreshWidget()"
50
+ (actionRequested)="handleAction($event)"
51
+ />
52
+ `,
53
+ })
54
+ export class DashboardCard {
55
+ readonly darkMode = false;
56
+ readonly loading = false;
57
+
58
+ readonly widget: WidgetContract = {
59
+ version: 2,
60
+ id: 'hello-widget',
61
+ size: 'square',
62
+ rows: [{ unit: 'auto' }],
63
+ columns: [{ size: 1, unit: 'star' }],
64
+ background: {
65
+ color: '#172033',
66
+ sensitive: false,
67
+ repeat: false,
68
+ size: 'cover',
69
+ anchor: 'center',
70
+ },
71
+ refresh: { enabled: true, label: 'Refresh widget' },
72
+ elements: [
73
+ {
74
+ type: 'text',
75
+ grid: { row: 0, column: 0, rowSpan: 1, columnSpan: 1, anchor: 'center' },
76
+ sensitive: false,
77
+ text: 'Hello from Widget Renderer',
78
+ style: {
79
+ color: '#ffffff',
80
+ font: { size: 16, bold: true },
81
+ align: 'center',
82
+ wrap: true,
83
+ },
84
+ },
85
+ ],
86
+ };
87
+
88
+ refreshWidget(): void {
89
+ // Fetch updated data in the consuming application.
90
+ }
91
+
92
+ handleAction(event: { element: ButtonElement; event: Event }): void {
93
+ // Handle the configured button request in the consuming application.
94
+ console.log(event);
95
+ }
96
+ }
97
+ ```
98
+
99
+ > **Important:** `WidgetRenderer` emits `refreshRequested` and `actionRequested`. It does not call the configured button URL or perform refresh requests itself.
100
+
101
+ ### Loading state
102
+
103
+ Set `loading` to `true` for the default square skeleton, or to a widget size such as `'wide'` for a matching skeleton:
104
+
105
+ ```html
106
+ <widget-renderer [loading]="'wide'" [darkMode]="true" />
107
+ ```
108
+
109
+ ### Dark mode
110
+
111
+ Pass `darkMode` to style the loading state for a dark surface. The rendered widget itself uses the colors provided by its contract, so applications can control the surrounding theme and widget palette independently.
112
+
113
+ ### Outputs
114
+
115
+ `refreshRequested` is emitted when the refresh control is clicked. `actionRequested` is emitted with the clicked `ButtonElement` and DOM event:
116
+
117
+ ```ts
118
+ import { ButtonElement } from '@lieselthuriot/widget-renderer';
119
+
120
+ handleAction(event: { element: ButtonElement; event: Event }): void {
121
+ event.event.preventDefault();
122
+ const { method, url, body, requestRefresh } = event.element;
123
+ // Perform the application-specific request here.
124
+ console.log(method, url, body, requestRefresh);
125
+ }
126
+ ```
127
+
128
+ ### Localization
129
+
130
+ Renderer-owned accessible labels use English defaults and can be localized with the optional `locale` input. The input accepts partial nested overrides, so you can provide a complete locale or change only the values you need. Widget content, labels, image alternative text, refresh labels, and validation diagnostics remain consumer-provided.
131
+
132
+ ```ts
133
+ import {
134
+ germanWidgetRendererLocale,
135
+ WidgetRendererLocaleOverrides,
136
+ } from '@lieselthuriot/widget-renderer';
137
+
138
+ readonly rendererLocale: WidgetRendererLocaleOverrides = {
139
+ ...germanWidgetRendererLocale,
140
+ };
141
+ ```
142
+
143
+ Built-in `germanWidgetRendererLocale`, `frenchWidgetRendererLocale`, and `dutchWidgetRendererLocale` values are available. For a small customization, override only the required values:
144
+
145
+ ```ts
146
+ import { WidgetRendererLocaleOverrides } from '@lieselthuriot/widget-renderer';
147
+
148
+ readonly rendererLocale: WidgetRendererLocaleOverrides = {
149
+ aria: {
150
+ showSensitiveData: 'Afficher les données privées',
151
+ chart: {
152
+ noDataSummary: (title) => `${title ? `${title} : ` : ''}aucune donnée.`,
153
+ },
154
+ },
155
+ };
156
+ ```
157
+
158
+ ```html
159
+ <widget-renderer [widget]="widget" [locale]="rendererLocale" />
160
+ ```
161
+
162
+ The `aria` locale covers the loading status, toolbar, sensitive-content controls, image failure fallback, chart and progress accessible names, generated chart summaries, and progress segment labels. Locale changes update the rendered labels without changing widget-provided content.
163
+
164
+ ## Widget contract
165
+
166
+ Every input must use `version: 2` and include:
167
+
168
+ | Field | Description |
169
+ | ------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------- |
170
+ | `id` | A stable identifier for the widget |
171
+ | `size` | `mini`, `compact`, `square`, `wide`, `mega`, `banner`, `hero`, or `tall`; dimensions include the 8px layout gap |
172
+ | `rows` | Non-empty grid tracks using `pixel`, `auto`, or `star` units; `size` is required for `pixel` and `star` tracks and must be omitted for `auto` tracks |
173
+ | `columns` | Non-empty grid tracks using `pixel`, `auto`, or `star` units; `size` is required for `pixel` and `star` tracks and must be omitted for `auto` tracks |
174
+ | `background` | Color, optional image, sizing, position, repeat, overlay, and sensitivity settings |
175
+ | `refresh` | Whether to show a refresh action and its accessible label |
176
+ | `elements` | An array of supported widget elements |
177
+
178
+ Element positions are zero-based. For example, `row: 0` and `column: 0` place an element in the first grid cell.
179
+
180
+ ### Supported elements
181
+
182
+ - `text`: formatted text with alignment, wrapping, font styling, color, background, radius, and padding
183
+ - `image`: remote or local image with alternative text, fit mode, radius, and overlay color
184
+ - `button`: `get` or `post` action metadata, optional body, solid or outline styling, and hover behavior
185
+ - `divider`: horizontal or vertical separator
186
+ - `pie`, `donut`, and `funnel`: labeled data points with optional legends and per-point colors
187
+ - `gauge`: a value within a minimum and maximum range
188
+ - `bar` and `line`: labeled series data with axis and line styling
189
+ - `progress`: a range-based progress bar with label, suffix, legend, and active animation options
190
+
191
+ Editor-only `id` fields are not required. The renderer accepts `WidgetContract` data and normalizes optional button fields and text padding before rendering.
192
+
193
+ ### Sensitive content
194
+
195
+ Set `sensitive: true` on the background or an element to blur it and remove hidden content from the accessible tree. The renderer provides a local show/hide control when sensitive content is present.
196
+
197
+ > **Warning:** Sensitive-content handling is a display and privacy feature, not encryption. Sensitive values remain in the JavaScript object supplied by the consuming application.
198
+
199
+ ## Validation and normalization
200
+
201
+ Use the exported helpers when data enters your application:
202
+
203
+ ```ts
204
+ import {
205
+ normalizeWidgetInput,
206
+ parseWidgetInput,
207
+ safeParseWidgetInput,
208
+ } from '@lieselthuriot/widget-renderer';
209
+
210
+ const result = safeParseWidgetInput(payload);
211
+
212
+ if (!result.success) {
213
+ console.error(result.issues);
214
+ } else {
215
+ const widget = normalizeWidgetInput(result.data);
216
+ }
217
+
218
+ // Throws WidgetValidationError when the payload is invalid.
219
+ const parsed = parseWidgetInput(payload);
220
+ ```
221
+
222
+ `WidgetValidationIssue` entries contain a `path` such as `elements[0].style.font.size` and a human-readable `message`. Invalid input supplied to `WidgetRenderer` produces a safe empty widget and exposes issues through `validationIssues`. `isWidgetContract` and `isWidget` are also available as type guards.
223
+
224
+ ## Public API
225
+
226
+ Import everything from `@lieselthuriot/widget-renderer`; internal file paths are not part of the public API. The entry point exports:
227
+
228
+ - `WidgetRenderer`, the standalone rendering component, and its `WidgetLoading` input type. Element and chart implementation components are internal.
229
+ - Widget model types, including `WidgetContract`, `WidgetInput`, `Widget`, and the element types
230
+ - `normalizeWidgetInput`
231
+ - `safeParseWidgetInput`, `parseWidgetInput`, `isWidgetContract`, `isWidget`, `WidgetValidationError`, and the validation result types
232
+ - Grid layout helpers and widget dimensions, such as `widgetWidth`, `widgetHeight`, `gridTemplate`, and `elementGridStyle`
233
+ - Locales: `defaultWidgetRendererLocale`, `germanWidgetRendererLocale`, `frenchWidgetRendererLocale`, `dutchWidgetRendererLocale`, `WidgetRendererLocale`, and `WidgetRendererLocaleOverrides`
234
+ - `chartAriaLabel` and `chartAccessibilitySummary`
235
+
236
+ The complete export list is maintained in [`src/public-api.ts`](https://github.com/LieselThuriot/widget-studio/blob/main/projects/widget-renderer/src/public-api.ts).
237
+
238
+ ## Contributing
239
+
240
+ The package is developed in the [widget-studio repository](https://github.com/LieselThuriot/widget-studio), together with the editor, the landscape package, and the C# contracts. See the repository README for build, test, and release instructions. Report problems in the [issue tracker](https://github.com/LieselThuriot/widget-studio/issues).
241
+
242
+ ## License
243
+
244
+ [MIT](https://github.com/LieselThuriot/widget-studio/blob/main/LICENSE)
@@ -0,0 +1,178 @@
1
+ import * as i0 from "@angular/core";
2
+ import { Injectable, signal } from "@angular/core";
3
+ const chart = {
4
+ progressAriaLabel: (title, value, range) => `${title || "Progress"}, ${value} of ${range}.`,
5
+ gaugeAriaLabel: (title, value, max) => `${title || "Gauge"}, ${value} of ${max}.`,
6
+ chartAriaLabel: (title, chartType, pointCount) => `${title || chartType} chart, ${pointCount} data points.`,
7
+ progressSummary: (title, value, min, max, breakdown) => `${title || "Progress"}: ${value} of ${max}; range ${min} to ${max}.${breakdown}`,
8
+ gaugeSummary: (title, value, max, percentage) => `${title || "Gauge"}: value ${value} of ${max}, ${percentage} percent.`,
9
+ dataSummary: (title, points) => `${title ? `${title}: ` : ""}${points}.`,
10
+ noDataSummary: (title) => `${title ? `${title}: ` : ""}no data.`,
11
+ noSegmentsSummary: " No segments.",
12
+ morePoints: (count) => `${count} more points.`,
13
+ progressSegmentLabel: (label, value) => `${label}: ${value}`,
14
+ progressBreakdown: (label, start, end) => `${label}: ${start} to ${end}`
15
+ };
16
+ const defaultWidgetRendererLocale = { aria: {
17
+ toolbarActions: "Widget actions",
18
+ loading: "Widget loading",
19
+ showSensitiveData: "Show sensitive data",
20
+ hideSensitiveData: "Hide sensitive data",
21
+ imageUnavailable: "Image unavailable",
22
+ chart
23
+ } };
24
+ function locale(aria, chartLocale) {
25
+ return { aria: {
26
+ ...aria,
27
+ chart: chartLocale
28
+ } };
29
+ }
30
+ const germanWidgetRendererLocale = locale({
31
+ toolbarActions: "Widget-Aktionen",
32
+ loading: "Widget wird geladen",
33
+ showSensitiveData: "Sensible Daten anzeigen",
34
+ hideSensitiveData: "Sensible Daten ausblenden",
35
+ imageUnavailable: "Bild nicht verfügbar"
36
+ }, {
37
+ ...chart,
38
+ progressAriaLabel: (title, value, range) => `${title || "Fortschritt"}, ${value} von ${range}.`,
39
+ gaugeAriaLabel: (title, value, max) => `${title || "Messwert"}, ${value} von ${max}.`,
40
+ chartAriaLabel: (title, type, count) => `${title || `${type}-Diagramm`}, ${count} Datenpunkte.`,
41
+ progressSummary: (title, value, min, max, breakdown) => `${title || "Fortschritt"}: ${value} von ${max}; Bereich ${min} bis ${max}.${breakdown}`,
42
+ gaugeSummary: (title, value, max, percentage) => `${title || "Messwert"}: Wert ${value} von ${max}, ${percentage} Prozent.`,
43
+ noDataSummary: (title) => `${title ? `${title}: ` : ""}keine Daten.`,
44
+ noSegmentsSummary: " Keine Segmente.",
45
+ morePoints: (count) => `${count} weitere Datenpunkte.`,
46
+ progressSegmentLabel: (label, value) => `${label}: ${value}`,
47
+ progressBreakdown: (label, start, end) => `${label}: ${start} bis ${end}`
48
+ });
49
+ const frenchWidgetRendererLocale = locale({
50
+ toolbarActions: "Actions du widget",
51
+ loading: "Chargement du widget",
52
+ showSensitiveData: "Afficher les données sensibles",
53
+ hideSensitiveData: "Masquer les données sensibles",
54
+ imageUnavailable: "Image indisponible"
55
+ }, {
56
+ ...chart,
57
+ progressAriaLabel: (title, value, range) => `${title || "Progression"}, ${value} sur ${range}.`,
58
+ gaugeAriaLabel: (title, value, max) => `${title || "Jauge"}, ${value} sur ${max}.`,
59
+ chartAriaLabel: (title, type, count) => `${title || `Graphique ${type}`}, ${count} points de données.`,
60
+ progressSummary: (title, value, min, max, breakdown) => `${title || "Progression"} : ${value} sur ${max} ; plage de ${min} à ${max}.${breakdown}`,
61
+ gaugeSummary: (title, value, max, percentage) => `${title || "Jauge"} : valeur ${value} sur ${max}, ${percentage} pour cent.`,
62
+ noDataSummary: (title) => `${title ? `${title} : ` : ""}aucune donnée.`,
63
+ noSegmentsSummary: " Aucun segment.",
64
+ morePoints: (count) => `${count} points de données supplémentaires.`,
65
+ progressSegmentLabel: (label, value) => `${label} : ${value}`,
66
+ progressBreakdown: (label, start, end) => `${label} : de ${start} à ${end}`
67
+ });
68
+ const dutchWidgetRendererLocale = locale({
69
+ toolbarActions: "Widgetacties",
70
+ loading: "Widget wordt geladen",
71
+ showSensitiveData: "Gevoelige gegevens tonen",
72
+ hideSensitiveData: "Gevoelige gegevens verbergen",
73
+ imageUnavailable: "Afbeelding niet beschikbaar"
74
+ }, {
75
+ ...chart,
76
+ progressAriaLabel: (title, value, range) => `${title || "Voortgang"}, ${value} van ${range}.`,
77
+ gaugeAriaLabel: (title, value, max) => `${title || "Meter"}, ${value} van ${max}.`,
78
+ chartAriaLabel: (title, type, count) => `${title || `${type}-grafiek`}, ${count} gegevenspunten.`,
79
+ progressSummary: (title, value, min, max, breakdown) => `${title || "Voortgang"}: ${value} van ${max}; bereik ${min} tot ${max}.${breakdown}`,
80
+ gaugeSummary: (title, value, max, percentage) => `${title || "Meter"}: waarde ${value} van ${max}, ${percentage} procent.`,
81
+ noDataSummary: (title) => `${title ? `${title}: ` : ""}geen gegevens.`,
82
+ noSegmentsSummary: " Geen segmenten.",
83
+ morePoints: (count) => `${count} extra gegevenspunten.`,
84
+ progressSegmentLabel: (label, value) => `${label}: ${value}`,
85
+ progressBreakdown: (label, start, end) => `${label}: ${start} tot ${end}`
86
+ });
87
+ var WidgetTooltip = class WidgetTooltip {
88
+ progress = signal(null, ...ngDevMode ? [{ debugName: "progress" }] : /* istanbul ignore next */ []);
89
+ overlay = signal(null, ...ngDevMode ? [{ debugName: "overlay" }] : /* istanbul ignore next */ []);
90
+ progressOwner = null;
91
+ setOverlay(overlay) {
92
+ this.overlay.set(overlay);
93
+ }
94
+ overlayElement() {
95
+ return this.overlay();
96
+ }
97
+ showProgress(owner, state) {
98
+ this.progressOwner = owner;
99
+ this.progress.set(state);
100
+ }
101
+ hideProgress(owner) {
102
+ if (this.progressOwner !== owner) return;
103
+ this.progressOwner = null;
104
+ this.progress.set(null);
105
+ }
106
+ clear() {
107
+ this.progressOwner = null;
108
+ this.progress.set(null);
109
+ this.overlay.set(null);
110
+ }
111
+ static ɵfac = i0.ɵɵngDeclareFactory({
112
+ minVersion: "12.0.0",
113
+ version: "22.2.3",
114
+ ngImport: i0,
115
+ type: WidgetTooltip,
116
+ deps: [],
117
+ target: i0.ɵɵFactoryTarget.Injectable
118
+ });
119
+ static ɵprov = i0.ɵɵngDeclareInjectable({
120
+ minVersion: "12.0.0",
121
+ version: "22.2.3",
122
+ ngImport: i0,
123
+ type: WidgetTooltip
124
+ });
125
+ };
126
+ i0.ɵɵngDeclareClassMetadata({
127
+ minVersion: "12.0.0",
128
+ version: "22.2.3",
129
+ ngImport: i0,
130
+ type: WidgetTooltip,
131
+ decorators: [{ type: Injectable }]
132
+ });
133
+ const MAX_POINTS = 20;
134
+ function number(value) {
135
+ return Number.isInteger(value) ? `${value}` : value.toFixed(2).replace(/0+$/, "").replace(/\.$/, "");
136
+ }
137
+ function points(data, locale) {
138
+ const visible = data.slice(0, MAX_POINTS).map((point) => `${point.label}: ${number(point.value)}`);
139
+ const remaining = data.length - visible.length;
140
+ return remaining > 0 ? `${visible.join("; ")}; ${locale.morePoints(remaining)}` : visible.join("; ");
141
+ }
142
+ function progressSegments(element) {
143
+ let position = element.data.min;
144
+ return element.data.items.flatMap((item) => {
145
+ const start = position;
146
+ position += item.value;
147
+ return item.kind === "segment" ? [{
148
+ segment: item,
149
+ start,
150
+ end: position
151
+ }] : [];
152
+ });
153
+ }
154
+ function progressValue(element) {
155
+ return element.data.items.reduce((total, item) => total + (item.kind === "segment" ? item.value : 0), 0);
156
+ }
157
+ function chartAriaLabel(element, locale = defaultWidgetRendererLocale.aria.chart) {
158
+ if (element.type === "progress") return locale.progressAriaLabel(element.title, number(progressValue(element)), number(element.data.max - element.data.min));
159
+ if (element.type === "gauge") return locale.gaugeAriaLabel(element.title, number(element.data.value), number(element.data.max));
160
+ return locale.chartAriaLabel(element.title, element.type, element.data.length);
161
+ }
162
+ function chartAccessibilitySummary(element, locale = defaultWidgetRendererLocale.aria.chart) {
163
+ if (element.type === "progress") {
164
+ const segments = progressSegments(element);
165
+ const breakdown = segments.length ? ` ${segments.map(({ segment, start, end }) => locale.progressBreakdown(segment.label, number(start), number(end))).join("; ")}.` : locale.noSegmentsSummary;
166
+ return locale.progressSummary(element.title, number(progressValue(element)), number(element.data.min), number(element.data.max), breakdown);
167
+ }
168
+ if (element.type === "gauge") {
169
+ const range = element.data.max - element.data.min;
170
+ const percentage = Math.max(0, Math.min(100, (element.data.value - element.data.min) / range * 100));
171
+ return locale.gaugeSummary(element.title, number(element.data.value), number(element.data.max), number(percentage));
172
+ }
173
+ if (!element.data.length) return locale.noDataSummary(element.title);
174
+ return locale.dataSummary(element.title, points(element.data, locale));
175
+ }
176
+ export { dutchWidgetRendererLocale as a, defaultWidgetRendererLocale as i, chartAriaLabel as n, frenchWidgetRendererLocale as o, WidgetTooltip as r, germanWidgetRendererLocale as s, chartAccessibilitySummary as t };
177
+
178
+ //# sourceMappingURL=lieselthuriot-widget-renderer-chart-accessibility-CXEeXwdH.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"lieselthuriot-widget-renderer-chart-accessibility-CXEeXwdH.mjs","names":[],"sources":["../../../projects/widget-renderer/src/lib/widget-locale.ts","../../../projects/widget-renderer/src/lib/widget-tooltip.ts","../../../projects/widget-renderer/src/lib/chart-accessibility.ts"],"sourcesContent":["export interface WidgetRendererChartLocale {\n progressAriaLabel: (title: string | undefined, value: string, range: string) => string;\n gaugeAriaLabel: (title: string | undefined, value: string, max: string) => string;\n chartAriaLabel: (title: string | undefined, chartType: string, pointCount: number) => string;\n progressSummary: (\n title: string | undefined,\n value: string,\n min: string,\n max: string,\n breakdown: string,\n ) => string;\n gaugeSummary: (\n title: string | undefined,\n value: string,\n max: string,\n percentage: string,\n ) => string;\n dataSummary: (title: string | undefined, points: string) => string;\n noDataSummary: (title: string | undefined) => string;\n noSegmentsSummary: string;\n morePoints: (count: number) => string;\n progressSegmentLabel: (label: string, value: string) => string;\n progressBreakdown: (label: string, start: string, end: string) => string;\n}\n\nexport interface WidgetRendererAriaLocale {\n toolbarActions: string;\n loading: string;\n showSensitiveData: string;\n hideSensitiveData: string;\n imageUnavailable: string;\n chart: WidgetRendererChartLocale;\n}\n\nexport interface WidgetRendererLocale {\n aria: WidgetRendererAriaLocale;\n}\n\nexport interface WidgetRendererLocaleOverrides {\n aria?: Partial<Omit<WidgetRendererAriaLocale, 'chart'>> & {\n chart?: Partial<WidgetRendererChartLocale>;\n };\n}\n\nconst chart = {\n progressAriaLabel: (title: string | undefined, value: string, range: string) =>\n `${title || 'Progress'}, ${value} of ${range}.`,\n gaugeAriaLabel: (title: string | undefined, value: string, max: string) =>\n `${title || 'Gauge'}, ${value} of ${max}.`,\n chartAriaLabel: (title: string | undefined, chartType: string, pointCount: number) =>\n `${title || chartType} chart, ${pointCount} data points.`,\n progressSummary: (\n title: string | undefined,\n value: string,\n min: string,\n max: string,\n breakdown: string,\n ) => `${title || 'Progress'}: ${value} of ${max}; range ${min} to ${max}.${breakdown}`,\n gaugeSummary: (title: string | undefined, value: string, max: string, percentage: string) =>\n `${title || 'Gauge'}: value ${value} of ${max}, ${percentage} percent.`,\n dataSummary: (title: string | undefined, points: string) =>\n `${title ? `${title}: ` : ''}${points}.`,\n noDataSummary: (title: string | undefined) => `${title ? `${title}: ` : ''}no data.`,\n noSegmentsSummary: ' No segments.',\n morePoints: (count: number) => `${count} more points.`,\n progressSegmentLabel: (label: string, value: string) => `${label}: ${value}`,\n progressBreakdown: (label: string, start: string, end: string) => `${label}: ${start} to ${end}`,\n} satisfies WidgetRendererChartLocale;\n\nexport const defaultWidgetRendererLocale: WidgetRendererLocale = {\n aria: {\n toolbarActions: 'Widget actions',\n loading: 'Widget loading',\n showSensitiveData: 'Show sensitive data',\n hideSensitiveData: 'Hide sensitive data',\n imageUnavailable: 'Image unavailable',\n chart,\n },\n};\n\nfunction locale(\n aria: Omit<WidgetRendererAriaLocale, 'chart'>,\n chartLocale: WidgetRendererChartLocale,\n): WidgetRendererLocale {\n return { aria: { ...aria, chart: chartLocale } };\n}\n\nexport const germanWidgetRendererLocale = locale(\n {\n toolbarActions: 'Widget-Aktionen',\n loading: 'Widget wird geladen',\n showSensitiveData: 'Sensible Daten anzeigen',\n hideSensitiveData: 'Sensible Daten ausblenden',\n imageUnavailable: 'Bild nicht verfügbar',\n },\n {\n ...chart,\n progressAriaLabel: (title, value, range) => `${title || 'Fortschritt'}, ${value} von ${range}.`,\n gaugeAriaLabel: (title, value, max) => `${title || 'Messwert'}, ${value} von ${max}.`,\n chartAriaLabel: (title, type, count) => `${title || `${type}-Diagramm`}, ${count} Datenpunkte.`,\n progressSummary: (title, value, min, max, breakdown) =>\n `${title || 'Fortschritt'}: ${value} von ${max}; Bereich ${min} bis ${max}.${breakdown}`,\n gaugeSummary: (title, value, max, percentage) =>\n `${title || 'Messwert'}: Wert ${value} von ${max}, ${percentage} Prozent.`,\n noDataSummary: (title) => `${title ? `${title}: ` : ''}keine Daten.`,\n noSegmentsSummary: ' Keine Segmente.',\n morePoints: (count) => `${count} weitere Datenpunkte.`,\n progressSegmentLabel: (label, value) => `${label}: ${value}`,\n progressBreakdown: (label, start, end) => `${label}: ${start} bis ${end}`,\n },\n);\n\nexport const frenchWidgetRendererLocale = locale(\n {\n toolbarActions: 'Actions du widget',\n loading: 'Chargement du widget',\n showSensitiveData: 'Afficher les données sensibles',\n hideSensitiveData: 'Masquer les données sensibles',\n imageUnavailable: 'Image indisponible',\n },\n {\n ...chart,\n progressAriaLabel: (title, value, range) => `${title || 'Progression'}, ${value} sur ${range}.`,\n gaugeAriaLabel: (title, value, max) => `${title || 'Jauge'}, ${value} sur ${max}.`,\n chartAriaLabel: (title, type, count) =>\n `${title || `Graphique ${type}`}, ${count} points de données.`,\n progressSummary: (title, value, min, max, breakdown) =>\n `${title || 'Progression'} : ${value} sur ${max} ; plage de ${min} à ${max}.${breakdown}`,\n gaugeSummary: (title, value, max, percentage) =>\n `${title || 'Jauge'} : valeur ${value} sur ${max}, ${percentage} pour cent.`,\n noDataSummary: (title) => `${title ? `${title} : ` : ''}aucune donnée.`,\n noSegmentsSummary: ' Aucun segment.',\n morePoints: (count) => `${count} points de données supplémentaires.`,\n progressSegmentLabel: (label, value) => `${label} : ${value}`,\n progressBreakdown: (label, start, end) => `${label} : de ${start} à ${end}`,\n },\n);\n\nexport const dutchWidgetRendererLocale = locale(\n {\n toolbarActions: 'Widgetacties',\n loading: 'Widget wordt geladen',\n showSensitiveData: 'Gevoelige gegevens tonen',\n hideSensitiveData: 'Gevoelige gegevens verbergen',\n imageUnavailable: 'Afbeelding niet beschikbaar',\n },\n {\n ...chart,\n progressAriaLabel: (title, value, range) => `${title || 'Voortgang'}, ${value} van ${range}.`,\n gaugeAriaLabel: (title, value, max) => `${title || 'Meter'}, ${value} van ${max}.`,\n chartAriaLabel: (title, type, count) =>\n `${title || `${type}-grafiek`}, ${count} gegevenspunten.`,\n progressSummary: (title, value, min, max, breakdown) =>\n `${title || 'Voortgang'}: ${value} van ${max}; bereik ${min} tot ${max}.${breakdown}`,\n gaugeSummary: (title, value, max, percentage) =>\n `${title || 'Meter'}: waarde ${value} van ${max}, ${percentage} procent.`,\n noDataSummary: (title) => `${title ? `${title}: ` : ''}geen gegevens.`,\n noSegmentsSummary: ' Geen segmenten.',\n morePoints: (count) => `${count} extra gegevenspunten.`,\n progressSegmentLabel: (label, value) => `${label}: ${value}`,\n progressBreakdown: (label, start, end) => `${label}: ${start} tot ${end}`,\n },\n);\n","import { Injectable, signal } from '@angular/core';\n\nexport interface ProgressTooltipState {\n x: number;\n y: number;\n label: string;\n value: number;\n color?: string;\n}\n\n@Injectable()\nexport class WidgetTooltip {\n readonly progress = signal<ProgressTooltipState | null>(null);\n private readonly overlay = signal<HTMLElement | null>(null);\n private progressOwner: object | null = null;\n\n setOverlay(overlay: HTMLElement): void {\n this.overlay.set(overlay);\n }\n\n overlayElement(): HTMLElement | null {\n return this.overlay();\n }\n\n showProgress(owner: object, state: ProgressTooltipState): void {\n this.progressOwner = owner;\n this.progress.set(state);\n }\n\n hideProgress(owner: object): void {\n if (this.progressOwner !== owner) return;\n this.progressOwner = null;\n this.progress.set(null);\n }\n\n clear(): void {\n this.progressOwner = null;\n this.progress.set(null);\n this.overlay.set(null);\n }\n}\n","import { ChartElement, ProgressElement, ProgressSegment } from './widget.model';\nimport { defaultWidgetRendererLocale, WidgetRendererChartLocale } from './widget-locale';\n\nconst MAX_POINTS = 20;\n\nfunction number(value: number): string {\n return Number.isInteger(value)\n ? `${value}`\n : value.toFixed(2).replace(/0+$/, '').replace(/\\.$/, '');\n}\n\nfunction points(\n data: { label: string; value: number }[],\n locale: WidgetRendererChartLocale,\n): string {\n const visible = data\n .slice(0, MAX_POINTS)\n .map((point) => `${point.label}: ${number(point.value)}`);\n const remaining = data.length - visible.length;\n return remaining > 0\n ? `${visible.join('; ')}; ${locale.morePoints(remaining)}`\n : visible.join('; ');\n}\n\nfunction progressSegments(\n element: ProgressElement,\n): { segment: ProgressSegment; start: number; end: number }[] {\n let position = element.data.min;\n return element.data.items.flatMap((item) => {\n const start = position;\n position += item.value;\n return item.kind === 'segment' ? [{ segment: item, start, end: position }] : [];\n });\n}\n\nfunction progressValue(element: ProgressElement): number {\n return element.data.items.reduce(\n (total, item) => total + (item.kind === 'segment' ? item.value : 0),\n 0,\n );\n}\n\nexport function chartAriaLabel(\n element: ChartElement | ProgressElement,\n locale = defaultWidgetRendererLocale.aria.chart,\n): string {\n if (element.type === 'progress') {\n return locale.progressAriaLabel(\n element.title,\n number(progressValue(element)),\n number(element.data.max - element.data.min),\n );\n }\n if (element.type === 'gauge') {\n return locale.gaugeAriaLabel(\n element.title,\n number(element.data.value),\n number(element.data.max),\n );\n }\n return locale.chartAriaLabel(element.title, element.type, element.data.length);\n}\n\nexport function chartAccessibilitySummary(\n element: ChartElement | ProgressElement,\n locale = defaultWidgetRendererLocale.aria.chart,\n): string {\n if (element.type === 'progress') {\n const segments = progressSegments(element);\n const breakdown = segments.length\n ? ` ${segments.map(({ segment, start, end }) => locale.progressBreakdown(segment.label, number(start), number(end))).join('; ')}.`\n : locale.noSegmentsSummary;\n return locale.progressSummary(\n element.title,\n number(progressValue(element)),\n number(element.data.min),\n number(element.data.max),\n breakdown,\n );\n }\n if (element.type === 'gauge') {\n const range = element.data.max - element.data.min;\n const percentage = Math.max(\n 0,\n Math.min(100, ((element.data.value - element.data.min) / range) * 100),\n );\n return locale.gaugeSummary(\n element.title,\n number(element.data.value),\n number(element.data.max),\n number(percentage),\n );\n }\n if (!element.data.length) return locale.noDataSummary(element.title);\n return locale.dataSummary(element.title, points(element.data, locale));\n}\n"],"mappings":"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"}