@lieselthuriot/widget-renderer 0.1.0 → 0.2.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/README.md CHANGED
@@ -1,244 +1,299 @@
1
- # Widget Renderer
2
-
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)
1
+ # Widget Renderer
2
+
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` and `@angular/core` are peer dependencies. The package supports Angular 21 and 22. Charts are drawn as SVG by the package itself, with [`d3-array`](https://www.npmjs.com/package/d3-array) and [`d3-shape`](https://www.npmjs.com/package/d3-shape) installed automatically as regular dependencies for the chart geometry.
18
+
19
+ ### Upgrading from 0.1
20
+
21
+ Version 0.2 no longer uses ECharts. Remove `echarts` from your dependencies if nothing else in your application needs it. The widget contract is unchanged, but charts look slightly different:
22
+
23
+ - Pie, donut, gauge, and funnel charts stack their title, chart, and legend, so the chart uses the available space and never overlaps the legend.
24
+ - Bar and line axes, ticks, and labels are laid out by the renderer; crowded category labels are thinned out automatically.
25
+ - Clicking a legend entry no longer hides the matching data point.
26
+ - Every chart can be focused with the keyboard. Arrow keys, Home, and End move between data points, Escape hides the tooltip, and the selected point is announced to screen readers.
27
+ - The keyboard focus ring can be themed with the `--widget-renderer-focus-ring` and `--widget-renderer-focus-ring-contrast` CSS custom properties.
28
+ - Custom locales can set the new `dataPointLabel` and `gaugeValueLabel` chart entries; English values are used when they are omitted from overrides.
29
+
30
+ ## Features
31
+
32
+ - Render `mini`, `compact`, `tall`, `square`, `wide`, `mega`, `banner`, and `hero` widgets from a version 2 contract
33
+ - Place elements with configurable rows, columns, spans, and anchors
34
+ - Support text, images, buttons, dividers, pie, donut, funnel, gauge, bar, line, and progress elements
35
+ - Render charts as lightweight, keyboard-accessible SVG without a charting library
36
+ - Validate incoming data with structured paths and messages
37
+ - Normalize optional contract fields such as button defaults and text padding
38
+ - Blur sensitive backgrounds and elements until explicitly revealed
39
+ - Emit refresh and button-action events without making network requests
40
+ - Provide accessible names and text summaries for charts and progress bars
41
+ - Respect reduced-motion preferences for loading and active progress animations
42
+ - Localize renderer-owned accessible labels, with built-in German, French, and Dutch locales
43
+
44
+ ## Usage
45
+
46
+ Import the standalone `WidgetRenderer` component into an Angular component and bind a `WidgetContract`:
47
+
48
+ ```ts
49
+ import { Component } from '@angular/core';
50
+ import { ButtonElement, WidgetContract, WidgetRenderer } from '@lieselthuriot/widget-renderer';
51
+
52
+ @Component({
53
+ selector: 'app-dashboard-card',
54
+ imports: [WidgetRenderer],
55
+ template: `
56
+ <widget-renderer
57
+ [widget]="widget"
58
+ [darkMode]="darkMode"
59
+ [loading]="loading"
60
+ (refreshRequested)="refreshWidget()"
61
+ (actionRequested)="handleAction($event)"
62
+ />
63
+ `,
64
+ })
65
+ export class DashboardCard {
66
+ readonly darkMode = false;
67
+ readonly loading = false;
68
+
69
+ readonly widget: WidgetContract = {
70
+ version: 2,
71
+ id: 'hello-widget',
72
+ size: 'square',
73
+ rows: [{ unit: 'auto' }],
74
+ columns: [{ size: 1, unit: 'star' }],
75
+ background: {
76
+ color: '#172033',
77
+ sensitive: false,
78
+ repeat: false,
79
+ size: 'cover',
80
+ anchor: 'center',
81
+ },
82
+ refresh: { enabled: true, label: 'Refresh widget' },
83
+ elements: [
84
+ {
85
+ type: 'text',
86
+ grid: { row: 0, column: 0, rowSpan: 1, columnSpan: 1, anchor: 'center' },
87
+ sensitive: false,
88
+ text: 'Hello from Widget Renderer',
89
+ style: {
90
+ color: '#ffffff',
91
+ font: { size: 16, bold: true },
92
+ align: 'center',
93
+ wrap: true,
94
+ },
95
+ },
96
+ ],
97
+ };
98
+
99
+ refreshWidget(): void {
100
+ // Fetch updated data in the consuming application.
101
+ }
102
+
103
+ handleAction(event: { element: ButtonElement; event: Event }): void {
104
+ // Handle the configured button request in the consuming application.
105
+ console.log(event);
106
+ }
107
+ }
108
+ ```
109
+
110
+ > **Important:** `WidgetRenderer` emits `refreshRequested` and `actionRequested`. It does not call the configured button URL or perform refresh requests itself.
111
+
112
+ ### Loading state
113
+
114
+ Set `loading` to `true` for the default square skeleton, or to a widget size such as `'wide'` for a matching skeleton:
115
+
116
+ ```html
117
+ <widget-renderer [loading]="'wide'" [darkMode]="true" />
118
+ ```
119
+
120
+ ### Dark mode
121
+
122
+ 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.
123
+
124
+ ### Outputs
125
+
126
+ `refreshRequested` is emitted when the refresh control is clicked. `actionRequested` is emitted with the clicked `ButtonElement` and DOM event:
127
+
128
+ ```ts
129
+ import { ButtonElement } from '@lieselthuriot/widget-renderer';
130
+
131
+ handleAction(event: { element: ButtonElement; event: Event }): void {
132
+ event.event.preventDefault();
133
+ const { method, url, body, requestRefresh } = event.element;
134
+ // Perform the application-specific request here.
135
+ console.log(method, url, body, requestRefresh);
136
+ }
137
+ ```
138
+
139
+ ### Localization
140
+
141
+ 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.
142
+
143
+ ```ts
144
+ import {
145
+ germanWidgetRendererLocale,
146
+ WidgetRendererLocaleOverrides,
147
+ } from '@lieselthuriot/widget-renderer';
148
+
149
+ readonly rendererLocale: WidgetRendererLocaleOverrides = {
150
+ ...germanWidgetRendererLocale,
151
+ };
152
+ ```
153
+
154
+ Built-in `germanWidgetRendererLocale`, `frenchWidgetRendererLocale`, and `dutchWidgetRendererLocale` values are available. For a small customization, override only the required values:
155
+
156
+ ```ts
157
+ import { WidgetRendererLocaleOverrides } from '@lieselthuriot/widget-renderer';
158
+
159
+ readonly rendererLocale: WidgetRendererLocaleOverrides = {
160
+ aria: {
161
+ showSensitiveData: 'Afficher les données privées',
162
+ chart: {
163
+ noDataSummary: (title) => `${title ? `${title} : ` : ''}aucune donnée.`,
164
+ },
165
+ },
166
+ };
167
+ ```
168
+
169
+ ```html
170
+ <widget-renderer [widget]="widget" [locale]="rendererLocale" />
171
+ ```
172
+
173
+ The `aria` locale covers the loading status, toolbar, sensitive-content controls, image failure fallback, chart and progress accessible names, generated chart summaries, chart data point announcements, and progress segment labels. Locale changes update the rendered labels without changing widget-provided content.
174
+
175
+ ## Widget contract
176
+
177
+ Every input must use `version: 2` and include:
178
+
179
+ | Field | Description |
180
+ | ------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------- |
181
+ | `id` | A stable identifier for the widget |
182
+ | `size` | `mini`, `compact`, `square`, `wide`, `mega`, `banner`, `hero`, or `tall`; dimensions include the 8px layout gap |
183
+ | `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 |
184
+ | `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 |
185
+ | `background` | Color, optional image, sizing, position, repeat, overlay, and sensitivity settings |
186
+ | `refresh` | Whether to show a refresh action and its accessible label |
187
+ | `elements` | An array of supported widget elements |
188
+
189
+ Element positions are zero-based. For example, `row: 0` and `column: 0` place an element in the first grid cell.
190
+
191
+ ### Supported elements
192
+
193
+ - `text`: formatted text with alignment, wrapping, font styling, color, background, radius, and padding
194
+ - `image`: remote or local image with alternative text, fit mode, radius, and overlay color
195
+ - `button`: `get` or `post` action metadata, optional body, solid or outline styling, and hover behavior
196
+ - `divider`: horizontal or vertical separator
197
+ - `pie`, `donut`, and `funnel`: labeled data points with optional legends and per-point colors
198
+ - `gauge`: a value within a minimum and maximum range
199
+ - `bar` and `line`: labeled series data with axis and line styling
200
+ - `progress`: a range-based progress bar with label, suffix, legend, and active animation options
201
+
202
+ Editor-only `id` fields are not required. The renderer accepts `WidgetContract` data and normalizes optional button fields and text padding before rendering.
203
+
204
+ ### Sensitive content
205
+
206
+ 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.
207
+
208
+ > **Warning:** Sensitive-content handling is a display and privacy feature, not encryption. Sensitive values remain in the JavaScript object supplied by the consuming application.
209
+
210
+ ## Validation and normalization
211
+
212
+ Use the exported helpers when data enters your application:
213
+
214
+ ```ts
215
+ import {
216
+ normalizeWidgetInput,
217
+ parseWidgetInput,
218
+ safeParseWidgetInput,
219
+ } from '@lieselthuriot/widget-renderer';
220
+
221
+ const result = safeParseWidgetInput(payload);
222
+
223
+ if (!result.success) {
224
+ console.error(result.issues);
225
+ } else {
226
+ const widget = normalizeWidgetInput(result.data);
227
+ }
228
+
229
+ // Throws WidgetValidationError when the payload is invalid.
230
+ const parsed = parseWidgetInput(payload);
231
+ ```
232
+
233
+ `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.
234
+
235
+ ### External validation rules
236
+
237
+ Add your own checks with `validationRules`. Rules run only after the built-in validation passes and receive the typed widget. Each rule returns `WidgetValidationIssue` entries; return an empty array when the widget is allowed.
238
+
239
+ ```ts
240
+ import { WidgetValidationRuleInput } from '@lieselthuriot/widget-renderer';
241
+
242
+ const allowedColors = new Set(['#ffffff', '#1f2937']);
243
+
244
+ export const rules: WidgetValidationRuleInput[] = [
245
+ // A plain function is a blocking rule.
246
+ (widget) =>
247
+ widget.background.image
248
+ ? [{ path: 'background.image', message: 'Background images are not allowed' }]
249
+ : [],
250
+ // Report-only rules let the widget render and only expose their issues.
251
+ {
252
+ mode: 'report',
253
+ validate: (widget) =>
254
+ widget.background.color && !allowedColors.has(widget.background.color.toLowerCase())
255
+ ? [{ path: 'background.color', message: 'Color is not allowed' }]
256
+ : [],
257
+ },
258
+ ];
259
+ ```
260
+
261
+ ```html
262
+ <widget-renderer [widget]="widget" [validationRules]="rules" />
263
+ ```
264
+
265
+ - `mode: 'block'` (the default): the renderer shows the safe empty widget and the validation overlay.
266
+ - `mode: 'report'`: the widget renders normally; the issue is only exposed through `validationIssues`.
267
+ - Rule issues get a `severity` of `block` or `report`. A rule that throws produces one issue with its own mode.
268
+ - The same rules work outside the component: `safeParseWidgetInput(payload, { rules })` fails only on blocking issues and returns report-only issues in `result.issues`. `parseWidgetInput(payload, { rules })` throws only on blocking issues.
269
+
270
+ ## Layout fingerprints
271
+
272
+ `computeWidgetFingerprint(payload)` returns a stable ID of the widget's look, such as `wfp:1:3f9c…`, or `null` for invalid input. Use it to approve a layout once and then accept any widget with the same fingerprint.
273
+
274
+ The fingerprint covers the size, grid tracks, background (including its image), element types, positions, order, styles, image sources, and button method and behavior. It ignores content that can differ per visitor: the widget `id`, text, button labels, image alt text, chart titles, chart and progress data, button URLs and bodies, and the refresh label. Only whether text, labels, alt text, and titles are empty is included. Hex colors are compared case-insensitively, and omitted defaults match their explicit values.
275
+
276
+ `WidgetStudio.Contracts` computes the same value with `WidgetFingerprint.Compute`, so a .NET backend can verify fingerprints itself. The version number after `wfp:` (`WIDGET_FINGERPRINT_VERSION`) changes whenever the rules change.
277
+
278
+ ## Public API
279
+
280
+ Import everything from `@lieselthuriot/widget-renderer`; internal file paths are not part of the public API. The entry point exports:
281
+
282
+ - `WidgetRenderer`, the standalone rendering component, and its `WidgetLoading` input type. Element and chart implementation components are internal.
283
+ - Widget model types, including `WidgetContract`, `WidgetInput`, `Widget`, and the element types
284
+ - `normalizeWidgetInput`
285
+ - `safeParseWidgetInput`, `parseWidgetInput`, `isWidgetContract`, `isWidget`, `WidgetValidationError`, and the validation result types
286
+ - `computeWidgetFingerprint` and `WIDGET_FINGERPRINT_VERSION`
287
+ - Grid layout helpers and widget dimensions, such as `widgetWidth`, `widgetHeight`, `gridTemplate`, and `elementGridStyle`
288
+ - Locales: `defaultWidgetRendererLocale`, `germanWidgetRendererLocale`, `frenchWidgetRendererLocale`, `dutchWidgetRendererLocale`, `WidgetRendererLocale`, and `WidgetRendererLocaleOverrides`
289
+ - `chartAriaLabel` and `chartAccessibilitySummary`
290
+
291
+ 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).
292
+
293
+ ## Contributing
294
+
295
+ 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).
296
+
297
+ ## License
298
+
299
+ [MIT](https://github.com/LieselThuriot/widget-studio/blob/main/LICENSE)